vue-wswg-editor 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/dist/style.css +1 -0
- package/dist/types/components/AddBlockItem/AddBlockItem.vue.d.ts +6 -0
- package/dist/types/components/BlockBrowser/BlockBrowser.vue.d.ts +2 -0
- package/dist/types/components/BlockComponent/BlockComponent.vue.d.ts +15 -0
- package/dist/types/components/BlockEditorFieldNode/BlockEditorFieldNode.vue.d.ts +15 -0
- package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +15 -0
- package/dist/types/components/BlockMarginFieldNode/BlockMarginNode.vue.d.ts +23 -0
- package/dist/types/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue.d.ts +15 -0
- package/dist/types/components/BrowserNavigation/BrowserNavigation.vue.d.ts +5 -0
- package/dist/types/components/EmptyState/EmptyState.vue.d.ts +15 -0
- package/dist/types/components/PageBlockList/PageBlockList.vue.d.ts +19 -0
- package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +30 -0
- package/dist/types/components/PageBuilderToolbar/PageBuilderToolbar.vue.d.ts +28 -0
- package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +6 -0
- package/dist/types/components/PageRenderer/blockModules.d.ts +1 -0
- package/dist/types/components/PageSettings/PageSettings.vue.d.ts +15 -0
- package/dist/types/components/ResizeHandle/ResizeHandle.vue.d.ts +6 -0
- package/dist/types/components/WswgJsonEditor/WswgJsonEditor.test.d.ts +1 -0
- package/dist/types/components/WswgJsonEditor/WswgJsonEditor.vue.d.ts +40 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dist/types/util/fieldConfig.d.ts +82 -0
- package/dist/types/util/helpers.d.ts +28 -0
- package/dist/types/util/registry.d.ts +21 -0
- package/dist/types/util/validation.d.ts +15 -0
- package/dist/vue-wswg-editor.es.js +3377 -0
- package/package.json +85 -0
- package/src/assets/images/empty-state.jpg +0 -0
- package/src/assets/styles/_mixins.scss +73 -0
- package/src/assets/styles/main.css +3 -0
- package/src/components/AddBlockItem/AddBlockItem.vue +50 -0
- package/src/components/BlockBrowser/BlockBrowser.vue +69 -0
- package/src/components/BlockComponent/BlockComponent.vue +186 -0
- package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +378 -0
- package/src/components/BlockEditorFields/BlockEditorFields.vue +91 -0
- package/src/components/BlockMarginFieldNode/BlockMarginNode.vue +132 -0
- package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +217 -0
- package/src/components/BrowserNavigation/BrowserNavigation.vue +27 -0
- package/src/components/EmptyState/EmptyState.vue +94 -0
- package/src/components/PageBlockList/PageBlockList.vue +103 -0
- package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +241 -0
- package/src/components/PageBuilderToolbar/PageBuilderToolbar.vue +63 -0
- package/src/components/PageRenderer/PageRenderer.vue +65 -0
- package/src/components/PageRenderer/blockModules-alternative.ts.example +9 -0
- package/src/components/PageRenderer/blockModules-manual.ts.example +19 -0
- package/src/components/PageRenderer/blockModules-runtime.ts.example +23 -0
- package/src/components/PageRenderer/blockModules.ts +3 -0
- package/src/components/PageSettings/PageSettings.vue +86 -0
- package/src/components/ResizeHandle/ResizeHandle.vue +105 -0
- package/src/components/WswgJsonEditor/WswgJsonEditor.test.ts +43 -0
- package/src/components/WswgJsonEditor/WswgJsonEditor.vue +391 -0
- package/src/index.ts +15 -0
- package/src/shims.d.ts +72 -0
- package/src/style.css +3 -0
- package/src/types/Block.d.ts +19 -0
- package/src/types/Layout.d.ts +9 -0
- package/src/util/fieldConfig.ts +173 -0
- package/src/util/helpers.ts +176 -0
- package/src/util/registry.ts +149 -0
- package/src/util/validation.ts +110 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Control bar -->
|
|
3
|
+
<div class="flex divide-x border-b">
|
|
4
|
+
<slot name="default">
|
|
5
|
+
<!-- no default toolbar content -->
|
|
6
|
+
</slot>
|
|
7
|
+
|
|
8
|
+
<!-- Desktop / Mobile view toggle -->
|
|
9
|
+
<div v-if="hasPageSettings" class="ml-auto inline-flex gap-2 px-5 py-2.5">
|
|
10
|
+
<button
|
|
11
|
+
class="inline-flex items-center rounded-md border px-3 py-2 text-xs"
|
|
12
|
+
:class="
|
|
13
|
+
showPageSettings
|
|
14
|
+
? 'bg-blue-50 text-blue-700 border-blue-800/20'
|
|
15
|
+
: 'bg-zinc-50 text-zinc-500 hover:border-zinc-400 hover:text-zinc-900'
|
|
16
|
+
"
|
|
17
|
+
title="Page settings"
|
|
18
|
+
@click="
|
|
19
|
+
showPageSettings = !showPageSettings;
|
|
20
|
+
activeBlock = null;
|
|
21
|
+
"
|
|
22
|
+
>
|
|
23
|
+
<Cog6ToothIcon class="size-4" />
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="inline-flex gap-2 px-5 py-2.5">
|
|
27
|
+
<button
|
|
28
|
+
class="inline-flex items-center rounded-md border px-3 py-2 text-xs"
|
|
29
|
+
:class="
|
|
30
|
+
editorViewport === 'mobile'
|
|
31
|
+
? 'bg-blue-50 text-blue-700 border-blue-800/20'
|
|
32
|
+
: 'bg-zinc-50 text-zinc-500 hover:border-zinc-400 hover:text-zinc-900'
|
|
33
|
+
"
|
|
34
|
+
title="Mobile view"
|
|
35
|
+
@click="editorViewport = 'mobile'"
|
|
36
|
+
>
|
|
37
|
+
<DevicePhoneMobileIcon class="size-4" />
|
|
38
|
+
</button>
|
|
39
|
+
<button
|
|
40
|
+
class="inline-flex items-center rounded-md border px-3 py-2 text-xs"
|
|
41
|
+
:class="
|
|
42
|
+
editorViewport === 'desktop'
|
|
43
|
+
? 'bg-blue-50 text-blue-700 border-blue-800/20'
|
|
44
|
+
: 'bg-zinc-50 text-zinc-500 hover:border-zinc-400 hover:text-zinc-900'
|
|
45
|
+
"
|
|
46
|
+
title="Desktop view"
|
|
47
|
+
@click="editorViewport = 'desktop'"
|
|
48
|
+
>
|
|
49
|
+
<ComputerDesktopIcon class="size-4" />
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
const editorViewport = defineModel<"desktop" | "mobile">("editorViewport");
|
|
56
|
+
const activeBlock = defineModel<any>("activeBlock");
|
|
57
|
+
const showPageSettings = defineModel<boolean>("showPageSettings");
|
|
58
|
+
import { ComputerDesktopIcon, DevicePhoneMobileIcon, Cog6ToothIcon } from "@heroicons/vue/24/outline";
|
|
59
|
+
|
|
60
|
+
defineProps<{
|
|
61
|
+
hasPageSettings: boolean;
|
|
62
|
+
}>();
|
|
63
|
+
</script>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div id="page-blocks-wrapper">
|
|
3
|
+
<div v-for="block in blocks" :key="block.id" class="block-wrapper" :class="{ [getMarginClass(block)]: true }">
|
|
4
|
+
<component :is="getBlock(block.type)" v-bind="block" :key="`block-${block.id}`" />
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { type Component } from "vue";
|
|
11
|
+
import { generateNameVariations } from "../../util/helpers";
|
|
12
|
+
import { blockModules } from "./blockModules";
|
|
13
|
+
import type { Block } from "../../types/Block";
|
|
14
|
+
|
|
15
|
+
defineProps<{
|
|
16
|
+
blocks: Block[];
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
function getBlock(blockType: string): Component | undefined {
|
|
20
|
+
// Generate name variations and try to find a match in blockModules keys (file paths)
|
|
21
|
+
const nameVariations = generateNameVariations(blockType);
|
|
22
|
+
|
|
23
|
+
// Iterate through all blockModules entries
|
|
24
|
+
for (const [filePath, module] of Object.entries(blockModules)) {
|
|
25
|
+
// Check if any variation matches the file path
|
|
26
|
+
for (const variation of nameVariations) {
|
|
27
|
+
// Check if the file path contains the variation followed by .vue
|
|
28
|
+
// e.g., "hero-section" matches "blocks/hero-section/hero-section.vue"
|
|
29
|
+
if (filePath.includes(`${variation}.vue`)) {
|
|
30
|
+
// Extract the default export (the Vue component)
|
|
31
|
+
const component = (module as any).default;
|
|
32
|
+
if (component) {
|
|
33
|
+
return component;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get the margin class for the block
|
|
43
|
+
// Margin is an object with top and bottom properties
|
|
44
|
+
// margin classses are formatted as `margin-<direction>-<size>`
|
|
45
|
+
function getMarginClass(block: Block): string {
|
|
46
|
+
const top = block.margin?.top || "none";
|
|
47
|
+
const bottom = block.margin?.bottom || "none";
|
|
48
|
+
|
|
49
|
+
// Map margin sizes to custom class names: none, sm, md, lg, xl
|
|
50
|
+
const getClass = (size: string, direction: "top" | "bottom"): string => {
|
|
51
|
+
const normalizedSize = size === "small" ? "sm" : size === "medium" ? "md" : size === "large" ? "lg" : size;
|
|
52
|
+
return `margin-${direction}-${normalizedSize}`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return [getClass(top, "top"), getClass(bottom, "bottom")].join(" ");
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<style scoped lang="scss">
|
|
60
|
+
@use "../../assets/styles/mixins" as *;
|
|
61
|
+
|
|
62
|
+
.block-wrapper {
|
|
63
|
+
@include block-margin-classes;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Alternative 1: Use relative paths instead of aliases
|
|
2
|
+
// This works because the consuming app processes this file
|
|
3
|
+
// The path is relative to where the consuming app's vite.config.ts is located
|
|
4
|
+
// For participant/admin projects, this resolves to ../../page-builder/blocks/**/*.vue
|
|
5
|
+
export const blockModules = import.meta.glob("../../page-builder/blocks/**/*.vue", { eager: true });
|
|
6
|
+
|
|
7
|
+
// However, this is fragile because it depends on the file location relative to the consuming app
|
|
8
|
+
// Better: Use a build-time script (see Alternative 2)
|
|
9
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Alternative 4: Manual explicit imports
|
|
2
|
+
// Most explicit, but requires updating when adding new blocks
|
|
3
|
+
|
|
4
|
+
import HeroSection from "../../../../page-builder/blocks/hero-section/hero-section.vue";
|
|
5
|
+
import CardsSection from "../../../../page-builder/blocks/cards-section/cards-section.vue";
|
|
6
|
+
import FaqSection from "../../../../page-builder/blocks/faq-section/faq-section.vue";
|
|
7
|
+
import FlexibleContent from "../../../../page-builder/blocks/flexible-content/flexible-content.vue";
|
|
8
|
+
import TabsSection from "../../../../page-builder/blocks/tabs-section/tabs-section.vue";
|
|
9
|
+
import TextSection from "../../../../page-builder/blocks/text-section/text-section.vue";
|
|
10
|
+
|
|
11
|
+
export const blockModules = {
|
|
12
|
+
"hero-section": HeroSection,
|
|
13
|
+
"cards-section": CardsSection,
|
|
14
|
+
"faq-section": FaqSection,
|
|
15
|
+
"flexible-content": FlexibleContent,
|
|
16
|
+
"tabs-section": TabsSection,
|
|
17
|
+
"text-section": TextSection,
|
|
18
|
+
};
|
|
19
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Alternative 3: Runtime registration pattern
|
|
2
|
+
// Components register themselves when imported
|
|
3
|
+
|
|
4
|
+
import { ref, type Ref } from "vue";
|
|
5
|
+
import type { Component } from "vue";
|
|
6
|
+
|
|
7
|
+
// Registry store
|
|
8
|
+
export const blockRegistry: Ref<Record<string, Component>> = ref({});
|
|
9
|
+
|
|
10
|
+
// Registration function
|
|
11
|
+
export function registerBlock(type: string, component: Component) {
|
|
12
|
+
blockRegistry.value[type] = component;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// In each block component, add:
|
|
16
|
+
// import { registerBlock } from "./blockModules-runtime";
|
|
17
|
+
// registerBlock("hero-section", defineComponent({ ... }));
|
|
18
|
+
|
|
19
|
+
// Then manually import blocks:
|
|
20
|
+
// import "./blocks/hero-section/hero-section.vue";
|
|
21
|
+
// import "./blocks/cards-section/cards-section.vue";
|
|
22
|
+
// etc.
|
|
23
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="page-settings">
|
|
3
|
+
<div class="flex items-start justify-between border-b bg-white p-5">
|
|
4
|
+
<div>
|
|
5
|
+
<button
|
|
6
|
+
class="cursor-pointer text-sm text-zinc-500 hover:text-zinc-900 hover:underline"
|
|
7
|
+
@click="emit('close')"
|
|
8
|
+
>
|
|
9
|
+
← Back
|
|
10
|
+
</button>
|
|
11
|
+
<h4 class="mt-1 text-lg font-bold">Page settings</h4>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="border-b p-5">
|
|
15
|
+
<!-- Page layout -->
|
|
16
|
+
<div class="editor-field-node">
|
|
17
|
+
<!-- Label -->
|
|
18
|
+
<div class="mb-1.5 flex items-center gap-1.5">
|
|
19
|
+
<label class="mr-auto font-medium first-letter:uppercase">Page layout</label>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<select v-model="pageData.settings.layout" class="form-control" @change="getLayoutSettings">
|
|
23
|
+
<option v-for="layout in availableLayouts" :key="`layout-${layout.__name}`" :value="layout.__name">
|
|
24
|
+
{{ layout.label }}
|
|
25
|
+
</option>
|
|
26
|
+
</select>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<!-- Page settings -->
|
|
30
|
+
<div class="editor-field-node">
|
|
31
|
+
<BlockEditorFields
|
|
32
|
+
v-model="pageData.settings"
|
|
33
|
+
:fields="pageSettingsFields"
|
|
34
|
+
:editable="true"
|
|
35
|
+
:isLayoutBlock="true"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script setup lang="ts">
|
|
42
|
+
import { computed, onBeforeMount, onMounted, ref } from "vue";
|
|
43
|
+
import { getLayoutFields, getLayouts } from "../../util/registry";
|
|
44
|
+
import BlockEditorFields from "../BlockEditorFields/BlockEditorFields.vue";
|
|
45
|
+
|
|
46
|
+
const emit = defineEmits<{
|
|
47
|
+
(e: "close"): void;
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
const pageData = defineModel<any>();
|
|
51
|
+
const pageSettingsFields = ref<any>({});
|
|
52
|
+
|
|
53
|
+
defineProps<{
|
|
54
|
+
title?: string;
|
|
55
|
+
}>();
|
|
56
|
+
|
|
57
|
+
const availableLayouts = computed(() => {
|
|
58
|
+
return getLayouts();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function getLayoutSettings() {
|
|
62
|
+
if (!pageData.value.settings.layout) return;
|
|
63
|
+
pageSettingsFields.value = getLayoutFields(pageData.value.settings.layout) || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onBeforeMount(() => {
|
|
67
|
+
if (!pageData.value.settings) {
|
|
68
|
+
pageData.value.settings = null;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
onMounted(() => {
|
|
73
|
+
getLayoutSettings();
|
|
74
|
+
});
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<style scoped lang="scss">
|
|
78
|
+
select.form-control {
|
|
79
|
+
padding-right: 32px;
|
|
80
|
+
appearance: none;
|
|
81
|
+
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); // down chevron
|
|
82
|
+
background-repeat: no-repeat;
|
|
83
|
+
background-position: right 8px center;
|
|
84
|
+
background-size: 20px;
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
id="page-builder-resize-handle"
|
|
4
|
+
ref="resizeHandle"
|
|
5
|
+
class="resize-handle shrink-0 cursor-col-resize transition-colors duration-200"
|
|
6
|
+
@mousedown="startResize"
|
|
7
|
+
></div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { ref } from "vue";
|
|
12
|
+
import { defineEmits } from "vue";
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
(e: "sidebarWidth", width: number): void;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const resizeHandle = ref<HTMLElement | null>(null);
|
|
19
|
+
const sidebarWidth = ref(380); // Default sidebar width (380px)
|
|
20
|
+
const isResizing = ref(false);
|
|
21
|
+
|
|
22
|
+
// Resize functionality
|
|
23
|
+
function startResize(event: MouseEvent) {
|
|
24
|
+
isResizing.value = true;
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
|
|
27
|
+
const startX = event.clientX;
|
|
28
|
+
const startSidebarWidth = sidebarWidth.value;
|
|
29
|
+
|
|
30
|
+
function handleMouseMove(event: MouseEvent) {
|
|
31
|
+
if (!isResizing.value) return;
|
|
32
|
+
|
|
33
|
+
const deltaX = event.clientX - startX;
|
|
34
|
+
const newSidebarWidth = Math.max(300, Math.min(800, startSidebarWidth - deltaX)); // Min 300px, Max 800px
|
|
35
|
+
|
|
36
|
+
sidebarWidth.value = newSidebarWidth;
|
|
37
|
+
emit("sidebarWidth", newSidebarWidth);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handleMouseUp() {
|
|
41
|
+
isResizing.value = false;
|
|
42
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
43
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
44
|
+
document.body.style.cursor = "";
|
|
45
|
+
document.body.style.userSelect = "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
49
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
50
|
+
document.body.style.cursor = "col-resize";
|
|
51
|
+
document.body.style.userSelect = "none";
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style scoped lang="scss">
|
|
56
|
+
/* Resize handle styles */
|
|
57
|
+
.resize-handle {
|
|
58
|
+
position: relative;
|
|
59
|
+
top: 0;
|
|
60
|
+
right: 0;
|
|
61
|
+
z-index: 2;
|
|
62
|
+
width: 3px;
|
|
63
|
+
height: 100%;
|
|
64
|
+
cursor: col-resize;
|
|
65
|
+
background-color: var(--grey-20);
|
|
66
|
+
transition: all 0.2s ease-in-out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.resize-handle:hover {
|
|
70
|
+
width: 3px;
|
|
71
|
+
background-color: var(--yellow-40);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.resize-handle:active {
|
|
75
|
+
width: 3px;
|
|
76
|
+
background-color: var(--yellow-50);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Add a subtle indicator when resizing */
|
|
80
|
+
.resize-handle::before {
|
|
81
|
+
position: absolute;
|
|
82
|
+
top: 50%;
|
|
83
|
+
left: -6px;
|
|
84
|
+
z-index: 20;
|
|
85
|
+
width: 14px;
|
|
86
|
+
height: 40px;
|
|
87
|
+
content: "";
|
|
88
|
+
background-color: var(--yellow-40);
|
|
89
|
+
border-radius: var(--border-radius-16);
|
|
90
|
+
opacity: 0;
|
|
91
|
+
transform: translateY(-50%);
|
|
92
|
+
transition:
|
|
93
|
+
opacity 0.2s ease-in-out,
|
|
94
|
+
background-color 0.2s ease-in-out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.resize-handle:hover::before {
|
|
98
|
+
opacity: 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.resize-handle:active::before {
|
|
102
|
+
background-color: var(--yellow-50);
|
|
103
|
+
opacity: 1;
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mount } from "@vue/test-utils";
|
|
3
|
+
import WswgJsonEditor from "./WswgJsonEditor.vue";
|
|
4
|
+
|
|
5
|
+
describe("WswgJsonEditor", () => {
|
|
6
|
+
it("renders the placeholder component", () => {
|
|
7
|
+
const wrapper = mount(WswgJsonEditor);
|
|
8
|
+
|
|
9
|
+
expect(wrapper.text()).toContain("WswgJsonEditor");
|
|
10
|
+
expect(wrapper.text()).toContain("This is a placeholder component");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("renders header slot content when provided", () => {
|
|
14
|
+
const headerContent = "Custom Header Content";
|
|
15
|
+
const wrapper = mount(WswgJsonEditor, {
|
|
16
|
+
slots: {
|
|
17
|
+
header: headerContent,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(wrapper.text()).toContain(headerContent);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders header slot with HTML content", () => {
|
|
25
|
+
const wrapper = mount(WswgJsonEditor, {
|
|
26
|
+
slots: {
|
|
27
|
+
header: "<div class='custom-header'>Header Title</div>",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(wrapper.find(".custom-header").exists()).toBe(true);
|
|
32
|
+
expect(wrapper.text()).toContain("Header Title");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("does not render header slot when not provided", () => {
|
|
36
|
+
const wrapper = mount(WswgJsonEditor);
|
|
37
|
+
|
|
38
|
+
// The component should still render without the header slot
|
|
39
|
+
expect(wrapper.text()).toContain("WswgJsonEditor");
|
|
40
|
+
// Header slot should be empty/not present
|
|
41
|
+
expect(wrapper.html()).not.toContain("custom-header");
|
|
42
|
+
});
|
|
43
|
+
});
|