vue-wswg-editor 0.0.12 → 0.0.14
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/BlockEditorFields/BlockEditorFields.vue.d.ts +1 -0
- package/dist/types/components/EditorPageRenderer/EditorPageRenderer.vue.d.ts +21 -0
- package/dist/types/components/EmptyState/EmptyState.vue.d.ts +2 -8
- 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/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +2 -0
- package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +2 -0
- package/dist/types/components/PageSettings/PageSettings.vue.d.ts +2 -0
- package/dist/types/components/{WswgJsonEditor/WswgJsonEditor.vue.d.ts → WswgPageBuilder/WswgPageBuilder.vue.d.ts} +2 -0
- package/dist/types/index.d.ts +8 -2
- package/dist/types/util/registry.d.ts +2 -0
- package/dist/types/util/theme-registry.d.ts +42 -0
- package/dist/types/util/validation.d.ts +2 -2
- package/dist/vite-plugin.js +33 -29
- package/dist/vue-wswg-editor.es.js +2783 -1905
- 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 +230 -0
- package/src/components/IframePreview/iframePreviewApp.ts +308 -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 +18 -38
- 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 +102 -11
- package/src/vite-plugin.ts +8 -4
- package/types/vue-wswg-editor.d.ts +4 -0
- package/dist/types/components/PageRenderer/blockModules.d.ts +0 -3
- package/dist/types/components/PageRenderer/layoutModules.d.ts +0 -3
- 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
- /package/dist/types/components/{WswgJsonEditor/WswgJsonEditor.test.d.ts → WswgPageBuilder/WswgPageBuilder.test.d.ts} +0 -0
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
v-model="pageData"
|
|
15
15
|
:editable="editable"
|
|
16
16
|
:settingsKey="settingsKey"
|
|
17
|
+
:activeTab="activeSettingsTab"
|
|
17
18
|
@close="showPageSettings = false"
|
|
18
19
|
/>
|
|
19
20
|
<!-- Active section-->
|
|
@@ -90,9 +91,8 @@
|
|
|
90
91
|
|
|
91
92
|
<script setup lang="ts">
|
|
92
93
|
import { computed } from "vue";
|
|
93
|
-
import {
|
|
94
|
+
import { getBlock } from "../../util/theme-registry";
|
|
94
95
|
import type { Block } from "../../types/Block";
|
|
95
|
-
import { toCamelCase } from "../../util/helpers";
|
|
96
96
|
import BlockBrowser from "../BlockBrowser/BlockBrowser.vue";
|
|
97
97
|
import BlockEditorFields from "../BlockEditorFields/BlockEditorFields.vue";
|
|
98
98
|
import PageBlockList from "../PageBlockList/PageBlockList.vue";
|
|
@@ -114,12 +114,14 @@ const props = withDefaults(
|
|
|
114
114
|
blocksKey?: string;
|
|
115
115
|
settingsKey?: string;
|
|
116
116
|
hasPageSettings?: boolean;
|
|
117
|
+
activeSettingsTab?: string;
|
|
117
118
|
}>(),
|
|
118
119
|
{
|
|
119
120
|
editable: true,
|
|
120
121
|
blocksKey: "blocks",
|
|
121
122
|
settingsKey: "settings",
|
|
122
123
|
hasPageSettings: false,
|
|
124
|
+
activeSettingsTab: undefined,
|
|
123
125
|
}
|
|
124
126
|
);
|
|
125
127
|
|
|
@@ -136,7 +138,7 @@ const computedActiveBlock = computed(() => {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
// Find the corresponding block in the registry
|
|
139
|
-
const registryBlock =
|
|
141
|
+
const registryBlock = getBlock(blockType);
|
|
140
142
|
|
|
141
143
|
if (!registryBlock) {
|
|
142
144
|
// If no registry block found, return activeBlock as-is
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div id="page-viewport" class="page-renderer-wrapper relative">
|
|
3
3
|
<template v-if="isReady">
|
|
4
|
-
<component :is="layoutComponent" v-if="withLayout && layoutComponent" v-bind="settings">
|
|
4
|
+
<component :is="layoutComponent" v-if="withLayout && layoutComponent" v-bind="settings" :blocks="blocks">
|
|
5
5
|
<template #default>
|
|
6
6
|
<div id="page-blocks-wrapper">
|
|
7
7
|
<div
|
|
@@ -24,18 +24,15 @@
|
|
|
24
24
|
>
|
|
25
25
|
<component :is="getBlock(block.type)" v-bind="block" :key="`block-${block.id}`" />
|
|
26
26
|
</div>
|
|
27
|
-
<pre>{{ blocks }}</pre>
|
|
28
27
|
</div>
|
|
29
28
|
</template>
|
|
30
29
|
</div>
|
|
31
30
|
</template>
|
|
32
31
|
|
|
33
32
|
<script setup lang="ts">
|
|
34
|
-
import {
|
|
35
|
-
import { generateNameVariations } from "../../util/helpers";
|
|
36
|
-
import { getBlockModule, loadBlockModules } from "./blockModules";
|
|
37
|
-
import { loadLayoutModules, getLayoutModule } from "./layoutModules";
|
|
33
|
+
import { computed, withDefaults, onBeforeMount, ref } from "vue";
|
|
38
34
|
import type { Block } from "../../types/Block";
|
|
35
|
+
import { initialiseRegistry, getBlock, getLayout } from "../../util/theme-registry";
|
|
39
36
|
|
|
40
37
|
const props = withDefaults(
|
|
41
38
|
defineProps<{
|
|
@@ -43,45 +40,21 @@ const props = withDefaults(
|
|
|
43
40
|
layout?: string;
|
|
44
41
|
settings?: Record<string, any>;
|
|
45
42
|
withLayout?: boolean;
|
|
43
|
+
theme?: string;
|
|
46
44
|
}>(),
|
|
47
45
|
{
|
|
48
46
|
layout: "default",
|
|
49
47
|
settings: () => ({}),
|
|
50
48
|
withLayout: false,
|
|
49
|
+
theme: "default",
|
|
51
50
|
}
|
|
52
51
|
);
|
|
53
52
|
|
|
54
53
|
const isReady = ref(false);
|
|
55
54
|
|
|
56
|
-
function getBlock(blockType: string): Component | undefined {
|
|
57
|
-
// Generate name variations and try to find a match
|
|
58
|
-
const nameVariations = generateNameVariations(blockType);
|
|
59
|
-
|
|
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;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function getLayout(layoutName: string): Component | undefined {
|
|
70
|
-
// Generate name variations and try to find a match in layoutModules keys (file paths)
|
|
71
|
-
const nameVariations = generateNameVariations(layoutName);
|
|
72
|
-
|
|
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;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
55
|
// Get the layout component based on the layout prop
|
|
83
56
|
const layoutComponent = computed(() => {
|
|
84
|
-
return getLayout(props.layout);
|
|
57
|
+
return getLayout(props.layout || props.settings?.layout);
|
|
85
58
|
});
|
|
86
59
|
|
|
87
60
|
// Get the margin class for the block
|
|
@@ -102,16 +75,23 @@ function getMarginClass(block: Block): string {
|
|
|
102
75
|
|
|
103
76
|
onBeforeMount(async () => {
|
|
104
77
|
isReady.value = false;
|
|
105
|
-
|
|
106
|
-
|
|
78
|
+
|
|
79
|
+
// Initialise registries for editor preview
|
|
80
|
+
// Exclude the editing registry to load only the theme and blocks
|
|
81
|
+
await initialiseRegistry(props.theme, false);
|
|
82
|
+
|
|
107
83
|
isReady.value = true;
|
|
108
84
|
});
|
|
109
85
|
</script>
|
|
110
86
|
|
|
111
|
-
<style
|
|
87
|
+
<style lang="scss">
|
|
112
88
|
@use "../../assets/styles/mixins" as *;
|
|
113
89
|
|
|
114
|
-
|
|
115
|
-
|
|
90
|
+
// Styles are scoped to the component using the root class selector instead of Vue's scoped attribute
|
|
91
|
+
// This ensures styles work correctly when the component is consumed from a library
|
|
92
|
+
.page-renderer-wrapper {
|
|
93
|
+
.block-wrapper {
|
|
94
|
+
@include block-margin-classes;
|
|
95
|
+
}
|
|
116
96
|
}
|
|
117
97
|
</style>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<h4 class="mt-1 text-lg font-bold">Page settings</h4>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
|
-
<div class="border-b border-gray-300 p-5">
|
|
14
|
+
<div v-if="availableLayouts.length > 1" class="border-b border-gray-300 p-5">
|
|
15
15
|
<!-- Page layout -->
|
|
16
16
|
<div class="editor-field-node">
|
|
17
17
|
<!-- Label -->
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
:fields="pageSettingsFields"
|
|
38
38
|
:editable="true"
|
|
39
39
|
:isLayoutBlock="true"
|
|
40
|
+
:activeTab="activeTab"
|
|
40
41
|
/>
|
|
41
42
|
</div>
|
|
42
43
|
</div>
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
|
|
45
46
|
<script setup lang="ts">
|
|
46
47
|
import { computed, onBeforeMount, onMounted, ref } from "vue";
|
|
47
|
-
import {
|
|
48
|
+
import { themeLayouts } from "../../util/theme-registry";
|
|
48
49
|
import BlockEditorFields from "../BlockEditorFields/BlockEditorFields.vue";
|
|
49
50
|
|
|
50
51
|
const emit = defineEmits<{
|
|
@@ -57,23 +58,26 @@ const pageSettingsFields = ref<any>({});
|
|
|
57
58
|
const props = withDefaults(
|
|
58
59
|
defineProps<{
|
|
59
60
|
settingsKey?: string;
|
|
61
|
+
activeTab?: string;
|
|
60
62
|
}>(),
|
|
61
63
|
{
|
|
62
64
|
settingsKey: "settings",
|
|
65
|
+
activeTab: undefined,
|
|
63
66
|
}
|
|
64
67
|
);
|
|
65
68
|
const availableLayouts = computed(() => {
|
|
66
|
-
return
|
|
69
|
+
return Object.values(themeLayouts.value) || [];
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
function getLayoutSettings() {
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const activeLayout = pageData.value[props.settingsKey].layout;
|
|
74
|
+
if (!activeLayout) return;
|
|
75
|
+
pageSettingsFields.value = themeLayouts.value[activeLayout]?.fields || null;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
onBeforeMount(() => {
|
|
75
79
|
if (!pageData.value[props.settingsKey]) {
|
|
76
|
-
pageData.value.
|
|
80
|
+
pageData.value[props.settingsKey] = {};
|
|
77
81
|
}
|
|
78
82
|
});
|
|
79
83
|
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
<div class="resize-handle-wrapper group">
|
|
3
|
+
<div
|
|
4
|
+
id="page-builder-resize-handle"
|
|
5
|
+
ref="resizeHandle"
|
|
6
|
+
class="resize-handle shrink-0 cursor-col-resize transition-colors duration-200"
|
|
7
|
+
@mousedown="startResize"
|
|
8
|
+
></div>
|
|
9
|
+
<span
|
|
10
|
+
class="viewport-size absolute right-1 top-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
|
11
|
+
>
|
|
12
|
+
<div class="rounded-sm bg-yellow-400 px-1.5 py-1 font-mono text-xs font-medium text-gray-900">
|
|
13
|
+
{{ computedViewportSize }}
|
|
14
|
+
</div>
|
|
15
|
+
</span>
|
|
16
|
+
</div>
|
|
8
17
|
</template>
|
|
9
18
|
|
|
10
19
|
<script setup lang="ts">
|
|
11
|
-
import { ref } from "vue";
|
|
12
|
-
import { defineEmits } from "vue";
|
|
20
|
+
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
|
13
21
|
|
|
14
22
|
const emit = defineEmits<{
|
|
15
23
|
(e: "sidebarWidth", width: number): void;
|
|
@@ -18,6 +26,9 @@ const emit = defineEmits<{
|
|
|
18
26
|
const resizeHandle = ref<HTMLElement | null>(null);
|
|
19
27
|
const sidebarWidth = ref(380); // Default sidebar width (380px)
|
|
20
28
|
const isResizing = ref(false);
|
|
29
|
+
const viewportWidth = ref<number>(0);
|
|
30
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
31
|
+
let updateViewportWidthFn: (() => void) | null = null;
|
|
21
32
|
|
|
22
33
|
// Resize functionality
|
|
23
34
|
function startResize(event: MouseEvent) {
|
|
@@ -50,15 +61,62 @@ function startResize(event: MouseEvent) {
|
|
|
50
61
|
document.body.style.cursor = "col-resize";
|
|
51
62
|
document.body.style.userSelect = "none";
|
|
52
63
|
}
|
|
64
|
+
|
|
65
|
+
// Calculate viewport width based on #page-viewport element
|
|
66
|
+
const computedViewportSize = computed(() => {
|
|
67
|
+
return viewportWidth.value > 0 ? `${viewportWidth.value}px` : "0px";
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Setup ResizeObserver to watch #page-viewport element
|
|
71
|
+
onMounted(() => {
|
|
72
|
+
updateViewportWidthFn = () => {
|
|
73
|
+
const viewportElement = document.getElementById("page-preview-container");
|
|
74
|
+
if (viewportElement) {
|
|
75
|
+
viewportWidth.value = viewportElement.offsetWidth;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Initial measurement
|
|
80
|
+
updateViewportWidthFn();
|
|
81
|
+
|
|
82
|
+
// Use ResizeObserver to watch for size changes
|
|
83
|
+
const viewportElement = document.getElementById("page-preview-container");
|
|
84
|
+
if (viewportElement) {
|
|
85
|
+
resizeObserver = new ResizeObserver(() => {
|
|
86
|
+
if (updateViewportWidthFn) {
|
|
87
|
+
updateViewportWidthFn();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
resizeObserver.observe(viewportElement);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Also listen for window resize events as a fallback
|
|
94
|
+
window.addEventListener("resize", updateViewportWidthFn);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
onBeforeUnmount(() => {
|
|
98
|
+
if (resizeObserver) {
|
|
99
|
+
resizeObserver.disconnect();
|
|
100
|
+
resizeObserver = null;
|
|
101
|
+
}
|
|
102
|
+
if (updateViewportWidthFn) {
|
|
103
|
+
window.removeEventListener("resize", updateViewportWidthFn);
|
|
104
|
+
updateViewportWidthFn = null;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
53
107
|
</script>
|
|
54
108
|
|
|
55
109
|
<style scoped lang="scss">
|
|
56
110
|
/* Resize handle styles */
|
|
57
|
-
.resize-handle {
|
|
111
|
+
.resize-handle-wrapper {
|
|
58
112
|
position: sticky;
|
|
59
113
|
top: 0;
|
|
60
114
|
right: 0;
|
|
61
|
-
z-index:
|
|
115
|
+
z-index: 30;
|
|
116
|
+
height: var(--editor-height);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.resize-handle {
|
|
62
120
|
width: 3px;
|
|
63
121
|
height: var(--editor-height);
|
|
64
122
|
cursor: col-resize;
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { mount } from "@vue/test-utils";
|
|
3
|
-
import
|
|
3
|
+
import WswgPageBuilder from "./WswgPageBuilder.vue";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
5
|
+
describe("WswgPageBuilder", () => {
|
|
6
6
|
it("renders the placeholder component", () => {
|
|
7
|
-
const wrapper = mount(
|
|
7
|
+
const wrapper = mount(WswgPageBuilder);
|
|
8
8
|
|
|
9
|
-
expect(wrapper.text()).toContain("
|
|
9
|
+
expect(wrapper.text()).toContain("WswgPageBuilder");
|
|
10
10
|
expect(wrapper.text()).toContain("This is a placeholder component");
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it("renders header slot content when provided", () => {
|
|
14
14
|
const headerContent = "Custom Header Content";
|
|
15
|
-
const wrapper = mount(
|
|
15
|
+
const wrapper = mount(WswgPageBuilder, {
|
|
16
16
|
slots: {
|
|
17
17
|
header: headerContent,
|
|
18
18
|
},
|
|
@@ -22,7 +22,7 @@ describe("WswgJsonEditor", () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it("renders header slot with HTML content", () => {
|
|
25
|
-
const wrapper = mount(
|
|
25
|
+
const wrapper = mount(WswgPageBuilder, {
|
|
26
26
|
slots: {
|
|
27
27
|
header: "<div class='custom-header'>Header Title</div>",
|
|
28
28
|
},
|
|
@@ -33,10 +33,10 @@ describe("WswgJsonEditor", () => {
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it("does not render header slot when not provided", () => {
|
|
36
|
-
const wrapper = mount(
|
|
36
|
+
const wrapper = mount(WswgPageBuilder);
|
|
37
37
|
|
|
38
38
|
// The component should still render without the header slot
|
|
39
|
-
expect(wrapper.text()).toContain("
|
|
39
|
+
expect(wrapper.text()).toContain("WswgPageBuilder");
|
|
40
40
|
// Header slot should be empty/not present
|
|
41
41
|
expect(wrapper.html()).not.toContain("custom-header");
|
|
42
42
|
});
|