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,391 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="wswg-json-editor">
|
|
3
|
+
<div class="wswg-json-editor-header">
|
|
4
|
+
<!-- header slot for custom control elements -->
|
|
5
|
+
<slot name="header">
|
|
6
|
+
<!-- no default header content -->
|
|
7
|
+
</slot>
|
|
8
|
+
<!-- wswg toolbar-->
|
|
9
|
+
<PageBuilderToolbar
|
|
10
|
+
v-model:editorViewport="editorViewport"
|
|
11
|
+
v-model:showPageSettings="showPageSettings"
|
|
12
|
+
v-model:activeBlock="activeBlock"
|
|
13
|
+
:hasPageSettings="hasPageSettings"
|
|
14
|
+
>
|
|
15
|
+
<slot name="toolbar">
|
|
16
|
+
<!-- no default toolbar content -->
|
|
17
|
+
</slot>
|
|
18
|
+
</PageBuilderToolbar>
|
|
19
|
+
</div>
|
|
20
|
+
<slot v-if="loading" name="loading">
|
|
21
|
+
<div class="wswg-json-editor-loading">
|
|
22
|
+
<span>Loading...</span>
|
|
23
|
+
</div>
|
|
24
|
+
</slot>
|
|
25
|
+
<!-- WYSIWYG editor -->
|
|
26
|
+
<div v-else class="wswg-json-editor-canvas">
|
|
27
|
+
<!-- Page preview -->
|
|
28
|
+
<div class="wswg-json-editor-canvas-preview">
|
|
29
|
+
<div
|
|
30
|
+
class="mx-auto h-full overflow-hidden rounded-lg bg-white transition-all duration-300"
|
|
31
|
+
:class="{ 'w-full': editorViewport === 'desktop', 'w-96': editorViewport === 'mobile' }"
|
|
32
|
+
>
|
|
33
|
+
<BrowserNavigation v-if="showBrowserBar" :url="url" />
|
|
34
|
+
<div v-if="pageLayout" id="page-preview-viewport" class="h-full overflow-y-auto">
|
|
35
|
+
<component :is="pageLayout">
|
|
36
|
+
<template #default>
|
|
37
|
+
<!-- No blocks found -->
|
|
38
|
+
<EmptyState
|
|
39
|
+
v-if="!pageData[blocksKey]?.length"
|
|
40
|
+
v-model:showAddBlockMenu="showAddBlockMenu"
|
|
41
|
+
:editable="editable"
|
|
42
|
+
@block-added="handleAddBlock"
|
|
43
|
+
/>
|
|
44
|
+
<!-- Blocks found -->
|
|
45
|
+
<div v-else id="page-blocks-wrapper">
|
|
46
|
+
<div v-for="(block, blockIndex) in pageData[blocksKey]" :key="block.id">
|
|
47
|
+
<BlockComponent
|
|
48
|
+
:block="block"
|
|
49
|
+
:blockIndex="blockIndex"
|
|
50
|
+
:activeBlock="activeBlock"
|
|
51
|
+
:editable="editable"
|
|
52
|
+
:hoveredBlockId="hoveredBlockId"
|
|
53
|
+
@hover-block="setHoveredBlockId"
|
|
54
|
+
@click-block="handleBlockClick"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
</component>
|
|
60
|
+
</div>
|
|
61
|
+
<p v-else>No layout component found</p>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Resizable divider -->
|
|
66
|
+
<ResizeHandle @sidebar-width="handleSidebarWidth" />
|
|
67
|
+
|
|
68
|
+
<!-- Sidebar -->
|
|
69
|
+
<PageBuilderSidebar
|
|
70
|
+
v-model="pageData"
|
|
71
|
+
v-model:activeBlock="activeBlock"
|
|
72
|
+
v-model:hoveredBlockId="hoveredBlockId"
|
|
73
|
+
v-model:showPageSettings="showPageSettings"
|
|
74
|
+
v-model:showAddBlockMenu="showAddBlockMenu"
|
|
75
|
+
:editable="editable"
|
|
76
|
+
:blocksKey="blocksKey"
|
|
77
|
+
:settingsKey="settingsKey"
|
|
78
|
+
:style="{ width: sidebarWidth + 'px' }"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script setup lang="ts">
|
|
85
|
+
import { ref, shallowRef, withDefaults, watch, type Component, onBeforeMount, nextTick, computed } from "vue";
|
|
86
|
+
import PageBuilderToolbar from "../PageBuilderToolbar/PageBuilderToolbar.vue";
|
|
87
|
+
import ResizeHandle from "../ResizeHandle/ResizeHandle.vue";
|
|
88
|
+
import PageBuilderSidebar from "../PageBuilderSidebar/PageBuilderSidebar.vue";
|
|
89
|
+
import BrowserNavigation from "../BrowserNavigation/BrowserNavigation.vue";
|
|
90
|
+
import BlockComponent from "../BlockComponent/BlockComponent.vue";
|
|
91
|
+
import EmptyState from "../EmptyState/EmptyState.vue";
|
|
92
|
+
import { getBlockComponent, getLayouts } from "../../util/registry";
|
|
93
|
+
import type { Block } from "../../types/Block";
|
|
94
|
+
import Sortable from "sortablejs";
|
|
95
|
+
|
|
96
|
+
const props = withDefaults(
|
|
97
|
+
defineProps<{
|
|
98
|
+
editable?: boolean;
|
|
99
|
+
loading?: boolean;
|
|
100
|
+
url?: string;
|
|
101
|
+
showBrowserBar?: boolean;
|
|
102
|
+
blocksKey?: string;
|
|
103
|
+
settingsKey?: string;
|
|
104
|
+
defaultBlockMargin?: "none" | "small" | "medium" | "large";
|
|
105
|
+
}>(),
|
|
106
|
+
{
|
|
107
|
+
editable: false,
|
|
108
|
+
loading: false,
|
|
109
|
+
url: "",
|
|
110
|
+
showBrowserBar: false,
|
|
111
|
+
blocksKey: "blocks",
|
|
112
|
+
settingsKey: "settings",
|
|
113
|
+
defaultBlockMargin: "none",
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const editorViewport = ref<"desktop" | "mobile">("desktop");
|
|
118
|
+
const showPageSettings = ref(false);
|
|
119
|
+
const showAddBlockMenu = ref(false);
|
|
120
|
+
const activeBlock = ref<any>(null);
|
|
121
|
+
const isSorting = ref(false);
|
|
122
|
+
const hoveredBlockId = ref<string | null>(null);
|
|
123
|
+
const sidebarWidth = ref(380); // Default sidebar width (380px)
|
|
124
|
+
|
|
125
|
+
// Model value for the JSON page data
|
|
126
|
+
const pageData = defineModel<any>();
|
|
127
|
+
|
|
128
|
+
// Layout component - dynamically imported from page-builder directory
|
|
129
|
+
// Using shallowRef to avoid making the component reactive (performance optimization)
|
|
130
|
+
const pageLayout = shallowRef<Component | undefined>(undefined);
|
|
131
|
+
|
|
132
|
+
// Apply the sidebar width from the resize handle
|
|
133
|
+
function handleSidebarWidth(width: number) {
|
|
134
|
+
sidebarWidth.value = width;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Load layout component dynamically from @page-builder/layout/
|
|
138
|
+
async function loadLayout(layoutName: string | undefined) {
|
|
139
|
+
// Use "default" layout if no layout is provided
|
|
140
|
+
const layout = layoutName || "default";
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Try to import the layout from @page-builder/layout/
|
|
144
|
+
// This alias is configured in the consuming app (admin)
|
|
145
|
+
const layoutModule = await import(`@page-builder/layout/${layout}.vue`);
|
|
146
|
+
pageLayout.value = layoutModule.default;
|
|
147
|
+
await nextTick();
|
|
148
|
+
initSortable();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// Layout doesn't exist, return undefined
|
|
151
|
+
console.warn(`Layout "${layout}" not found in @page-builder/layout/`, error);
|
|
152
|
+
pageLayout.value = undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if the page has settings
|
|
157
|
+
const hasPageSettings = computed(() => {
|
|
158
|
+
// Show page settings if there are multiple layouts to choose from
|
|
159
|
+
const layouts = getLayouts();
|
|
160
|
+
if (Object.keys(layouts).length > 1) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// If the layout has settings
|
|
165
|
+
if (pageData.value?.[props.settingsKey]) {
|
|
166
|
+
if (Object.keys(pageData.value?.[props.settingsKey]).length > 0) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return false;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
function handleBlockClick(block: Block | null) {
|
|
175
|
+
activeBlock.value = block;
|
|
176
|
+
showPageSettings.value = false;
|
|
177
|
+
hoveredBlockId.value = null;
|
|
178
|
+
showAddBlockMenu.value = false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handleAddBlock(blockType: string, insertIndex?: number) {
|
|
182
|
+
// Ensure blocks array exists
|
|
183
|
+
if (!pageData.value[props.blocksKey]) {
|
|
184
|
+
pageData.value[props.blocksKey] = [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Record if this is an add from the EmptyState
|
|
188
|
+
const isAddFromEmptyState = insertIndex === undefined;
|
|
189
|
+
|
|
190
|
+
// Create a new block object
|
|
191
|
+
const newBlock = {
|
|
192
|
+
id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
193
|
+
type: blockType,
|
|
194
|
+
margin: props.defaultBlockMargin
|
|
195
|
+
? { top: props.defaultBlockMargin, bottom: props.defaultBlockMargin }
|
|
196
|
+
: undefined,
|
|
197
|
+
};
|
|
198
|
+
// Get the default prop values from the block component
|
|
199
|
+
const blockComponent = getBlockComponent(blockType);
|
|
200
|
+
if (blockComponent?.props) {
|
|
201
|
+
// loop props and set their default value
|
|
202
|
+
Object.entries(blockComponent.props).forEach(([key, value]: [string, any]) => {
|
|
203
|
+
if (value.default) {
|
|
204
|
+
if (typeof value.default === "function") {
|
|
205
|
+
newBlock[key as keyof typeof newBlock] = value.default();
|
|
206
|
+
} else {
|
|
207
|
+
newBlock[key as keyof typeof newBlock] = value.default;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Add the new block at the specified index or at the end
|
|
214
|
+
if (insertIndex !== undefined) {
|
|
215
|
+
pageData.value[props.blocksKey].splice(insertIndex, 0, newBlock);
|
|
216
|
+
} else {
|
|
217
|
+
pageData.value[props.blocksKey].push(newBlock);
|
|
218
|
+
// Set the new block as active only when adding to the end (from EmptyState)
|
|
219
|
+
activeBlock.value = newBlock;
|
|
220
|
+
showAddBlockMenu.value = false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// finally, if this is an add from the EmptyState, we need to trigger a re-render and initialise the sortable
|
|
224
|
+
if (isAddFromEmptyState) {
|
|
225
|
+
nextTick(() => {
|
|
226
|
+
// The component will re-render with the new block data
|
|
227
|
+
initSortable();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return newBlock;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function setHoveredBlockId(id: string | null) {
|
|
235
|
+
if (isSorting.value) return;
|
|
236
|
+
hoveredBlockId.value = id;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Watch for changes in pageData layout and load the corresponding component
|
|
240
|
+
watch(
|
|
241
|
+
() => pageData.value?.[props.settingsKey]?.layout,
|
|
242
|
+
(layoutName) => {
|
|
243
|
+
loadLayout(layoutName);
|
|
244
|
+
},
|
|
245
|
+
{ immediate: true }
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Watch for activeBlock changes and scroll to the corresponding block in the preview
|
|
249
|
+
watch(
|
|
250
|
+
() => activeBlock.value?.id,
|
|
251
|
+
async (blockId) => {
|
|
252
|
+
if (!blockId) return;
|
|
253
|
+
|
|
254
|
+
// Wait for DOM to update
|
|
255
|
+
await nextTick();
|
|
256
|
+
|
|
257
|
+
// Find the block element by data-block-id
|
|
258
|
+
const blockElement = document.querySelector(`[data-block-id="${blockId}"]`) as HTMLElement;
|
|
259
|
+
if (!blockElement) return;
|
|
260
|
+
|
|
261
|
+
// Scroll the block into view, centered in the scrollable container
|
|
262
|
+
blockElement.scrollIntoView({
|
|
263
|
+
behavior: "smooth",
|
|
264
|
+
block: "center",
|
|
265
|
+
inline: "nearest",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
function initSortable() {
|
|
271
|
+
const sortableBlocksWrapper = document.getElementById("page-blocks-wrapper");
|
|
272
|
+
if (!sortableBlocksWrapper) return;
|
|
273
|
+
new Sortable(sortableBlocksWrapper, {
|
|
274
|
+
animation: 150,
|
|
275
|
+
ghostClass: "sortable-ghost",
|
|
276
|
+
chosenClass: "sortable-chosen",
|
|
277
|
+
dragClass: "sortable-drag",
|
|
278
|
+
group: "page-blocks",
|
|
279
|
+
onStart: () => {
|
|
280
|
+
isSorting.value = true;
|
|
281
|
+
},
|
|
282
|
+
onAdd: (event: any) => {
|
|
283
|
+
// This fires when an item is added from another list (drag from sidebar)
|
|
284
|
+
const { item: draggedElement, newIndex } = event;
|
|
285
|
+
const blockType = draggedElement.getAttribute("data-block-type");
|
|
286
|
+
|
|
287
|
+
if (blockType) {
|
|
288
|
+
// Use the consolidated handleAddBlock function
|
|
289
|
+
handleAddBlock(blockType, newIndex);
|
|
290
|
+
|
|
291
|
+
// Remove the cloned HTML element that SortableJS added
|
|
292
|
+
draggedElement.remove();
|
|
293
|
+
|
|
294
|
+
// Force Vue to re-render by triggering reactivity
|
|
295
|
+
nextTick(() => {
|
|
296
|
+
// The component will re-render with the new block data
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
onEnd: (event: any) => {
|
|
301
|
+
isSorting.value = false;
|
|
302
|
+
const { oldIndex, newIndex } = event;
|
|
303
|
+
|
|
304
|
+
// Only handle reordering if this wasn't an add operation (oldIndex will be null for adds)
|
|
305
|
+
if (oldIndex !== null && oldIndex !== newIndex) {
|
|
306
|
+
const movedBlock = pageData.value?.[props.blocksKey]?.splice(oldIndex, 1)[0];
|
|
307
|
+
pageData.value?.[props.blocksKey]?.splice(newIndex, 0, movedBlock);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
onBeforeMount(() => {
|
|
314
|
+
if (!pageData.value?.[props.settingsKey]) {
|
|
315
|
+
if (pageData.value) {
|
|
316
|
+
pageData.value[props.settingsKey] = {};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!pageData.value?.[props.settingsKey]?.layout) {
|
|
321
|
+
if (pageData.value && pageData.value[props.settingsKey]) {
|
|
322
|
+
pageData.value[props.settingsKey].layout = "default";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Sanitise the layout data (must be a valid layout name string)
|
|
327
|
+
if (pageData.value?.[props.settingsKey]?.layout) {
|
|
328
|
+
const layouts = getLayouts();
|
|
329
|
+
const settings = pageData.value?.[props.settingsKey];
|
|
330
|
+
if (settings) {
|
|
331
|
+
if (typeof settings.layout !== "string") {
|
|
332
|
+
settings.layout = "default";
|
|
333
|
+
} else if (!layouts[settings.layout]) {
|
|
334
|
+
settings.layout = "default";
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!pageData.value?.[props.blocksKey]) {
|
|
340
|
+
if (pageData.value) {
|
|
341
|
+
pageData.value[props.blocksKey] = [];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
</script>
|
|
346
|
+
|
|
347
|
+
<style lang="scss">
|
|
348
|
+
.wswg-json-editor {
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
width: 100%;
|
|
352
|
+
max-width: 100%;
|
|
353
|
+
height: 100vh;
|
|
354
|
+
|
|
355
|
+
&-header {
|
|
356
|
+
position: sticky;
|
|
357
|
+
top: 0;
|
|
358
|
+
z-index: 20;
|
|
359
|
+
background-color: #fff;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
&-loading {
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
justify-content: center;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
&-canvas {
|
|
369
|
+
display: flex;
|
|
370
|
+
flex: 1;
|
|
371
|
+
flex-grow: 1;
|
|
372
|
+
flex-shrink: 0;
|
|
373
|
+
height: 100%;
|
|
374
|
+
overflow-y: auto;
|
|
375
|
+
background-color: #6a6a6a;
|
|
376
|
+
|
|
377
|
+
&-preview {
|
|
378
|
+
flex: 1;
|
|
379
|
+
flex-grow: 1;
|
|
380
|
+
flex-shrink: 0;
|
|
381
|
+
padding: 2rem;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
&-sidebar {
|
|
385
|
+
min-width: 300px;
|
|
386
|
+
padding: 2rem;
|
|
387
|
+
background: #fff;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
</style>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Export field configuration utilities first (before any side effects)
|
|
2
|
+
// This ensures createField is available immediately without waiting for CSS or component imports
|
|
3
|
+
export { createField } from "./util/fieldConfig";
|
|
4
|
+
export type { EditorFieldConfig, ValidatorFunction } from "./util/fieldConfig";
|
|
5
|
+
export { getLayouts } from "./util/registry";
|
|
6
|
+
export { validateField, validateAllFields } from "./util/validation";
|
|
7
|
+
|
|
8
|
+
// Export components (component exports don't cause side effects until used)
|
|
9
|
+
export { default as WswgJsonEditor } from "./components/WswgJsonEditor/WswgJsonEditor.vue";
|
|
10
|
+
// Export PageRenderer separately - it doesn't use the registry, so it won't trigger field loading
|
|
11
|
+
export { default as PageRenderer } from "./components/PageRenderer/PageRenderer.vue";
|
|
12
|
+
|
|
13
|
+
// Import CSS - Vite will extract this to dist/style.css during build
|
|
14
|
+
// Consuming apps should import "vue-wswg-editor/style.css"
|
|
15
|
+
import "./style.css";
|
package/src/shims.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
declare module "*.vue" {
|
|
2
|
+
import type { DefineComponent } from "vue";
|
|
3
|
+
|
|
4
|
+
const component: DefineComponent<object, object, any>;
|
|
5
|
+
export default component;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Image imports - Vite returns the URL as a string
|
|
9
|
+
declare module "*.jpg" {
|
|
10
|
+
const src: string;
|
|
11
|
+
export default src;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module "*.jpeg" {
|
|
15
|
+
const src: string;
|
|
16
|
+
export default src;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module "*.png" {
|
|
20
|
+
const src: string;
|
|
21
|
+
export default src;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module "*.gif" {
|
|
25
|
+
const src: string;
|
|
26
|
+
export default src;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
declare module "*.svg" {
|
|
30
|
+
const src: string;
|
|
31
|
+
export default src;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare module "*.webp" {
|
|
35
|
+
const src: string;
|
|
36
|
+
export default src;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Vite import.meta.glob type definitions
|
|
40
|
+
interface ImportMeta {
|
|
41
|
+
glob<T = any>(
|
|
42
|
+
pattern: string | string[],
|
|
43
|
+
options?: {
|
|
44
|
+
eager?: boolean;
|
|
45
|
+
import?: string;
|
|
46
|
+
query?: string | Record<string, string | number | boolean>;
|
|
47
|
+
exclude?: string | string[];
|
|
48
|
+
as?: "url" | "raw";
|
|
49
|
+
}
|
|
50
|
+
): Record<string, () => Promise<T>> | Record<string, T> | Record<string, string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// SortableJS type declaration - reference @types/sortablejs and re-export as ESM default
|
|
54
|
+
/// <reference types="sortablejs" />
|
|
55
|
+
declare module "sortablejs" {
|
|
56
|
+
// Reference the Sortable class from @types/sortablejs via triple-slash directive
|
|
57
|
+
// This allows default import with esModuleInterop
|
|
58
|
+
const Sortable: {
|
|
59
|
+
new (element: HTMLElement, options?: any): any;
|
|
60
|
+
active: any;
|
|
61
|
+
utils: any;
|
|
62
|
+
mount(...plugins: any[]): void;
|
|
63
|
+
create(element: HTMLElement, options?: any): any;
|
|
64
|
+
dragged: HTMLElement | null;
|
|
65
|
+
ghost: HTMLElement | null;
|
|
66
|
+
clone: HTMLElement | null;
|
|
67
|
+
get(element: HTMLElement): any;
|
|
68
|
+
readonly version: string;
|
|
69
|
+
};
|
|
70
|
+
export = Sortable;
|
|
71
|
+
export default Sortable;
|
|
72
|
+
}
|
package/src/style.css
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom component type with additional block-specific properties
|
|
3
|
+
* These properties are set via defineOptions() in Vue components
|
|
4
|
+
*/
|
|
5
|
+
export type Block = Component & {
|
|
6
|
+
__name: string; // The autogenerated name of the block
|
|
7
|
+
props: Record<string, any>;
|
|
8
|
+
id: string;
|
|
9
|
+
type: string;
|
|
10
|
+
// Defined in template
|
|
11
|
+
label?: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
// fields file
|
|
14
|
+
fields?: Record<string, any>;
|
|
15
|
+
// Auto generated from the component path
|
|
16
|
+
directory?: string; // Where the block component is located (e.g., "@page-builder/blocks/hero-section")
|
|
17
|
+
// Margin
|
|
18
|
+
margin?: { top?: string; bottom?: string };
|
|
19
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom component type with additional layout-specific properties
|
|
3
|
+
* These properties are set via defineOptions() in Vue components
|
|
4
|
+
*/
|
|
5
|
+
export type Layout = Component & {
|
|
6
|
+
__name: string; // The autogenerated name of the layout
|
|
7
|
+
label: string; // The custom name of the layout
|
|
8
|
+
fields?: Record<string, EditorFieldConfig>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Component } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validator function that returns:
|
|
5
|
+
* - `true` if the value is valid
|
|
6
|
+
* - `false` if the value is invalid (generic error)
|
|
7
|
+
* - `string` if the value is invalid (specific error message)
|
|
8
|
+
*/
|
|
9
|
+
export type ValidatorFunction = (value: any) => Promise<boolean | string>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Editor field types for the page builder sidebar
|
|
13
|
+
*/
|
|
14
|
+
export type EditorFieldType =
|
|
15
|
+
| "text" // ✅
|
|
16
|
+
| "textarea" // ✅
|
|
17
|
+
| "number" // ✅
|
|
18
|
+
| "boolean" // ✅
|
|
19
|
+
| "email" // ✅
|
|
20
|
+
| "url" // ✅
|
|
21
|
+
| "select" // ✅
|
|
22
|
+
| "checkbox" // ✅
|
|
23
|
+
| "radio" // ✅
|
|
24
|
+
| "color" // ✅
|
|
25
|
+
| "range" // ✅
|
|
26
|
+
| "repeater" // ✅
|
|
27
|
+
| "margin" // ✅
|
|
28
|
+
| "custom"; // 🔌 (image, json, video, richtext, etc)
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Simple utility for defining page builder props with editor field metadata
|
|
32
|
+
* This approach separates Vue props from editor field configuration
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export interface EditorFieldConfig {
|
|
36
|
+
type: EditorFieldType;
|
|
37
|
+
component?: Component; // if providing a custom editor field component
|
|
38
|
+
required?: boolean;
|
|
39
|
+
default?: any;
|
|
40
|
+
label?: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
placeholder?: string;
|
|
43
|
+
rows?: number;
|
|
44
|
+
options?: Array<{ label: string; value: any; id: string }>;
|
|
45
|
+
step?: number;
|
|
46
|
+
hidden?: boolean;
|
|
47
|
+
group?: string;
|
|
48
|
+
clearable?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Validator function that returns:
|
|
51
|
+
* - `true` if the value is valid
|
|
52
|
+
* - `false` if the value is invalid (generic error)
|
|
53
|
+
* - `string` if the value is invalid (specific error message)
|
|
54
|
+
*/
|
|
55
|
+
validator?: ValidatorFunction;
|
|
56
|
+
// Repeater-specific properties
|
|
57
|
+
repeaterFields?: Record<string, EditorFieldConfig>;
|
|
58
|
+
repeaterFieldLabel?: string; // attribute key for the repeater field label
|
|
59
|
+
// String length validation
|
|
60
|
+
minLength?: number;
|
|
61
|
+
maxLength?: number;
|
|
62
|
+
// Number validation
|
|
63
|
+
min?: number;
|
|
64
|
+
max?: number;
|
|
65
|
+
// Repeater validation
|
|
66
|
+
minItems?: number;
|
|
67
|
+
maxItems?: number;
|
|
68
|
+
// Image specific
|
|
69
|
+
// Whether the image is responsive eg: xs, sm, md, lg, xl, primary
|
|
70
|
+
responsive?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Helper function to create editor field configurations
|
|
75
|
+
*/
|
|
76
|
+
export const createField = {
|
|
77
|
+
custom: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
78
|
+
type: "custom",
|
|
79
|
+
...config,
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
text: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
83
|
+
type: "text",
|
|
84
|
+
...config,
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
textarea: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
88
|
+
type: "textarea",
|
|
89
|
+
...config,
|
|
90
|
+
}),
|
|
91
|
+
|
|
92
|
+
number: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
93
|
+
type: "number",
|
|
94
|
+
...config,
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
boolean: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
98
|
+
type: "boolean",
|
|
99
|
+
...config,
|
|
100
|
+
}),
|
|
101
|
+
|
|
102
|
+
email: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
103
|
+
type: "email",
|
|
104
|
+
...config,
|
|
105
|
+
}),
|
|
106
|
+
|
|
107
|
+
url: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
108
|
+
type: "url",
|
|
109
|
+
...config,
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
select: <T>(
|
|
113
|
+
options: Array<{ label: string; value: T; id: string }>,
|
|
114
|
+
config: Partial<Omit<EditorFieldConfig, "options">> = {}
|
|
115
|
+
): EditorFieldConfig => ({
|
|
116
|
+
type: "select",
|
|
117
|
+
options,
|
|
118
|
+
...config,
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
radio: <T>(
|
|
122
|
+
options: Array<{ label: string; value: T; id: string }>,
|
|
123
|
+
config: Partial<Omit<EditorFieldConfig, "options">> = {}
|
|
124
|
+
): EditorFieldConfig => ({
|
|
125
|
+
type: "radio",
|
|
126
|
+
options,
|
|
127
|
+
...config,
|
|
128
|
+
}),
|
|
129
|
+
|
|
130
|
+
checkbox: <T>(
|
|
131
|
+
options: Array<{ label: string; value: T; id: string }>,
|
|
132
|
+
config: Partial<Omit<EditorFieldConfig, "options">> = {}
|
|
133
|
+
): EditorFieldConfig => ({
|
|
134
|
+
type: "checkbox",
|
|
135
|
+
options,
|
|
136
|
+
...config,
|
|
137
|
+
}),
|
|
138
|
+
|
|
139
|
+
color: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
140
|
+
type: "color",
|
|
141
|
+
...config,
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
range: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
145
|
+
type: "range",
|
|
146
|
+
...config,
|
|
147
|
+
}),
|
|
148
|
+
|
|
149
|
+
repeater: (
|
|
150
|
+
repeaterFields: Record<string, EditorFieldConfig>,
|
|
151
|
+
config: Partial<EditorFieldConfig> = {}
|
|
152
|
+
): EditorFieldConfig => ({
|
|
153
|
+
type: "repeater",
|
|
154
|
+
repeaterFields,
|
|
155
|
+
...config,
|
|
156
|
+
}),
|
|
157
|
+
|
|
158
|
+
margin: (config: Partial<EditorFieldConfig> = {}): EditorFieldConfig => ({
|
|
159
|
+
type: "margin",
|
|
160
|
+
options: [
|
|
161
|
+
{ label: "None", value: "none", id: "margin-none" },
|
|
162
|
+
{ label: "Small", value: "small", id: "margin-small" },
|
|
163
|
+
{ label: "Medium", value: "medium", id: "margin-medium" },
|
|
164
|
+
{ label: "Large", value: "large", id: "margin-large" },
|
|
165
|
+
],
|
|
166
|
+
default: "none",
|
|
167
|
+
description: "Vertical margin spacing",
|
|
168
|
+
group: "settings",
|
|
169
|
+
placeholder: "Select a margin...",
|
|
170
|
+
label: "Margin",
|
|
171
|
+
...config,
|
|
172
|
+
}),
|
|
173
|
+
};
|