vue-wswg-editor 0.0.11 → 0.0.13
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 +23 -8
- package/dist/style.css +1 -1
- 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 +17 -0
- package/dist/types/components/BlockImageFieldNode/BlockImageNode.vue.d.ts +19 -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/EditorPageRenderer/EditorPageRenderer.vue.d.ts +21 -0
- package/dist/types/components/EmptyState/EmptyState.vue.d.ts +9 -0
- package/dist/types/components/IframePreview/IframePreview.vue.d.ts +26 -0
- package/dist/types/components/IframePreview/iframeContent.d.ts +9 -0
- package/dist/types/components/IframePreview/iframePreviewApp.d.ts +36 -0
- package/dist/types/components/IframePreview/messageHandler.d.ts +55 -0
- package/dist/types/components/IframePreview/types.d.ts +77 -0
- package/dist/types/components/PageBlockList/PageBlockList.vue.d.ts +19 -0
- package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +37 -0
- package/dist/types/components/PageBuilderToolbar/PageBuilderToolbar.vue.d.ts +28 -0
- package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +15 -0
- package/dist/types/components/PageSettings/PageSettings.vue.d.ts +19 -0
- package/dist/types/components/ResizeHandle/ResizeHandle.vue.d.ts +6 -0
- package/dist/types/components/WswgPageBuilder/WswgPageBuilder.test.d.ts +1 -0
- package/dist/types/components/WswgPageBuilder/WswgPageBuilder.vue.d.ts +38 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/util/fieldConfig.d.ts +87 -0
- package/dist/types/util/helpers.d.ts +28 -0
- package/dist/types/util/registry.d.ts +27 -0
- package/dist/types/util/theme-registry.d.ts +42 -0
- package/dist/types/util/validation.d.ts +26 -0
- package/dist/types/vite-plugin.d.ts +9 -0
- package/dist/vite-plugin.js +80 -0
- package/dist/vue-wswg-editor.es.js +2854 -2006
- package/package.json +1 -2
- package/src/assets/styles/_mixins.scss +15 -0
- package/src/components/AddBlockItem/AddBlockItem.vue +13 -4
- package/src/components/BlockBrowser/BlockBrowser.vue +5 -5
- package/src/components/BlockComponent/BlockComponent.vue +23 -50
- package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +12 -10
- package/src/components/BlockEditorFields/BlockEditorFields.vue +24 -4
- package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +9 -4
- package/src/components/BrowserNavigation/BrowserNavigation.vue +1 -1
- package/src/components/EditorPageRenderer/EditorPageRenderer.vue +641 -0
- package/src/components/EmptyState/EmptyState.vue +3 -12
- package/src/components/IframePreview/IframePreview.vue +211 -0
- package/src/components/IframePreview/iframeContent.ts +210 -0
- package/src/components/IframePreview/iframePreviewApp.ts +221 -0
- package/src/components/IframePreview/messageHandler.ts +219 -0
- package/src/components/IframePreview/types.ts +126 -0
- package/src/components/PageBlockList/PageBlockList.vue +8 -6
- package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +5 -3
- package/src/components/PageRenderer/PageRenderer.vue +9 -33
- package/src/components/PageSettings/PageSettings.vue +10 -6
- package/src/components/ResizeHandle/ResizeHandle.vue +68 -10
- package/src/components/{WswgJsonEditor/WswgJsonEditor.test.ts → WswgPageBuilder/WswgPageBuilder.test.ts} +8 -8
- package/src/components/WswgPageBuilder/WswgPageBuilder.vue +375 -0
- package/src/index.ts +10 -2
- package/src/shims.d.ts +4 -0
- package/src/types/Theme.d.ts +15 -0
- package/src/util/registry.ts +2 -2
- package/src/util/theme-registry.ts +397 -0
- package/src/util/validation.ts +104 -13
- package/src/vite-plugin.ts +8 -4
- package/types/vue-wswg-editor.d.ts +4 -0
- package/src/components/PageRenderer/blockModules-alternative.ts.example +0 -9
- package/src/components/PageRenderer/blockModules-manual.ts.example +0 -19
- package/src/components/PageRenderer/blockModules-runtime.ts.example +0 -23
- package/src/components/PageRenderer/blockModules.ts +0 -32
- package/src/components/PageRenderer/layoutModules.ts +0 -32
- package/src/components/WswgJsonEditor/WswgJsonEditor.vue +0 -595
|
@@ -1,32 +0,0 @@
|
|
|
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,32 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,595 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="wswg-json-editor">
|
|
3
|
-
<slot v-if="loading" name="loading">
|
|
4
|
-
<div class="wswg-json-editor-loading flex h-full flex-col items-center justify-center gap-4">
|
|
5
|
-
<svg
|
|
6
|
-
class="size-9 animate-[spin_2000ms_linear_infinite]"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
10
|
-
>
|
|
11
|
-
<path
|
|
12
|
-
d="M13 2a1 1 0 0 0-2 0v4.167a1 1 0 1 0 2 0V2ZM13 17.833a1 1 0 0 0-2 0V22a1 1 0 1 0 2 0v-4.167ZM16.834 12a1 1 0 0 1 1-1H22a1 1 0 0 1 0 2h-4.166a1 1 0 0 1-1-1ZM2 11a1 1 0 0 0 0 2h4.167a1 1 0 1 0 0-2H2ZM19.916 4.085a1 1 0 0 1 0 1.414l-2.917 2.917A1 1 0 1 1 15.585 7l2.917-2.916a1 1 0 0 1 1.414 0ZM8.415 16.999a1 1 0 0 0-1.414-1.414L4.084 18.5A1 1 0 1 0 5.5 19.916l2.916-2.917ZM15.585 15.585a1 1 0 0 1 1.414 0l2.917 2.916a1 1 0 1 1-1.414 1.415l-2.917-2.917a1 1 0 0 1 0-1.414ZM5.499 4.085a1 1 0 0 0-1.415 1.414l2.917 2.917A1 1 0 0 0 8.415 7L5.5 4.085Z"
|
|
13
|
-
fill="#000000"
|
|
14
|
-
/>
|
|
15
|
-
</svg>
|
|
16
|
-
<span>Loading</span>
|
|
17
|
-
</div>
|
|
18
|
-
</slot>
|
|
19
|
-
<!-- WYSIWYG editor -->
|
|
20
|
-
<div v-else class="wswg-json-editor-body">
|
|
21
|
-
<!-- Page preview -->
|
|
22
|
-
<div class="wswg-json-editor-preview">
|
|
23
|
-
<div
|
|
24
|
-
class="mx-auto flex h-full flex-col transition-all duration-300"
|
|
25
|
-
:class="{ 'w-full': editorViewport === 'desktop', 'w-96': editorViewport === 'mobile' }"
|
|
26
|
-
>
|
|
27
|
-
<BrowserNavigation v-if="showBrowserBar" class="browser-navigation-bar" :url="url" />
|
|
28
|
-
<div
|
|
29
|
-
v-if="pageLayout"
|
|
30
|
-
id="page-viewport"
|
|
31
|
-
class="relative overflow-hidden rounded-b-lg bg-white"
|
|
32
|
-
:class="{ 'rounded-t-lg': !showBrowserBar }"
|
|
33
|
-
>
|
|
34
|
-
<component :is="pageLayout" v-bind="pageData?.[settingsKey]">
|
|
35
|
-
<template #default>
|
|
36
|
-
<!-- No blocks found -->
|
|
37
|
-
<EmptyState
|
|
38
|
-
v-if="!pageData?.[blocksKey]?.length"
|
|
39
|
-
v-model:showAddBlockMenu="showAddBlockMenu"
|
|
40
|
-
:editable="editable"
|
|
41
|
-
@block-added="handleAddBlock"
|
|
42
|
-
/>
|
|
43
|
-
<!-- Blocks found -->
|
|
44
|
-
<div v-else id="page-blocks-wrapper" ref="pageBlocksWrapperRef">
|
|
45
|
-
<div v-for="(block, blockIndex) in pageData[blocksKey]" :key="block.id">
|
|
46
|
-
<BlockComponent
|
|
47
|
-
:block="block"
|
|
48
|
-
:blockIndex="blockIndex"
|
|
49
|
-
:activeBlock="activeBlock"
|
|
50
|
-
:editable="editable"
|
|
51
|
-
:hoveredBlockId="hoveredBlockId"
|
|
52
|
-
@hover-block="setHoveredBlockId"
|
|
53
|
-
@click-block="handleBlockClick"
|
|
54
|
-
/>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
</template>
|
|
58
|
-
</component>
|
|
59
|
-
</div>
|
|
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>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
<!-- Resizable divider -->
|
|
94
|
-
<ResizeHandle @sidebar-width="handleSidebarWidth" />
|
|
95
|
-
|
|
96
|
-
<!-- Sidebar -->
|
|
97
|
-
<div
|
|
98
|
-
id="page-builder-sidebar"
|
|
99
|
-
class="page-builder-sidebar-wrapper bg-white"
|
|
100
|
-
:style="{ width: sidebarWidth + 'px' }"
|
|
101
|
-
>
|
|
102
|
-
<PageBuilderSidebar
|
|
103
|
-
v-model="pageData"
|
|
104
|
-
v-model:activeBlock="activeBlock"
|
|
105
|
-
v-model:hoveredBlockId="hoveredBlockId"
|
|
106
|
-
v-model:showPageSettings="showPageSettings"
|
|
107
|
-
v-model:showAddBlockMenu="showAddBlockMenu"
|
|
108
|
-
v-model:editorViewport="editorViewport"
|
|
109
|
-
:hasPageSettings="hasPageSettings"
|
|
110
|
-
:editable="editable"
|
|
111
|
-
:blocksKey="blocksKey"
|
|
112
|
-
:settingsKey="settingsKey"
|
|
113
|
-
/>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
</template>
|
|
118
|
-
|
|
119
|
-
<script setup lang="ts">
|
|
120
|
-
import {
|
|
121
|
-
ref,
|
|
122
|
-
shallowRef,
|
|
123
|
-
withDefaults,
|
|
124
|
-
watch,
|
|
125
|
-
type Component,
|
|
126
|
-
onBeforeMount,
|
|
127
|
-
onMounted,
|
|
128
|
-
nextTick,
|
|
129
|
-
computed,
|
|
130
|
-
} from "vue";
|
|
131
|
-
import ResizeHandle from "../ResizeHandle/ResizeHandle.vue";
|
|
132
|
-
import PageBuilderSidebar from "../PageBuilderSidebar/PageBuilderSidebar.vue";
|
|
133
|
-
import BrowserNavigation from "../BrowserNavigation/BrowserNavigation.vue";
|
|
134
|
-
import BlockComponent from "../BlockComponent/BlockComponent.vue";
|
|
135
|
-
import EmptyState from "../EmptyState/EmptyState.vue";
|
|
136
|
-
import { getBlockComponent, getLayouts, initialiseRegistry } from "../../util/registry";
|
|
137
|
-
import type { Block } from "../../types/Block";
|
|
138
|
-
import Sortable from "sortablejs";
|
|
139
|
-
|
|
140
|
-
const props = withDefaults(
|
|
141
|
-
defineProps<{
|
|
142
|
-
editable?: boolean;
|
|
143
|
-
loading?: boolean;
|
|
144
|
-
url?: string;
|
|
145
|
-
showBrowserBar?: boolean;
|
|
146
|
-
blocksKey?: string;
|
|
147
|
-
settingsKey?: string;
|
|
148
|
-
defaultBlockMargin?: "none" | "small" | "medium" | "large";
|
|
149
|
-
}>(),
|
|
150
|
-
{
|
|
151
|
-
editable: false,
|
|
152
|
-
loading: false,
|
|
153
|
-
url: "",
|
|
154
|
-
showBrowserBar: false,
|
|
155
|
-
blocksKey: "blocks",
|
|
156
|
-
settingsKey: "settings",
|
|
157
|
-
defaultBlockMargin: "none",
|
|
158
|
-
}
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const editorViewport = ref<"desktop" | "mobile">("desktop");
|
|
162
|
-
const showPageSettings = ref(false);
|
|
163
|
-
const showAddBlockMenu = ref(false);
|
|
164
|
-
const activeBlock = ref<any>(null);
|
|
165
|
-
const isSorting = ref(false);
|
|
166
|
-
const hoveredBlockId = ref<string | null>(null);
|
|
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
|
|
171
|
-
|
|
172
|
-
// Model value for the JSON page data
|
|
173
|
-
const pageData = defineModel<Record<string, any>>();
|
|
174
|
-
|
|
175
|
-
// Layout component - dynamically imported from page-builder directory
|
|
176
|
-
// Using shallowRef to avoid making the component reactive (performance optimization)
|
|
177
|
-
const pageLayout = shallowRef<Component | undefined>(undefined);
|
|
178
|
-
|
|
179
|
-
// Apply the sidebar width from the resize handle
|
|
180
|
-
function handleSidebarWidth(width: number) {
|
|
181
|
-
sidebarWidth.value = width;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Load layout component dynamically from @page-builder/layout/
|
|
185
|
-
async function loadLayout(layoutName: string | undefined) {
|
|
186
|
-
// Use "default" layout if no layout is provided
|
|
187
|
-
const layout = layoutName || "default";
|
|
188
|
-
try {
|
|
189
|
-
const availableLayouts = getLayouts();
|
|
190
|
-
const layoutModule = availableLayouts[layout];
|
|
191
|
-
pageLayout.value = layoutModule;
|
|
192
|
-
// Don't initialize Sortable here - let the watcher handle it
|
|
193
|
-
// The watcher will react to pageLayout.value changing and check all conditions
|
|
194
|
-
} catch (error) {
|
|
195
|
-
// Layout doesn't exist, return undefined
|
|
196
|
-
console.warn(`Layout "${layout}" not found in @page-builder/layout/`, error);
|
|
197
|
-
pageLayout.value = undefined;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Check if the page has settings
|
|
202
|
-
const hasPageSettings = computed(() => {
|
|
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
|
|
205
|
-
const layouts = getLayouts();
|
|
206
|
-
if (Object.keys(layouts).length > 1) {
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// If the layout has settings
|
|
211
|
-
if (pageData.value?.[props.settingsKey]) {
|
|
212
|
-
if (Object.keys(pageData.value?.[props.settingsKey]).length > 0) {
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return false;
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
function handleBlockClick(block: Block | null) {
|
|
221
|
-
activeBlock.value = block;
|
|
222
|
-
showPageSettings.value = false;
|
|
223
|
-
hoveredBlockId.value = null;
|
|
224
|
-
showAddBlockMenu.value = false;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function handleAddBlock(blockType: string, insertIndex?: number) {
|
|
228
|
-
if (!pageData.value) return;
|
|
229
|
-
|
|
230
|
-
// Ensure blocks array exists
|
|
231
|
-
if (!pageData.value[props.blocksKey]) {
|
|
232
|
-
pageData.value[props.blocksKey] = [];
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Record if this is an add from the EmptyState
|
|
236
|
-
const isAddFromEmptyState = insertIndex === undefined;
|
|
237
|
-
|
|
238
|
-
// Create a new block object
|
|
239
|
-
const newBlock = {
|
|
240
|
-
id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
241
|
-
type: blockType,
|
|
242
|
-
margin: props.defaultBlockMargin
|
|
243
|
-
? { top: props.defaultBlockMargin, bottom: props.defaultBlockMargin }
|
|
244
|
-
: undefined,
|
|
245
|
-
};
|
|
246
|
-
// Get the default prop values from the block component
|
|
247
|
-
const blockComponent = getBlockComponent(blockType);
|
|
248
|
-
if (blockComponent?.props) {
|
|
249
|
-
// loop props and set their default value
|
|
250
|
-
Object.entries(blockComponent.props).forEach(([key, value]: [string, any]) => {
|
|
251
|
-
if (value.default) {
|
|
252
|
-
if (typeof value.default === "function") {
|
|
253
|
-
newBlock[key as keyof typeof newBlock] = value.default();
|
|
254
|
-
} else {
|
|
255
|
-
newBlock[key as keyof typeof newBlock] = value.default;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Add the new block at the specified index or at the end
|
|
262
|
-
if (insertIndex !== undefined) {
|
|
263
|
-
pageData.value[props.blocksKey].splice(insertIndex, 0, newBlock);
|
|
264
|
-
} else {
|
|
265
|
-
pageData.value[props.blocksKey].push(newBlock);
|
|
266
|
-
// Set the new block as active only when adding to the end (from EmptyState)
|
|
267
|
-
activeBlock.value = newBlock;
|
|
268
|
-
showAddBlockMenu.value = false;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// finally, if this is an add from the EmptyState, we need to trigger a re-render and initialise the sortable
|
|
272
|
-
if (isAddFromEmptyState) {
|
|
273
|
-
await nextTick();
|
|
274
|
-
await initSortable();
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return newBlock;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function setHoveredBlockId(id: string | null) {
|
|
281
|
-
if (isSorting.value) return;
|
|
282
|
-
hoveredBlockId.value = id;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Watch for changes in pageData layout and load the corresponding component
|
|
286
|
-
watch(
|
|
287
|
-
() => pageData.value?.[props.settingsKey]?.layout,
|
|
288
|
-
(layoutName) => {
|
|
289
|
-
loadLayout(layoutName);
|
|
290
|
-
},
|
|
291
|
-
{ immediate: false }
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
// Watch for activeBlock changes and scroll to the corresponding block in the preview
|
|
295
|
-
watch(
|
|
296
|
-
() => activeBlock.value?.id,
|
|
297
|
-
async (blockId) => {
|
|
298
|
-
if (!blockId) return;
|
|
299
|
-
|
|
300
|
-
// Wait for DOM to update
|
|
301
|
-
await nextTick();
|
|
302
|
-
|
|
303
|
-
// Find the block element by data-block-id
|
|
304
|
-
const blockElement = document.querySelector(`[data-block-id="${blockId}"]`) as HTMLElement;
|
|
305
|
-
if (!blockElement) return;
|
|
306
|
-
|
|
307
|
-
// Scroll the block into view, centered in the scrollable container
|
|
308
|
-
blockElement.scrollIntoView({
|
|
309
|
-
behavior: "smooth",
|
|
310
|
-
block: "center",
|
|
311
|
-
inline: "nearest",
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
);
|
|
315
|
-
|
|
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
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
onBeforeMount(async () => {
|
|
478
|
-
// Initialize the registry first
|
|
479
|
-
await initialiseRegistry();
|
|
480
|
-
|
|
481
|
-
if (!pageData.value) {
|
|
482
|
-
pageData.value = {};
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (!pageData.value?.[props.settingsKey]) {
|
|
486
|
-
if (pageData.value) {
|
|
487
|
-
pageData.value[props.settingsKey] = {};
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (!pageData.value?.[props.settingsKey]?.layout) {
|
|
492
|
-
if (pageData.value && pageData.value[props.settingsKey]) {
|
|
493
|
-
pageData.value[props.settingsKey].layout = "default";
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Sanitise the layout data (must be a valid layout name string)
|
|
498
|
-
if (pageData.value?.[props.settingsKey]?.layout) {
|
|
499
|
-
const layouts = getLayouts();
|
|
500
|
-
const settings = pageData.value?.[props.settingsKey];
|
|
501
|
-
if (settings) {
|
|
502
|
-
if (typeof settings.layout !== "string") {
|
|
503
|
-
settings.layout = "default";
|
|
504
|
-
} else if (!layouts[settings.layout]) {
|
|
505
|
-
settings.layout = "default";
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (!pageData.value?.[props.blocksKey]) {
|
|
511
|
-
if (pageData.value) {
|
|
512
|
-
pageData.value[props.blocksKey] = [];
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
loadLayout(pageData.value?.[props.settingsKey]?.layout);
|
|
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
|
-
});
|
|
531
|
-
</script>
|
|
532
|
-
|
|
533
|
-
<style lang="scss">
|
|
534
|
-
$editor-background-color: #6a6a6a;
|
|
535
|
-
|
|
536
|
-
.wswg-json-editor {
|
|
537
|
-
--editor-height: calc(100vh);
|
|
538
|
-
--editor-bg-color: #6a6a6a;
|
|
539
|
-
|
|
540
|
-
position: relative;
|
|
541
|
-
width: 100%;
|
|
542
|
-
max-width: 100vw;
|
|
543
|
-
height: var(--editor-height);
|
|
544
|
-
overflow-y: auto;
|
|
545
|
-
|
|
546
|
-
&-loading {
|
|
547
|
-
display: flex;
|
|
548
|
-
align-items: center;
|
|
549
|
-
justify-content: center;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
&-body {
|
|
553
|
-
display: flex;
|
|
554
|
-
width: 100%;
|
|
555
|
-
background-color: var(--editor-bg-color, $editor-background-color);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
&-preview {
|
|
559
|
-
position: relative;
|
|
560
|
-
display: flex;
|
|
561
|
-
flex: 1;
|
|
562
|
-
flex-grow: 1;
|
|
563
|
-
flex-shrink: 0;
|
|
564
|
-
flex-direction: column;
|
|
565
|
-
height: 100%;
|
|
566
|
-
min-height: 0;
|
|
567
|
-
padding: 1.5rem;
|
|
568
|
-
|
|
569
|
-
.browser-navigation-bar {
|
|
570
|
-
position: sticky;
|
|
571
|
-
top: 1.5rem;
|
|
572
|
-
z-index: 30;
|
|
573
|
-
|
|
574
|
-
&::before {
|
|
575
|
-
position: absolute;
|
|
576
|
-
top: -1.5rem;
|
|
577
|
-
left: 0;
|
|
578
|
-
z-index: -1;
|
|
579
|
-
width: 100%;
|
|
580
|
-
height: 100%;
|
|
581
|
-
content: "";
|
|
582
|
-
background-color: var(--editor-bg-color, $editor-background-color);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
.page-builder-sidebar-wrapper {
|
|
588
|
-
position: sticky;
|
|
589
|
-
top: 0;
|
|
590
|
-
z-index: 12;
|
|
591
|
-
height: var(--editor-height);
|
|
592
|
-
overflow-y: auto;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
</style>
|