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
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "vue-wswg-editor",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
- "main": "src/index.ts",
6
- "module": "src/index.ts",
7
- "typings": "src/index.ts",
5
+ "main": "./dist/vue-wswg-editor.es.js",
6
+ "module": "./dist/vue-wswg-editor.es.js",
7
+ "typings": "./dist/types/index.d.ts",
8
+ "types": "./dist/types/index.d.ts",
8
9
  "exports": {
9
10
  ".": {
10
- "import": "./src/index.ts",
11
- "require": "./src/index.ts",
12
- "types": "./src/index.ts"
11
+ "import": "./dist/vue-wswg-editor.es.js",
12
+ "require": "./dist/vue-wswg-editor.es.js",
13
+ "types": "./dist/types/index.d.ts"
13
14
  },
14
15
  "./PageRenderer": {
15
16
  "import": "./src/components/PageRenderer/PageRenderer.vue",
@@ -24,12 +25,16 @@
24
25
  },
25
26
  "files": [
26
27
  "dist",
27
- "src"
28
+ "src",
29
+ "types"
28
30
  ],
29
31
  "scripts": {
30
32
  "dev": "concurrently \"vite build --watch\" \"vite build --config vite-plugin.config.ts --watch\"",
31
33
  "build": "vite build && vite build --config vite-plugin.config.ts && vue-tsc --declaration --emitDeclarationOnly",
32
34
  "preview": "vite preview",
35
+ "docs:dev": "vitepress dev docs",
36
+ "docs:build": "vitepress build docs",
37
+ "docs:preview": "vitepress preview docs",
33
38
  "test:unit": "vitest",
34
39
  "test:unit:ui": "vitest --ui",
35
40
  "test:unit:ci": "vitest run",
@@ -82,6 +87,8 @@
82
87
  "tailwindcss": "^3.4.10",
83
88
  "typescript": "~5.8.3",
84
89
  "vite": "^5.4.19",
90
+ "vitepress": "^1.6.4",
91
+ "vitepress-demo-plugin": "^1.5.1",
85
92
  "vitest": "^2.1.9",
86
93
  "vue-tsc": "^2.2.12",
87
94
  "yup": "^1.7.1"
@@ -3,64 +3,59 @@
3
3
  // Usage: @include block-margin-classes;
4
4
 
5
5
  @mixin block-margin-classes {
6
- // Margin size variables
7
- $sm-size: 2rem;
8
- $md-size: 4rem;
9
- $lg-size: 6rem;
10
-
11
6
  // Top margin classes
12
7
  &.margin-top-sm {
13
- padding-top: $sm-size;
8
+ padding-top: var(--block-margin-sm, 2rem);
14
9
 
15
10
  &::before {
16
11
  top: 0;
17
- height: $sm-size;
12
+ height: var(--block-margin-sm, 2rem);
18
13
  }
19
14
  }
20
15
 
21
16
  &.margin-top-md {
22
- padding-top: $md-size;
17
+ padding-top: var(--block-margin-md, 4rem);
23
18
 
24
19
  &::before {
25
20
  top: 0;
26
- height: $md-size;
21
+ height: var(--block-margin-md, 4rem);
27
22
  }
28
23
  }
29
24
 
30
25
  &.margin-top-lg {
31
- padding-top: $lg-size;
26
+ padding-top: var(--block-margin-lg, 6rem);
32
27
 
33
28
  &::before {
34
29
  top: 0;
35
- height: $lg-size;
30
+ height: var(--block-margin-lg, 6rem);
36
31
  }
37
32
  }
38
33
 
39
34
  // Bottom margin classes
40
35
  &.margin-bottom-sm {
41
- padding-bottom: $sm-size;
36
+ padding-bottom: var(--block-margin-sm, 2rem);
42
37
 
43
38
  &::after {
44
39
  bottom: 0;
45
- height: $sm-size;
40
+ height: var(--block-margin-sm, 2rem);
46
41
  }
47
42
  }
48
43
 
49
44
  &.margin-bottom-md {
50
- padding-bottom: $md-size;
45
+ padding-bottom: var(--block-margin-md, 4rem);
51
46
 
52
47
  &::after {
53
48
  bottom: 0;
54
- height: $md-size;
49
+ height: var(--block-margin-md, 4rem);
55
50
  }
56
51
  }
57
52
 
58
53
  &.margin-bottom-lg {
59
- padding-bottom: $lg-size;
54
+ padding-bottom: var(--block-margin-lg, 6rem);
60
55
 
61
56
  &::after {
62
57
  bottom: 0;
63
- height: $lg-size;
58
+ height: var(--block-margin-lg, 6rem);
64
59
  }
65
60
  }
66
61
 
@@ -62,7 +62,9 @@
62
62
 
63
63
  <!-- Range input -->
64
64
  <div v-else-if="fieldConfig.type === 'range'" class="flex items-center gap-2">
65
- <span class="rounded-full bg-zinc-100 px-2 py-1 text-sm font-bold text-zinc-600">{{ fieldValue }}</span>
65
+ <span class="rounded-full bg-zinc-100 px-2 py-1 text-sm font-bold text-zinc-600"
66
+ >{{ fieldValue }}{{ fieldConfig.valueSuffix || "" }}</span
67
+ >
66
68
  <input
67
69
  v-model="fieldValue"
68
70
  type="range"
@@ -139,6 +141,23 @@
139
141
  />
140
142
  </div>
141
143
 
144
+ <!-- Object Input -->
145
+ <div v-else-if="fieldConfig.type === 'object' && fieldConfig.objectFields">
146
+ <div class="mt-3 border-t border-gray-300 pt-3">
147
+ <BlockEditorFields
148
+ v-model="objectFieldValue"
149
+ :fields="fieldConfig.objectFields"
150
+ :editable="editable"
151
+ :nested="true"
152
+ />
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Image -->
157
+ <div v-else-if="fieldConfig.type === 'image'">
158
+ <BlockImageNode v-model="fieldValue" :fieldConfig="fieldConfig" :fieldName="fieldName" :editable="editable" />
159
+ </div>
160
+
142
161
  <!-- Margin -->
143
162
  <div v-else-if="fieldConfig.type === 'margin'">
144
163
  <BlockMarginNode v-model="fieldValue" :fieldConfig="fieldConfig" :fieldName="fieldName" :editable="editable" />
@@ -169,7 +188,10 @@ import { ref, computed, watch } from "vue";
169
188
  import type { EditorFieldConfig } from "../../util/fieldConfig";
170
189
  import BlockRepeaterNode from "../BlockRepeaterFieldNode/BlockRepeaterNode.vue";
171
190
  import BlockMarginNode from "../BlockMarginFieldNode/BlockMarginNode.vue";
191
+ import BlockImageNode from "../BlockImageFieldNode/BlockImageNode.vue";
192
+ import BlockEditorFields from "../BlockEditorFields/BlockEditorFields.vue";
172
193
  import { InformationCircleIcon } from "@heroicons/vue/24/outline";
194
+ import { validateField as validateFieldUtil } from "../../util/validation";
173
195
  import * as yup from "yup";
174
196
 
175
197
  const fieldValue = defineModel<any>();
@@ -202,6 +224,20 @@ const checkboxValues = computed({
202
224
  },
203
225
  });
204
226
 
227
+ // Computed property for object field values - ensure it's always an object
228
+ const objectFieldValue = computed({
229
+ get: () => {
230
+ // Ensure fieldValue is always an object for object fields
231
+ if (typeof fieldValue.value !== "object" || fieldValue.value === null || Array.isArray(fieldValue.value)) {
232
+ return {};
233
+ }
234
+ return fieldValue.value;
235
+ },
236
+ set: (newValue: Record<string, any>) => {
237
+ fieldValue.value = newValue;
238
+ },
239
+ });
240
+
205
241
  // Computed property to handle object/array conversion for text inputs
206
242
  const textFieldValue = computed({
207
243
  get: () => {
@@ -257,12 +293,12 @@ const canClearFieldValue = computed(() => {
257
293
  * minItems // For repeater fields
258
294
  * maxItems // For repeater fields
259
295
  * required // For all fields
296
+ * validator // Custom validation function
260
297
  */
261
298
 
262
299
  async function validateField(): Promise<void> {
263
- if (!props.fieldConfig.validator) return;
264
300
  try {
265
- const result = await props.fieldConfig.validator(fieldValue.value);
301
+ const result = await validateFieldUtil(fieldValue.value, props.fieldConfig);
266
302
  // True = valid, false = invalid, string = error message
267
303
  if (result === true) {
268
304
  validationErrorMessage.value = null;
@@ -280,7 +316,6 @@ async function validateField(): Promise<void> {
280
316
  function clearFieldValue(): void {
281
317
  fieldValue.value = null;
282
318
  validationErrorMessage.value = null;
283
- // validateField();
284
319
  }
285
320
 
286
321
  // Watch the field value and validate it
@@ -1,7 +1,11 @@
1
1
  <template>
2
2
  <div class="section-editor-fields">
3
3
  <!-- Field group tabs-->
4
- <div v-if="editorFieldGroups.length" class="field-group-tabs flex gap-2 border-b border-gray-300 px-5 pt-3">
4
+ <div
5
+ v-if="editorFieldGroups.length"
6
+ class="field-group-tabs flex gap-2 border-b border-gray-300"
7
+ :class="nested ? 'px-0 pt-0' : 'px-5 pt-3'"
8
+ >
5
9
  <button
6
10
  v-for="fieldGroupName in editorFieldGroups"
7
11
  :key="`fg_${fieldGroupName}`"
@@ -14,7 +18,11 @@
14
18
  </div>
15
19
 
16
20
  <!-- Fields -->
17
- <div v-if="blockData && Object.keys(editorFields).length > 0" class="flex flex-col gap-3 p-5">
21
+ <div
22
+ v-if="blockData && Object.keys(editorFields).length > 0"
23
+ class="flex flex-col gap-3"
24
+ :class="nested ? 'p-0' : 'p-5'"
25
+ >
18
26
  <div v-for="(fieldConfig, fieldName) in editorFields" :key="fieldName" class="prop-field">
19
27
  <BlockEditorFieldNode
20
28
  v-model="blockData[fieldName]"
@@ -26,7 +34,7 @@
26
34
  </div>
27
35
 
28
36
  <!-- No fields -->
29
- <div v-else class="p-5">
37
+ <div v-else :class="nested ? 'p-0' : 'p-5'">
30
38
  <div class="rounded-lg bg-zinc-100 px-4 py-3 text-sm font-medium text-zinc-600">
31
39
  <p>
32
40
  {{ isLayoutBlock ? "No settings available for this layout." : "No options available for this block." }}
@@ -47,6 +55,7 @@ const { fields, editable } = defineProps<{
47
55
  fields?: Record<string, EditorFieldConfig>;
48
56
  editable: boolean;
49
57
  isLayoutBlock?: boolean;
58
+ nested?: boolean;
50
59
  }>();
51
60
 
52
61
  const editorFields = computed(() => {
@@ -0,0 +1,373 @@
1
+ <template>
2
+ <div class="image-upload-field mb-2">
3
+ <!-- Image Preview with Upload Area (Compact Layout) -->
4
+ <div
5
+ v-if="imageUrl && editable"
6
+ class="flex gap-2"
7
+ @dragover.prevent="handleDragOver"
8
+ @dragleave.prevent="handleDragLeave"
9
+ @drop.prevent="handleDrop"
10
+ >
11
+ <!-- Hidden file input -->
12
+ <input
13
+ ref="fileInputRef"
14
+ type="file"
15
+ accept="image/*"
16
+ class="sr-only"
17
+ :disabled="isUploading || !editable"
18
+ @change="handleFileSelect"
19
+ />
20
+
21
+ <!-- Thumbnail Preview -->
22
+ <div class="relative shrink-0">
23
+ <img :src="imageUrl" alt="Uploaded image" class="size-16 rounded border border-gray-300 object-cover" />
24
+ <button
25
+ type="button"
26
+ class="absolute -right-1 -top-1 rounded-full bg-red-500 p-0.5 text-white shadow-sm hover:bg-red-600"
27
+ title="Remove image"
28
+ @click.stop="removeImage"
29
+ >
30
+ <svg
31
+ xmlns="http://www.w3.org/2000/svg"
32
+ fill="none"
33
+ viewBox="0 0 24 24"
34
+ stroke-width="2.5"
35
+ stroke="currentColor"
36
+ class="size-3"
37
+ >
38
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
39
+ </svg>
40
+ </button>
41
+ </div>
42
+
43
+ <!-- Upload Button -->
44
+ <button
45
+ type="button"
46
+ :class="[
47
+ 'flex-1rounded w-full rounded border px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors',
48
+ isUploading && 'pointer-events-none opacity-50',
49
+ isDragging && 'border-blue-500 bg-blue-50',
50
+ !isDragging && 'border-gray-300 bg-white hover:bg-gray-50',
51
+ ]"
52
+ @click="triggerFileInput"
53
+ @dragover.prevent="handleDragOver"
54
+ @dragleave.prevent="handleDragLeave"
55
+ @drop.prevent="handleDrop"
56
+ >
57
+ <span v-if="!isUploading" class="flex items-center justify-center gap-1.5">
58
+ <svg
59
+ xmlns="http://www.w3.org/2000/svg"
60
+ fill="none"
61
+ viewBox="0 0 24 24"
62
+ stroke-width="1.5"
63
+ stroke="currentColor"
64
+ class="size-3.5"
65
+ >
66
+ <path
67
+ stroke-linecap="round"
68
+ stroke-linejoin="round"
69
+ d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
70
+ />
71
+ </svg>
72
+ Replace image
73
+ </span>
74
+ <span v-else class="flex items-center justify-center gap-1.5">
75
+ <svg
76
+ class="size-3.5 animate-spin text-blue-500"
77
+ xmlns="http://www.w3.org/2000/svg"
78
+ fill="none"
79
+ viewBox="0 0 24 24"
80
+ >
81
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
82
+ <path
83
+ class="opacity-75"
84
+ fill="currentColor"
85
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
86
+ ></path>
87
+ </svg>
88
+ Uploading...
89
+ </span>
90
+ </button>
91
+ </div>
92
+
93
+ <!-- Upload Area (No Image) -->
94
+ <div
95
+ v-else-if="editable"
96
+ :class="[
97
+ 'relative cursor-pointer rounded border border-dashed transition-colors',
98
+ isDragging
99
+ ? 'border-blue-500 bg-blue-50'
100
+ : 'border-gray-300 bg-gray-50 hover:border-gray-400 hover:bg-gray-100',
101
+ isUploading && 'pointer-events-none opacity-50',
102
+ ]"
103
+ @click="triggerFileInput"
104
+ @dragover.prevent="handleDragOver"
105
+ @dragleave.prevent="handleDragLeave"
106
+ @drop.prevent="handleDrop"
107
+ >
108
+ <input
109
+ ref="fileInputRef"
110
+ type="file"
111
+ accept="image/*"
112
+ class="sr-only"
113
+ :disabled="isUploading || !editable"
114
+ @change="handleFileSelect"
115
+ />
116
+
117
+ <div class="flex items-center justify-center gap-2 p-3">
118
+ <!-- Upload Icon -->
119
+ <svg
120
+ v-if="!isUploading"
121
+ xmlns="http://www.w3.org/2000/svg"
122
+ fill="none"
123
+ viewBox="0 0 24 24"
124
+ stroke-width="1.5"
125
+ stroke="currentColor"
126
+ class="size-4 text-gray-400"
127
+ >
128
+ <path
129
+ stroke-linecap="round"
130
+ stroke-linejoin="round"
131
+ d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
132
+ />
133
+ </svg>
134
+
135
+ <!-- Loading Spinner -->
136
+ <svg
137
+ v-else
138
+ class="size-4 animate-spin text-blue-500"
139
+ xmlns="http://www.w3.org/2000/svg"
140
+ fill="none"
141
+ viewBox="0 0 24 24"
142
+ >
143
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
144
+ <path
145
+ class="opacity-75"
146
+ fill="currentColor"
147
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
148
+ ></path>
149
+ </svg>
150
+
151
+ <!-- Upload Text -->
152
+ <span v-if="!isUploading" class="text-xs font-medium text-gray-700">Click to upload image</span>
153
+ <span v-else class="text-xs font-medium text-blue-600">Uploading...</span>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- Read-only display -->
158
+ <div v-else-if="imageUrl" class="flex items-center gap-2">
159
+ <img :src="imageUrl" alt="Image" class="size-16 rounded border border-gray-300 object-cover" />
160
+ <span class="text-xs text-gray-500">Image uploaded</span>
161
+ </div>
162
+ <div v-else class="rounded border border-gray-200 bg-gray-50 p-2 text-center text-xs text-gray-400">No image</div>
163
+
164
+ <!-- Error Message -->
165
+ <div v-if="errorMessage" class="mt-1.5 rounded bg-red-50 px-2 py-1 text-xs text-red-600">
166
+ {{ errorMessage }}
167
+ </div>
168
+ </div>
169
+ </template>
170
+
171
+ <script setup lang="ts">
172
+ import { ref, watch } from "vue";
173
+
174
+ interface Props {
175
+ modelValue?: string | null;
176
+ editable?: boolean;
177
+ uploadUrl?: string; // Optional: custom upload endpoint URL
178
+ maxSizeMB?: number; // Maximum file size in MB
179
+ acceptedTypes?: string[]; // Accepted MIME types
180
+ }
181
+
182
+ const props = withDefaults(defineProps<Props>(), {
183
+ modelValue: null,
184
+ editable: true,
185
+ uploadUrl: undefined,
186
+ maxSizeMB: 10,
187
+ acceptedTypes: () => ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"],
188
+ });
189
+
190
+ const emit = defineEmits<{
191
+ "update:modelValue": [value: string | null];
192
+ }>();
193
+
194
+ const fileInputRef = ref<HTMLInputElement | null>(null);
195
+ const imageUrl = ref<string | null>(props.modelValue || null);
196
+ const isUploading = ref(false);
197
+ const isDragging = ref(false);
198
+ const errorMessage = ref<string | null>(null);
199
+ const selectedFile = ref<File | null>(null);
200
+
201
+ // Watch for external changes to modelValue
202
+ watch(
203
+ () => props.modelValue,
204
+ (newValue) => {
205
+ imageUrl.value = newValue || null;
206
+ }
207
+ );
208
+
209
+ // Watch for internal changes and emit updates
210
+ watch(imageUrl, (newValue) => {
211
+ emit("update:modelValue", newValue);
212
+ });
213
+
214
+ function triggerFileInput() {
215
+ if (!props.editable || isUploading.value) return;
216
+ fileInputRef.value?.click();
217
+ }
218
+
219
+ function handleFileSelect(event: Event) {
220
+ const target = event.target as HTMLInputElement;
221
+ const file = target.files?.[0];
222
+ if (file) {
223
+ processFile(file);
224
+ }
225
+ }
226
+
227
+ function handleDragOver(event: DragEvent) {
228
+ if (!props.editable || isUploading.value) return;
229
+ event.preventDefault();
230
+ isDragging.value = true;
231
+ }
232
+
233
+ function handleDragLeave() {
234
+ isDragging.value = false;
235
+ }
236
+
237
+ function handleDrop(event: DragEvent) {
238
+ if (!props.editable || isUploading.value) return;
239
+ event.preventDefault();
240
+ isDragging.value = false;
241
+
242
+ const file = event.dataTransfer?.files[0];
243
+ if (file) {
244
+ processFile(file);
245
+ }
246
+ }
247
+
248
+ function validateFile(file: File): boolean {
249
+ errorMessage.value = null;
250
+
251
+ // Check file type
252
+ if (!props.acceptedTypes.includes(file.type)) {
253
+ errorMessage.value = `File type not supported. Accepted types: ${props.acceptedTypes.join(", ")}`;
254
+ return false;
255
+ }
256
+
257
+ // Check file size
258
+ const fileSizeMB = file.size / (1024 * 1024);
259
+ if (fileSizeMB > props.maxSizeMB) {
260
+ errorMessage.value = `File size exceeds ${props.maxSizeMB}MB limit`;
261
+ return false;
262
+ }
263
+
264
+ return true;
265
+ }
266
+
267
+ async function processFile(file: File) {
268
+ if (!validateFile(file)) {
269
+ return;
270
+ }
271
+
272
+ selectedFile.value = file;
273
+ isUploading.value = true;
274
+ errorMessage.value = null;
275
+
276
+ try {
277
+ // Create preview URL immediately
278
+ const previewUrl = URL.createObjectURL(file);
279
+ imageUrl.value = previewUrl;
280
+
281
+ // Upload the file
282
+ const uploadedUrl = await uploadFile(file);
283
+
284
+ // Replace preview URL with uploaded URL
285
+ if (uploadedUrl) {
286
+ // Clean up preview URL
287
+ URL.revokeObjectURL(previewUrl);
288
+ imageUrl.value = uploadedUrl;
289
+ } else {
290
+ // If upload fails, keep preview URL but show error
291
+ errorMessage.value = "Upload failed. Please try again.";
292
+ }
293
+ } catch (error) {
294
+ console.error("File upload error:", error);
295
+ errorMessage.value = error instanceof Error ? error.message : "Upload failed. Please try again.";
296
+ // Keep preview URL on error
297
+ } finally {
298
+ isUploading.value = false;
299
+ // Reset file input
300
+ if (fileInputRef.value) {
301
+ fileInputRef.value.value = "";
302
+ }
303
+ }
304
+ }
305
+
306
+ async function uploadFile(file: File): Promise<string | null> {
307
+ // If a custom upload URL is provided, use it
308
+ if (props.uploadUrl) {
309
+ return uploadToCustomEndpoint(file, props.uploadUrl);
310
+ }
311
+
312
+ // Default implementation: Create a data URL (for basic use cases)
313
+ // In production, you should replace this with actual API call
314
+ return new Promise((resolve) => {
315
+ const reader = new FileReader();
316
+ reader.onload = (e) => {
317
+ const result = e.target?.result as string;
318
+ resolve(result);
319
+ };
320
+ reader.onerror = () => {
321
+ resolve(null);
322
+ };
323
+ reader.readAsDataURL(file);
324
+ });
325
+ }
326
+
327
+ async function uploadToCustomEndpoint(file: File, endpoint: string): Promise<string | null> {
328
+ try {
329
+ const formData = new FormData();
330
+ formData.append("file", file);
331
+
332
+ const response = await fetch(endpoint, {
333
+ method: "POST",
334
+ body: formData,
335
+ });
336
+
337
+ if (!response.ok) {
338
+ throw new Error(`Upload failed: ${response.statusText}`);
339
+ }
340
+
341
+ const data = await response.json();
342
+ // Expect response to have a 'url' property
343
+ return data.url || data.imageUrl || data.path || null;
344
+ } catch (error) {
345
+ console.error("Custom upload error:", error);
346
+ throw error;
347
+ }
348
+ }
349
+
350
+ function removeImage() {
351
+ if (!props.editable || isUploading.value) return;
352
+
353
+ // Clean up object URL if it's a preview
354
+ if (imageUrl.value && imageUrl.value.startsWith("blob:")) {
355
+ URL.revokeObjectURL(imageUrl.value);
356
+ }
357
+
358
+ imageUrl.value = null;
359
+ selectedFile.value = null;
360
+ errorMessage.value = null;
361
+
362
+ // Reset file input
363
+ if (fileInputRef.value) {
364
+ fileInputRef.value.value = "";
365
+ }
366
+ }
367
+ </script>
368
+
369
+ <style scoped>
370
+ .image-upload-field {
371
+ width: 100%;
372
+ }
373
+ </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div id="page-builder-sidebar" class="page-builder-sidebar">
2
+ <div class="page-builder-sidebar">
3
3
  <!-- Toolbar -->
4
4
  <PageBuilderToolbar
5
5
  v-model:editorViewport="editorViewport"
@@ -256,9 +256,5 @@ $toolbar-height: 0px;
256
256
  .page-builder-sidebar {
257
257
  min-width: 300px;
258
258
  background: #fff;
259
-
260
- // position: sticky;
261
- // top: 0;
262
- // z-index: 12;
263
259
  }
264
260
  </style>