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.
Files changed (61) hide show
  1. package/README.md +91 -0
  2. package/dist/style.css +1 -0
  3. package/dist/types/components/AddBlockItem/AddBlockItem.vue.d.ts +6 -0
  4. package/dist/types/components/BlockBrowser/BlockBrowser.vue.d.ts +2 -0
  5. package/dist/types/components/BlockComponent/BlockComponent.vue.d.ts +15 -0
  6. package/dist/types/components/BlockEditorFieldNode/BlockEditorFieldNode.vue.d.ts +15 -0
  7. package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +15 -0
  8. package/dist/types/components/BlockMarginFieldNode/BlockMarginNode.vue.d.ts +23 -0
  9. package/dist/types/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue.d.ts +15 -0
  10. package/dist/types/components/BrowserNavigation/BrowserNavigation.vue.d.ts +5 -0
  11. package/dist/types/components/EmptyState/EmptyState.vue.d.ts +15 -0
  12. package/dist/types/components/PageBlockList/PageBlockList.vue.d.ts +19 -0
  13. package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +30 -0
  14. package/dist/types/components/PageBuilderToolbar/PageBuilderToolbar.vue.d.ts +28 -0
  15. package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +6 -0
  16. package/dist/types/components/PageRenderer/blockModules.d.ts +1 -0
  17. package/dist/types/components/PageSettings/PageSettings.vue.d.ts +15 -0
  18. package/dist/types/components/ResizeHandle/ResizeHandle.vue.d.ts +6 -0
  19. package/dist/types/components/WswgJsonEditor/WswgJsonEditor.test.d.ts +1 -0
  20. package/dist/types/components/WswgJsonEditor/WswgJsonEditor.vue.d.ts +40 -0
  21. package/dist/types/index.d.ts +7 -0
  22. package/dist/types/tsconfig.tsbuildinfo +1 -0
  23. package/dist/types/util/fieldConfig.d.ts +82 -0
  24. package/dist/types/util/helpers.d.ts +28 -0
  25. package/dist/types/util/registry.d.ts +21 -0
  26. package/dist/types/util/validation.d.ts +15 -0
  27. package/dist/vue-wswg-editor.es.js +3377 -0
  28. package/package.json +85 -0
  29. package/src/assets/images/empty-state.jpg +0 -0
  30. package/src/assets/styles/_mixins.scss +73 -0
  31. package/src/assets/styles/main.css +3 -0
  32. package/src/components/AddBlockItem/AddBlockItem.vue +50 -0
  33. package/src/components/BlockBrowser/BlockBrowser.vue +69 -0
  34. package/src/components/BlockComponent/BlockComponent.vue +186 -0
  35. package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +378 -0
  36. package/src/components/BlockEditorFields/BlockEditorFields.vue +91 -0
  37. package/src/components/BlockMarginFieldNode/BlockMarginNode.vue +132 -0
  38. package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +217 -0
  39. package/src/components/BrowserNavigation/BrowserNavigation.vue +27 -0
  40. package/src/components/EmptyState/EmptyState.vue +94 -0
  41. package/src/components/PageBlockList/PageBlockList.vue +103 -0
  42. package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +241 -0
  43. package/src/components/PageBuilderToolbar/PageBuilderToolbar.vue +63 -0
  44. package/src/components/PageRenderer/PageRenderer.vue +65 -0
  45. package/src/components/PageRenderer/blockModules-alternative.ts.example +9 -0
  46. package/src/components/PageRenderer/blockModules-manual.ts.example +19 -0
  47. package/src/components/PageRenderer/blockModules-runtime.ts.example +23 -0
  48. package/src/components/PageRenderer/blockModules.ts +3 -0
  49. package/src/components/PageSettings/PageSettings.vue +86 -0
  50. package/src/components/ResizeHandle/ResizeHandle.vue +105 -0
  51. package/src/components/WswgJsonEditor/WswgJsonEditor.test.ts +43 -0
  52. package/src/components/WswgJsonEditor/WswgJsonEditor.vue +391 -0
  53. package/src/index.ts +15 -0
  54. package/src/shims.d.ts +72 -0
  55. package/src/style.css +3 -0
  56. package/src/types/Block.d.ts +19 -0
  57. package/src/types/Layout.d.ts +9 -0
  58. package/src/util/fieldConfig.ts +173 -0
  59. package/src/util/helpers.ts +176 -0
  60. package/src/util/registry.ts +149 -0
  61. 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
+ }