vue-wswg-editor 0.0.10 → 0.0.11

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 (46) hide show
  1. package/README.md +493 -20
  2. package/dist/style.css +1 -1
  3. package/dist/vue-wswg-editor.es.js +2249 -1782
  4. package/package.json +15 -8
  5. package/src/assets/styles/_mixins.scss +12 -17
  6. package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +39 -4
  7. package/src/components/BlockEditorFields/BlockEditorFields.vue +12 -3
  8. package/src/components/BlockImageFieldNode/BlockImageNode.vue +373 -0
  9. package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +1 -5
  10. package/src/components/PageRenderer/PageRenderer.vue +40 -41
  11. package/src/components/PageRenderer/blockModules.ts +32 -3
  12. package/src/components/PageRenderer/layoutModules.ts +32 -3
  13. package/src/components/WswgJsonEditor/WswgJsonEditor.vue +230 -62
  14. package/src/index.ts +2 -3
  15. package/src/style.css +10 -3
  16. package/src/util/fieldConfig.ts +22 -0
  17. package/src/util/helpers.ts +1 -1
  18. package/src/util/registry.ts +30 -23
  19. package/src/util/validation.ts +178 -23
  20. package/types/vue-wswg-editor.d.ts +161 -0
  21. package/dist/types/components/AddBlockItem/AddBlockItem.vue.d.ts +0 -6
  22. package/dist/types/components/BlockBrowser/BlockBrowser.vue.d.ts +0 -2
  23. package/dist/types/components/BlockComponent/BlockComponent.vue.d.ts +0 -15
  24. package/dist/types/components/BlockEditorFieldNode/BlockEditorFieldNode.vue.d.ts +0 -15
  25. package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +0 -15
  26. package/dist/types/components/BlockMarginFieldNode/BlockMarginNode.vue.d.ts +0 -23
  27. package/dist/types/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue.d.ts +0 -15
  28. package/dist/types/components/BrowserNavigation/BrowserNavigation.vue.d.ts +0 -5
  29. package/dist/types/components/EmptyState/EmptyState.vue.d.ts +0 -15
  30. package/dist/types/components/PageBlockList/PageBlockList.vue.d.ts +0 -19
  31. package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +0 -35
  32. package/dist/types/components/PageBuilderToolbar/PageBuilderToolbar.vue.d.ts +0 -28
  33. package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +0 -13
  34. package/dist/types/components/PageRenderer/blockModules.d.ts +0 -1
  35. package/dist/types/components/PageRenderer/layoutModules.d.ts +0 -1
  36. package/dist/types/components/PageSettings/PageSettings.vue.d.ts +0 -17
  37. package/dist/types/components/ResizeHandle/ResizeHandle.vue.d.ts +0 -6
  38. package/dist/types/components/WswgJsonEditor/WswgJsonEditor.test.d.ts +0 -1
  39. package/dist/types/components/WswgJsonEditor/WswgJsonEditor.vue.d.ts +0 -36
  40. package/dist/types/index.d.ts +0 -8
  41. package/dist/types/util/fieldConfig.d.ts +0 -83
  42. package/dist/types/util/helpers.d.ts +0 -28
  43. package/dist/types/util/registry.d.ts +0 -20
  44. package/dist/types/util/validation.d.ts +0 -15
  45. package/dist/types/vite-plugin.d.ts +0 -9
  46. package/dist/vite-plugin.js +0 -76
@@ -1,7 +1,21 @@
1
1
  <template>
2
- <component :is="layoutComponent" v-if="withLayout && layoutComponent" v-bind="settings">
3
- <template #default>
4
- <div id="page-blocks-wrapper">
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 { blockModules } from "./blockModules";
27
- import { layoutModules } from "./layoutModules";
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: true,
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 in blockModules keys (file paths)
57
+ // Generate name variations and try to find a match
46
58
  const nameVariations = generateNameVariations(blockType);
47
59
 
48
- // Iterate through all blockModules entries
49
- for (const [filePath, module] of Object.entries(blockModules)) {
50
- // Check if any variation matches the file path
51
- for (const variation of nameVariations) {
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
- // Iterate through all layoutModules entries
72
- for (const [filePath, module] of Object.entries(layoutModules)) {
73
- // Check if any variation matches the file path
74
- for (const variation of nameVariations) {
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
- // import.meta.glob must be at top level - it will be transformed by Vite plugin
2
- // This file is separate from the Vue component to ensure the transform hook can intercept it
3
- export const blockModules = import.meta.glob("@page-builder/blocks/**/*.vue", { eager: true });
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
- // import.meta.glob must be at top level - it will be transformed by Vite plugin
2
- // This file is separate from the Vue component to ensure the transform hook can intercept it
3
- export const layoutModules = import.meta.glob("@page-builder/layout/**/*.vue", { eager: true });
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-preview-viewport"
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
- <p v-else>No layout component found</p>
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 class="page-builder-sidebar-wrapper bg-white" :style="{ width: sidebarWidth + 'px' }">
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
- await nextTick();
157
- initSortable();
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
- // The component will re-render with the new block data
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
- function initSortable() {
282
- const sortableBlocksWrapper = document.getElementById("page-blocks-wrapper");
283
- if (!sortableBlocksWrapper) return;
284
- new Sortable(sortableBlocksWrapper, {
285
- animation: 150,
286
- ghostClass: "sortable-ghost",
287
- chosenClass: "sortable-chosen",
288
- dragClass: "sortable-drag",
289
- group: "page-blocks",
290
- onStart: () => {
291
- isSorting.value = true;
292
- },
293
- onAdd: (event: any) => {
294
- // This fires when an item is added from another list (drag from sidebar)
295
- const { item: draggedElement, newIndex } = event;
296
- const blockType = draggedElement.getAttribute("data-block-type");
297
-
298
- if (blockType) {
299
- // Use the consolidated handleAddBlock function
300
- handleAddBlock(blockType, newIndex);
301
-
302
- // Remove the cloned HTML element that SortableJS added
303
- draggedElement.remove();
304
-
305
- // Force Vue to re-render by triggering reactivity
306
- nextTick(() => {
307
- // The component will re-render with the new block data
308
- });
309
- }
310
- },
311
- onEnd: (event: any) => {
312
- isSorting.value = false;
313
- const { oldIndex, newIndex } = event;
314
-
315
- // Only handle reordering if this wasn't an add operation (oldIndex will be null for adds)
316
- if (oldIndex !== null && oldIndex !== newIndex) {
317
- const movedBlock = pageData.value?.[props.blocksKey]?.splice(oldIndex, 1)[0];
318
- pageData.value?.[props.blocksKey]?.splice(newIndex, 0, movedBlock);
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: 12;
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
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
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
+ }