vue-wswg-editor 0.0.10 → 0.0.12
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 +493 -20
- package/dist/style.css +1 -1
- package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +1 -0
- package/dist/types/components/BlockImageFieldNode/BlockImageNode.vue.d.ts +19 -0
- package/dist/types/components/PageRenderer/blockModules.d.ts +3 -1
- package/dist/types/components/PageRenderer/layoutModules.d.ts +3 -1
- package/dist/types/index.d.ts +2 -3
- package/dist/types/util/fieldConfig.d.ts +5 -1
- package/dist/types/util/registry.d.ts +5 -0
- package/dist/types/util/validation.d.ts +13 -2
- package/dist/vue-wswg-editor.es.js +2278 -1789
- package/package.json +15 -8
- package/src/assets/styles/_mixins.scss +12 -17
- package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +39 -4
- package/src/components/BlockEditorFields/BlockEditorFields.vue +12 -3
- package/src/components/BlockImageFieldNode/BlockImageNode.vue +373 -0
- package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +1 -5
- package/src/components/PageRenderer/PageRenderer.vue +40 -41
- package/src/components/PageRenderer/blockModules.ts +32 -3
- package/src/components/PageRenderer/layoutModules.ts +32 -3
- package/src/components/WswgJsonEditor/WswgJsonEditor.vue +230 -62
- package/src/index.ts +2 -3
- package/src/style.css +10 -3
- package/src/util/fieldConfig.ts +22 -0
- package/src/util/helpers.ts +1 -1
- package/src/util/registry.ts +30 -23
- package/src/util/validation.ts +176 -21
- package/types/vue-wswg-editor.d.ts +161 -0
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<template
|
|
4
|
-
<
|
|
2
|
+
<div id="page-viewport" class="page-renderer-wrapper relative">
|
|
3
|
+
<template v-if="isReady">
|
|
4
|
+
<component :is="layoutComponent" v-if="withLayout && layoutComponent" v-bind="settings">
|
|
5
|
+
<template #default>
|
|
6
|
+
<div id="page-blocks-wrapper">
|
|
7
|
+
<div
|
|
8
|
+
v-for="block in blocks"
|
|
9
|
+
:key="block.id"
|
|
10
|
+
class="block-wrapper"
|
|
11
|
+
:class="{ [getMarginClass(block)]: true }"
|
|
12
|
+
>
|
|
13
|
+
<component :is="getBlock(block.type)" v-bind="block" :key="`block-${block.id}`" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
</component>
|
|
18
|
+
<div v-else id="page-blocks-wrapper">
|
|
5
19
|
<div
|
|
6
20
|
v-for="block in blocks"
|
|
7
21
|
:key="block.id"
|
|
@@ -10,21 +24,17 @@
|
|
|
10
24
|
>
|
|
11
25
|
<component :is="getBlock(block.type)" v-bind="block" :key="`block-${block.id}`" />
|
|
12
26
|
</div>
|
|
27
|
+
<pre>{{ blocks }}</pre>
|
|
13
28
|
</div>
|
|
14
29
|
</template>
|
|
15
|
-
</component>
|
|
16
|
-
<div v-else id="page-blocks-wrapper">
|
|
17
|
-
<div v-for="block in blocks" :key="block.id" class="block-wrapper" :class="{ [getMarginClass(block)]: true }">
|
|
18
|
-
<component :is="getBlock(block.type)" v-bind="block" :key="`block-${block.id}`" />
|
|
19
|
-
</div>
|
|
20
30
|
</div>
|
|
21
31
|
</template>
|
|
22
32
|
|
|
23
33
|
<script setup lang="ts">
|
|
24
|
-
import { type Component, computed, withDefaults } from "vue";
|
|
34
|
+
import { type Component, computed, withDefaults, onBeforeMount, ref } from "vue";
|
|
25
35
|
import { generateNameVariations } from "../../util/helpers";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
36
|
+
import { getBlockModule, loadBlockModules } from "./blockModules";
|
|
37
|
+
import { loadLayoutModules, getLayoutModule } from "./layoutModules";
|
|
28
38
|
import type { Block } from "../../types/Block";
|
|
29
39
|
|
|
30
40
|
const props = withDefaults(
|
|
@@ -37,28 +47,20 @@ const props = withDefaults(
|
|
|
37
47
|
{
|
|
38
48
|
layout: "default",
|
|
39
49
|
settings: () => ({}),
|
|
40
|
-
withLayout:
|
|
50
|
+
withLayout: false,
|
|
41
51
|
}
|
|
42
52
|
);
|
|
43
53
|
|
|
54
|
+
const isReady = ref(false);
|
|
55
|
+
|
|
44
56
|
function getBlock(blockType: string): Component | undefined {
|
|
45
|
-
// Generate name variations and try to find a match
|
|
57
|
+
// Generate name variations and try to find a match
|
|
46
58
|
const nameVariations = generateNameVariations(blockType);
|
|
47
59
|
|
|
48
|
-
//
|
|
49
|
-
for (const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Check if the file path contains the variation followed by .vue
|
|
53
|
-
// e.g., "hero-section" matches "blocks/hero-section/hero-section.vue"
|
|
54
|
-
if (filePath.includes(`${variation}.vue`)) {
|
|
55
|
-
// Extract the default export (the Vue component)
|
|
56
|
-
const component = (module as any).default;
|
|
57
|
-
if (component) {
|
|
58
|
-
return component;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
60
|
+
// Try each variation to find the block component
|
|
61
|
+
for (const variation of nameVariations) {
|
|
62
|
+
const module = getBlockModule(variation);
|
|
63
|
+
if (module) return module;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
return undefined;
|
|
@@ -68,20 +70,10 @@ function getLayout(layoutName: string): Component | undefined {
|
|
|
68
70
|
// Generate name variations and try to find a match in layoutModules keys (file paths)
|
|
69
71
|
const nameVariations = generateNameVariations(layoutName);
|
|
70
72
|
|
|
71
|
-
//
|
|
72
|
-
for (const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Check if the file path contains the variation followed by .vue
|
|
76
|
-
// e.g., "default" matches "layout/default.vue" or "layout/default/default.vue"
|
|
77
|
-
if (filePath.includes(`${variation}.vue`)) {
|
|
78
|
-
// Extract the default export (the Vue component)
|
|
79
|
-
const component = (module as any).default;
|
|
80
|
-
if (component) {
|
|
81
|
-
return component;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
73
|
+
// Try each variation to find the block component
|
|
74
|
+
for (const variation of nameVariations) {
|
|
75
|
+
const module = getLayoutModule(variation);
|
|
76
|
+
if (module) return module;
|
|
85
77
|
}
|
|
86
78
|
|
|
87
79
|
return undefined;
|
|
@@ -107,6 +99,13 @@ function getMarginClass(block: Block): string {
|
|
|
107
99
|
|
|
108
100
|
return [getClass(top, "top"), getClass(bottom, "bottom")].join(" ");
|
|
109
101
|
}
|
|
102
|
+
|
|
103
|
+
onBeforeMount(async () => {
|
|
104
|
+
isReady.value = false;
|
|
105
|
+
await loadBlockModules();
|
|
106
|
+
await loadLayoutModules();
|
|
107
|
+
isReady.value = true;
|
|
108
|
+
});
|
|
110
109
|
</script>
|
|
111
110
|
|
|
112
111
|
<style scoped lang="scss">
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// Import from virtual module created by vue-wswg-editor vite plugin
|
|
2
|
+
// Use lazy initialization to avoid circular dependency issues with createField
|
|
3
|
+
// The import is deferred until first access, ensuring createField is available when blocks load
|
|
4
|
+
import { type Component } from "vue";
|
|
5
|
+
import { getModuleDefault } from "../../util/registry";
|
|
6
|
+
import { markRaw } from "vue";
|
|
7
|
+
let _blockModules: Record<string, Component> | null = null;
|
|
8
|
+
|
|
9
|
+
export async function loadBlockModules(): Promise<Record<string, Component> | null> {
|
|
10
|
+
if (!_blockModules) {
|
|
11
|
+
_blockModules = {};
|
|
12
|
+
// Lazy load virtual modules to prevent initialization order issues
|
|
13
|
+
const { modules: discoveredModules } = await import("vue-wswg-editor:blocks");
|
|
14
|
+
for (const [, module] of Object.entries(discoveredModules)) {
|
|
15
|
+
let resolvedModule = module;
|
|
16
|
+
// If module is a function (lazy-loaded), call it to get the actual module
|
|
17
|
+
if (typeof module === "function") {
|
|
18
|
+
resolvedModule = await module();
|
|
19
|
+
}
|
|
20
|
+
const component = getModuleDefault(resolvedModule);
|
|
21
|
+
if (component && component.__name) {
|
|
22
|
+
const blockType = component.type || component.__name;
|
|
23
|
+
_blockModules[blockType] = markRaw(component);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return _blockModules;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getBlockModule(type: string): Component | undefined {
|
|
31
|
+
return _blockModules?.[type];
|
|
32
|
+
}
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// Import from virtual module created by vue-wswg-editor vite plugin
|
|
2
|
+
// Use lazy initialization to avoid circular dependency issues with createField
|
|
3
|
+
// The import is deferred until first access, ensuring createField is available when blocks load
|
|
4
|
+
import { type Component } from "vue";
|
|
5
|
+
import { getModuleDefault } from "../../util/registry";
|
|
6
|
+
import { markRaw } from "vue";
|
|
7
|
+
let _layoutModules: Record<string, Component> | null = null;
|
|
8
|
+
|
|
9
|
+
export async function loadLayoutModules(): Promise<Record<string, Component> | null> {
|
|
10
|
+
if (!_layoutModules) {
|
|
11
|
+
_layoutModules = {};
|
|
12
|
+
// Lazy load virtual modules to prevent initialization order issues
|
|
13
|
+
const { modules: discoveredModules } = await import("vue-wswg-editor:layouts");
|
|
14
|
+
for (const [, module] of Object.entries(discoveredModules)) {
|
|
15
|
+
let resolvedModule = module;
|
|
16
|
+
// If module is a function (lazy-loaded), call it to get the actual module
|
|
17
|
+
if (typeof module === "function") {
|
|
18
|
+
resolvedModule = await module();
|
|
19
|
+
}
|
|
20
|
+
const component = getModuleDefault(resolvedModule);
|
|
21
|
+
if (component && component.__name) {
|
|
22
|
+
const layoutType = component.type || component.__name;
|
|
23
|
+
_layoutModules[layoutType] = markRaw(component);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return _layoutModules;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getLayoutModule(type: string): Component | undefined {
|
|
31
|
+
return _layoutModules?.[type];
|
|
32
|
+
}
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
<BrowserNavigation v-if="showBrowserBar" class="browser-navigation-bar" :url="url" />
|
|
28
28
|
<div
|
|
29
29
|
v-if="pageLayout"
|
|
30
|
-
id="page-
|
|
31
|
-
class="overflow-hidden rounded-b-lg bg-white"
|
|
30
|
+
id="page-viewport"
|
|
31
|
+
class="relative overflow-hidden rounded-b-lg bg-white"
|
|
32
32
|
:class="{ 'rounded-t-lg': !showBrowserBar }"
|
|
33
33
|
>
|
|
34
|
-
<component :is="pageLayout">
|
|
34
|
+
<component :is="pageLayout" v-bind="pageData?.[settingsKey]">
|
|
35
35
|
<template #default>
|
|
36
36
|
<!-- No blocks found -->
|
|
37
37
|
<EmptyState
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
@block-added="handleAddBlock"
|
|
42
42
|
/>
|
|
43
43
|
<!-- Blocks found -->
|
|
44
|
-
<div v-else id="page-blocks-wrapper">
|
|
44
|
+
<div v-else id="page-blocks-wrapper" ref="pageBlocksWrapperRef">
|
|
45
45
|
<div v-for="(block, blockIndex) in pageData[blocksKey]" :key="block.id">
|
|
46
46
|
<BlockComponent
|
|
47
47
|
:block="block"
|
|
@@ -57,7 +57,36 @@
|
|
|
57
57
|
</template>
|
|
58
58
|
</component>
|
|
59
59
|
</div>
|
|
60
|
-
|
|
60
|
+
<!-- No layout found -->
|
|
61
|
+
<div v-else class="rounded-b-lg bg-white px-5 py-12 md:py-20">
|
|
62
|
+
<div class="mx-auto max-w-md pb-7 text-center">
|
|
63
|
+
<svg
|
|
64
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
65
|
+
fill="none"
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
stroke-width="1.5"
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
class="mx-auto size-20 text-gray-400"
|
|
70
|
+
>
|
|
71
|
+
<path
|
|
72
|
+
stroke-linecap="round"
|
|
73
|
+
stroke-linejoin="round"
|
|
74
|
+
d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
|
|
75
|
+
></path>
|
|
76
|
+
</svg>
|
|
77
|
+
|
|
78
|
+
<h2 class="text-2xl font-bold text-gray-900">No layout found</h2>
|
|
79
|
+
|
|
80
|
+
<p class="mt-4 text-pretty text-gray-700">
|
|
81
|
+
Get started by creating your first layout. It only takes a few seconds.
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
<p class="mt-6 text-sm text-gray-700">
|
|
85
|
+
<a href="#" class="underline hover:text-gray-900">Learn how</a> or
|
|
86
|
+
<a href="#" class="underline hover:text-gray-900">view examples</a>
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
61
90
|
</div>
|
|
62
91
|
</div>
|
|
63
92
|
|
|
@@ -65,7 +94,11 @@
|
|
|
65
94
|
<ResizeHandle @sidebar-width="handleSidebarWidth" />
|
|
66
95
|
|
|
67
96
|
<!-- Sidebar -->
|
|
68
|
-
<div
|
|
97
|
+
<div
|
|
98
|
+
id="page-builder-sidebar"
|
|
99
|
+
class="page-builder-sidebar-wrapper bg-white"
|
|
100
|
+
:style="{ width: sidebarWidth + 'px' }"
|
|
101
|
+
>
|
|
69
102
|
<PageBuilderSidebar
|
|
70
103
|
v-model="pageData"
|
|
71
104
|
v-model:activeBlock="activeBlock"
|
|
@@ -91,16 +124,16 @@ import {
|
|
|
91
124
|
watch,
|
|
92
125
|
type Component,
|
|
93
126
|
onBeforeMount,
|
|
127
|
+
onMounted,
|
|
94
128
|
nextTick,
|
|
95
129
|
computed,
|
|
96
|
-
onMounted,
|
|
97
130
|
} from "vue";
|
|
98
131
|
import ResizeHandle from "../ResizeHandle/ResizeHandle.vue";
|
|
99
132
|
import PageBuilderSidebar from "../PageBuilderSidebar/PageBuilderSidebar.vue";
|
|
100
133
|
import BrowserNavigation from "../BrowserNavigation/BrowserNavigation.vue";
|
|
101
134
|
import BlockComponent from "../BlockComponent/BlockComponent.vue";
|
|
102
135
|
import EmptyState from "../EmptyState/EmptyState.vue";
|
|
103
|
-
import { getBlockComponent, getLayouts } from "../../util/registry";
|
|
136
|
+
import { getBlockComponent, getLayouts, initialiseRegistry } from "../../util/registry";
|
|
104
137
|
import type { Block } from "../../types/Block";
|
|
105
138
|
import Sortable from "sortablejs";
|
|
106
139
|
|
|
@@ -132,6 +165,9 @@ const activeBlock = ref<any>(null);
|
|
|
132
165
|
const isSorting = ref(false);
|
|
133
166
|
const hoveredBlockId = ref<string | null>(null);
|
|
134
167
|
const sidebarWidth = ref(380); // Default sidebar width (380px)
|
|
168
|
+
const sortableInstance = ref<InstanceType<typeof Sortable> | null>(null);
|
|
169
|
+
const pageBlocksWrapperRef = ref<HTMLElement | null>(null);
|
|
170
|
+
const isInitializingSortable = ref(false); // Prevent concurrent initialization
|
|
135
171
|
|
|
136
172
|
// Model value for the JSON page data
|
|
137
173
|
const pageData = defineModel<Record<string, any>>();
|
|
@@ -153,8 +189,8 @@ async function loadLayout(layoutName: string | undefined) {
|
|
|
153
189
|
const availableLayouts = getLayouts();
|
|
154
190
|
const layoutModule = availableLayouts[layout];
|
|
155
191
|
pageLayout.value = layoutModule;
|
|
156
|
-
|
|
157
|
-
|
|
192
|
+
// Don't initialize Sortable here - let the watcher handle it
|
|
193
|
+
// The watcher will react to pageLayout.value changing and check all conditions
|
|
158
194
|
} catch (error) {
|
|
159
195
|
// Layout doesn't exist, return undefined
|
|
160
196
|
console.warn(`Layout "${layout}" not found in @page-builder/layout/`, error);
|
|
@@ -165,6 +201,7 @@ async function loadLayout(layoutName: string | undefined) {
|
|
|
165
201
|
// Check if the page has settings
|
|
166
202
|
const hasPageSettings = computed(() => {
|
|
167
203
|
// Show page settings if there are multiple layouts to choose from
|
|
204
|
+
// Note: This computed might run before registry is initialized, so we check if layouts exist
|
|
168
205
|
const layouts = getLayouts();
|
|
169
206
|
if (Object.keys(layouts).length > 1) {
|
|
170
207
|
return true;
|
|
@@ -187,7 +224,7 @@ function handleBlockClick(block: Block | null) {
|
|
|
187
224
|
showAddBlockMenu.value = false;
|
|
188
225
|
}
|
|
189
226
|
|
|
190
|
-
function handleAddBlock(blockType: string, insertIndex?: number) {
|
|
227
|
+
async function handleAddBlock(blockType: string, insertIndex?: number) {
|
|
191
228
|
if (!pageData.value) return;
|
|
192
229
|
|
|
193
230
|
// Ensure blocks array exists
|
|
@@ -233,10 +270,8 @@ function handleAddBlock(blockType: string, insertIndex?: number) {
|
|
|
233
270
|
|
|
234
271
|
// finally, if this is an add from the EmptyState, we need to trigger a re-render and initialise the sortable
|
|
235
272
|
if (isAddFromEmptyState) {
|
|
236
|
-
nextTick(
|
|
237
|
-
|
|
238
|
-
initSortable();
|
|
239
|
-
});
|
|
273
|
+
await nextTick();
|
|
274
|
+
await initSortable();
|
|
240
275
|
}
|
|
241
276
|
|
|
242
277
|
return newBlock;
|
|
@@ -278,50 +313,171 @@ watch(
|
|
|
278
313
|
}
|
|
279
314
|
);
|
|
280
315
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
316
|
+
// Watch for the pageBlocksWrapperRef element to become available
|
|
317
|
+
watch(
|
|
318
|
+
pageBlocksWrapperRef,
|
|
319
|
+
async (element) => {
|
|
320
|
+
if (element) {
|
|
321
|
+
// Element is now in the DOM, initialize Sortable
|
|
322
|
+
await nextTick(); // One more tick to ensure it's fully rendered
|
|
323
|
+
await initSortable();
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
{ immediate: true }
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// Also watch for both pageLayout and blocks array changes to trigger re-initialization
|
|
330
|
+
// But only if Sortable isn't already initialized or currently sorting
|
|
331
|
+
watch(
|
|
332
|
+
() => [pageLayout.value, pageData.value?.[props.blocksKey]?.length],
|
|
333
|
+
async ([layout, blockCount], [oldLayout, oldBlockCount]) => {
|
|
334
|
+
// Skip if already initializing or sorting
|
|
335
|
+
if (isInitializingSortable.value || isSorting.value) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Only initialize Sortable if both layout exists and blocks exist
|
|
340
|
+
// And only if the layout changed or blocks were added/removed (not just reordered)
|
|
341
|
+
const layoutChanged = layout !== oldLayout;
|
|
342
|
+
const blocksChanged = blockCount !== oldBlockCount;
|
|
343
|
+
const shouldInitialize = layout && blockCount && blockCount > 0 && !props.loading && pageBlocksWrapperRef.value;
|
|
344
|
+
|
|
345
|
+
// Only re-initialize if layout changed or blocks were added/removed
|
|
346
|
+
// Don't re-initialize on reorder (same count, just different order)
|
|
347
|
+
if (shouldInitialize && (layoutChanged || blocksChanged || !sortableInstance.value)) {
|
|
348
|
+
// Wait for the dynamic component to fully mount and render
|
|
349
|
+
await nextTick();
|
|
350
|
+
await nextTick();
|
|
351
|
+
await initSortable();
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
{ immediate: false }
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
async function initSortable() {
|
|
358
|
+
// Don't re-initialize if already sorting or initializing (prevents conflicts)
|
|
359
|
+
if (isSorting.value || isInitializingSortable.value) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check prerequisites before attempting to initialize
|
|
364
|
+
if (props.loading) {
|
|
365
|
+
console.warn("Cannot initialize Sortable: component is still loading");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!pageLayout.value) {
|
|
370
|
+
console.warn("Cannot initialize Sortable: pageLayout is not set");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const blockCount = pageData.value?.[props.blocksKey]?.length;
|
|
375
|
+
if (!blockCount || blockCount === 0) {
|
|
376
|
+
console.warn("Cannot initialize Sortable: no blocks exist");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Use the template ref first, fallback to getElementById
|
|
381
|
+
const sortableBlocksWrapper = pageBlocksWrapperRef.value || document.getElementById("page-blocks-wrapper");
|
|
382
|
+
|
|
383
|
+
if (!sortableBlocksWrapper) {
|
|
384
|
+
console.warn("page-blocks-wrapper element not found. Conditions:", {
|
|
385
|
+
loading: props.loading,
|
|
386
|
+
hasPageLayout: !!pageLayout.value,
|
|
387
|
+
blockCount: pageData.value?.[props.blocksKey]?.length,
|
|
388
|
+
hasRef: !!pageBlocksWrapperRef.value,
|
|
389
|
+
pagePreviewViewportExists: !!document.getElementById("page-viewport"),
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// If Sortable is already initialized and working, don't re-initialize
|
|
395
|
+
if (sortableInstance.value) {
|
|
396
|
+
// Check if the instance is still valid by checking if the element is still attached
|
|
397
|
+
const { el } = sortableInstance.value;
|
|
398
|
+
if (el && el.isConnected && el === sortableBlocksWrapper) {
|
|
399
|
+
// Sortable is already initialized and valid, no need to re-initialize
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
// Instance exists but element is disconnected or different, destroy it
|
|
403
|
+
try {
|
|
404
|
+
sortableInstance.value.destroy();
|
|
405
|
+
} catch {
|
|
406
|
+
// Ignore errors during destruction
|
|
407
|
+
}
|
|
408
|
+
sortableInstance.value = null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
isInitializingSortable.value = true;
|
|
412
|
+
try {
|
|
413
|
+
sortableInstance.value = new Sortable(sortableBlocksWrapper, {
|
|
414
|
+
animation: 150,
|
|
415
|
+
ghostClass: "sortable-ghost",
|
|
416
|
+
chosenClass: "sortable-chosen",
|
|
417
|
+
dragClass: "sortable-drag",
|
|
418
|
+
group: "page-blocks",
|
|
419
|
+
forceFallback: false, // Use native HTML5 drag if available
|
|
420
|
+
fallbackOnBody: true, // Append fallback element to body
|
|
421
|
+
swapThreshold: 0.7, // Threshold for swap
|
|
422
|
+
onStart: () => {
|
|
423
|
+
isSorting.value = true;
|
|
424
|
+
},
|
|
425
|
+
onAdd: (event: any) => {
|
|
426
|
+
// This fires when an item is added from another list (drag from sidebar)
|
|
427
|
+
const { item: draggedElement, newIndex } = event;
|
|
428
|
+
const blockType = draggedElement.getAttribute("data-block-type");
|
|
429
|
+
|
|
430
|
+
if (blockType) {
|
|
431
|
+
// Use the consolidated handleAddBlock function
|
|
432
|
+
handleAddBlock(blockType, newIndex);
|
|
433
|
+
|
|
434
|
+
// Remove the cloned HTML element that SortableJS added
|
|
435
|
+
draggedElement.remove();
|
|
436
|
+
|
|
437
|
+
// Force Vue to re-render by triggering reactivity
|
|
438
|
+
nextTick(() => {
|
|
439
|
+
// The component will re-render with the new block data
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
onEnd: async (event: any) => {
|
|
444
|
+
const { oldIndex, newIndex } = event;
|
|
445
|
+
|
|
446
|
+
// Only handle reordering if this wasn't an add operation (oldIndex will be null for adds)
|
|
447
|
+
if (
|
|
448
|
+
oldIndex !== null &&
|
|
449
|
+
oldIndex !== undefined &&
|
|
450
|
+
newIndex !== null &&
|
|
451
|
+
newIndex !== undefined &&
|
|
452
|
+
oldIndex !== newIndex &&
|
|
453
|
+
pageData.value?.[props.blocksKey]
|
|
454
|
+
) {
|
|
455
|
+
// Wait for SortableJS to finish its DOM cleanup before we modify the array
|
|
456
|
+
await nextTick();
|
|
457
|
+
|
|
458
|
+
// Get the block data from the DOM element before Vue re-renders
|
|
459
|
+
const movedBlock = pageData.value[props.blocksKey][oldIndex];
|
|
460
|
+
if (movedBlock) {
|
|
461
|
+
// Update the array - this will trigger Vue to re-render
|
|
462
|
+
pageData.value[props.blocksKey].splice(oldIndex, 1);
|
|
463
|
+
pageData.value[props.blocksKey].splice(newIndex, 0, movedBlock);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Reset sorting state after a small delay to ensure DOM is stable
|
|
468
|
+
await nextTick();
|
|
469
|
+
isSorting.value = false;
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
} finally {
|
|
473
|
+
isInitializingSortable.value = false;
|
|
474
|
+
}
|
|
322
475
|
}
|
|
323
476
|
|
|
324
|
-
onBeforeMount(() => {
|
|
477
|
+
onBeforeMount(async () => {
|
|
478
|
+
// Initialize the registry first
|
|
479
|
+
await initialiseRegistry();
|
|
480
|
+
|
|
325
481
|
if (!pageData.value) {
|
|
326
482
|
pageData.value = {};
|
|
327
483
|
}
|
|
@@ -356,11 +512,22 @@ onBeforeMount(() => {
|
|
|
356
512
|
pageData.value[props.blocksKey] = [];
|
|
357
513
|
}
|
|
358
514
|
}
|
|
359
|
-
});
|
|
360
515
|
|
|
361
|
-
onMounted(() => {
|
|
362
516
|
loadLayout(pageData.value?.[props.settingsKey]?.layout);
|
|
363
517
|
});
|
|
518
|
+
|
|
519
|
+
// Initialize Sortable after component is mounted - watcher will handle the initialization
|
|
520
|
+
// This is just a fallback in case the watcher didn't fire
|
|
521
|
+
onMounted(async () => {
|
|
522
|
+
await nextTick();
|
|
523
|
+
await nextTick();
|
|
524
|
+
// Trigger the watcher by checking conditions
|
|
525
|
+
// The watcher will handle initialization if all conditions are met
|
|
526
|
+
if (pageData.value?.[props.blocksKey]?.length && pageLayout.value && !props.loading) {
|
|
527
|
+
await nextTick();
|
|
528
|
+
await initSortable();
|
|
529
|
+
}
|
|
530
|
+
});
|
|
364
531
|
</script>
|
|
365
532
|
|
|
366
533
|
<style lang="scss">
|
|
@@ -368,6 +535,7 @@ $editor-background-color: #6a6a6a;
|
|
|
368
535
|
|
|
369
536
|
.wswg-json-editor {
|
|
370
537
|
--editor-height: calc(100vh);
|
|
538
|
+
--editor-bg-color: #6a6a6a;
|
|
371
539
|
|
|
372
540
|
position: relative;
|
|
373
541
|
width: 100%;
|
|
@@ -384,7 +552,7 @@ $editor-background-color: #6a6a6a;
|
|
|
384
552
|
&-body {
|
|
385
553
|
display: flex;
|
|
386
554
|
width: 100%;
|
|
387
|
-
background-color: $editor-background-color;
|
|
555
|
+
background-color: var(--editor-bg-color, $editor-background-color);
|
|
388
556
|
}
|
|
389
557
|
|
|
390
558
|
&-preview {
|
|
@@ -401,7 +569,7 @@ $editor-background-color: #6a6a6a;
|
|
|
401
569
|
.browser-navigation-bar {
|
|
402
570
|
position: sticky;
|
|
403
571
|
top: 1.5rem;
|
|
404
|
-
z-index:
|
|
572
|
+
z-index: 30;
|
|
405
573
|
|
|
406
574
|
&::before {
|
|
407
575
|
position: absolute;
|
|
@@ -411,7 +579,7 @@ $editor-background-color: #6a6a6a;
|
|
|
411
579
|
width: 100%;
|
|
412
580
|
height: 100%;
|
|
413
581
|
content: "";
|
|
414
|
-
background-color: $editor-background-color;
|
|
582
|
+
background-color: var(--editor-bg-color, $editor-background-color);
|
|
415
583
|
}
|
|
416
584
|
}
|
|
417
585
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
// This ensures createField is available immediately without waiting for CSS or component imports
|
|
3
3
|
export { createField } from "./util/fieldConfig";
|
|
4
4
|
export type { EditorFieldConfig, ValidatorFunction } from "./util/fieldConfig";
|
|
5
|
-
export { getLayouts } from "./util/registry";
|
|
6
|
-
export { validateField, validateAllFields } from "./util/validation";
|
|
7
|
-
export type { ValidationResult } from "./util/validation";
|
|
5
|
+
export { getLayouts, initialiseRegistry } from "./util/registry";
|
|
6
|
+
export { validateField, validateAllFields, type ValidationResult } from "./util/validation";
|
|
8
7
|
|
|
9
8
|
// Export components (component exports don't cause side effects until used)
|
|
10
9
|
export { default as WswgJsonEditor } from "./components/WswgJsonEditor/WswgJsonEditor.vue";
|
package/src/style.css
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/****
|
|
2
|
+
* Wrap Tailwind v3 utilities in a custom layer with lower priority
|
|
3
|
+
* This allows Tailwind v4 utilities to override v3 utilities when both exist
|
|
4
|
+
****/
|
|
5
|
+
|
|
6
|
+
@layer vue-wswg-editor-library {
|
|
7
|
+
@tailwind base;
|
|
8
|
+
@tailwind components;
|
|
9
|
+
@tailwind utilities;
|
|
10
|
+
}
|