@type32/yaml-editor-form 0.1.2
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 +1092 -0
- package/dist/module.d.mts +8 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +25 -0
- package/dist/runtime/assets/css/main.css +1 -0
- package/dist/runtime/components/YamlCollapsible.d.vue.ts +47 -0
- package/dist/runtime/components/YamlCollapsible.vue +43 -0
- package/dist/runtime/components/YamlCollapsible.vue.d.ts +47 -0
- package/dist/runtime/components/YamlFieldInput.d.vue.ts +54 -0
- package/dist/runtime/components/YamlFieldInput.vue +158 -0
- package/dist/runtime/components/YamlFieldInput.vue.d.ts +54 -0
- package/dist/runtime/components/YamlFormEditor.d.vue.ts +66 -0
- package/dist/runtime/components/YamlFormEditor.vue +70 -0
- package/dist/runtime/components/YamlFormEditor.vue.d.ts +66 -0
- package/dist/runtime/components/YamlFormField.d.vue.ts +56 -0
- package/dist/runtime/components/YamlFormField.vue +492 -0
- package/dist/runtime/components/YamlFormField.vue.d.ts +56 -0
- package/dist/runtime/composables/useYamlFieldTypes.d.ts +13 -0
- package/dist/runtime/composables/useYamlFieldTypes.js +137 -0
- package/dist/runtime/composables/useYamlFormData.d.ts +24 -0
- package/dist/runtime/composables/useYamlFormData.js +122 -0
- package/dist/runtime/types/types.d.ts +30 -0
- package/dist/types.d.mts +3 -0
- package/package.json +76 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { YamlFieldType } from "../types/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* YAML Form Field - Recursive Component
|
|
4
|
+
*
|
|
5
|
+
* Schema-driven field renderer with support for custom components.
|
|
6
|
+
* Uses centralized field type registry for extensibility.
|
|
7
|
+
*
|
|
8
|
+
* To add custom field types:
|
|
9
|
+
* 1. Define your field type in the fieldTypes prop
|
|
10
|
+
* 2. Provide a custom component via slot: #field-{component}
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* <YamlFormField :fieldTypes="[{ type: 'image', label: 'Image', icon: 'i-lucide-image', defaultValue: '', component: 'image' }]">
|
|
14
|
+
* <template #field-image="{ modelValue, readonly }">
|
|
15
|
+
* <MyImagePicker v-model="modelValue" :disabled="readonly" />
|
|
16
|
+
* </template>
|
|
17
|
+
* </YamlFormField>
|
|
18
|
+
*/
|
|
19
|
+
type YamlValue = string | number | boolean | null | Date | YamlValue[] | {
|
|
20
|
+
[key: string]: YamlValue;
|
|
21
|
+
};
|
|
22
|
+
type __VLS_Props = {
|
|
23
|
+
fieldKey: string;
|
|
24
|
+
readonly?: boolean;
|
|
25
|
+
depth?: number;
|
|
26
|
+
/** Custom field type definitions (merged with defaults) */
|
|
27
|
+
fieldTypes?: YamlFieldType[];
|
|
28
|
+
};
|
|
29
|
+
type __VLS_ModelProps = {
|
|
30
|
+
modelValue: YamlValue;
|
|
31
|
+
};
|
|
32
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
33
|
+
declare var __VLS_147: string, __VLS_148: any;
|
|
34
|
+
type __VLS_Slots = {} & {
|
|
35
|
+
[K in NonNullable<typeof __VLS_147>]?: (props: typeof __VLS_148) => any;
|
|
36
|
+
};
|
|
37
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
38
|
+
remove: () => any;
|
|
39
|
+
"update:modelValue": (value: YamlValue) => any;
|
|
40
|
+
"update:fieldKey": (newKey: string) => any;
|
|
41
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
42
|
+
onRemove?: (() => any) | undefined;
|
|
43
|
+
"onUpdate:modelValue"?: ((value: YamlValue) => any) | undefined;
|
|
44
|
+
"onUpdate:fieldKey"?: ((newKey: string) => any) | undefined;
|
|
45
|
+
}>, {
|
|
46
|
+
readonly: boolean;
|
|
47
|
+
depth: number;
|
|
48
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
49
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
50
|
+
declare const _default: typeof __VLS_export;
|
|
51
|
+
export default _default;
|
|
52
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
53
|
+
new (): {
|
|
54
|
+
$slots: S;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useYamlFieldTypes } from "../composables/useYamlFieldTypes";
|
|
3
|
+
import { ref, watch, computed } from "vue";
|
|
4
|
+
const modelValue = defineModel({ type: [String, Number, Boolean, null, Date, Array, Object], ...{ required: true } });
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
fieldKey: { type: String, required: true },
|
|
7
|
+
readonly: { type: Boolean, required: false, default: false },
|
|
8
|
+
depth: { type: Number, required: false, default: 0 },
|
|
9
|
+
fieldTypes: { type: Array, required: false }
|
|
10
|
+
});
|
|
11
|
+
const emit = defineEmits(["remove", "update:fieldKey"]);
|
|
12
|
+
const {
|
|
13
|
+
fieldTypes,
|
|
14
|
+
getFieldType,
|
|
15
|
+
detectFieldType,
|
|
16
|
+
getDefaultValue,
|
|
17
|
+
getIcon,
|
|
18
|
+
getTypeMenuItems
|
|
19
|
+
} = useYamlFieldTypes(props.fieldTypes);
|
|
20
|
+
function isDateObject(val) {
|
|
21
|
+
return val instanceof Date && !isNaN(val.getTime());
|
|
22
|
+
}
|
|
23
|
+
function isDateString(val) {
|
|
24
|
+
if (typeof val !== "string") return false;
|
|
25
|
+
const isoDateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})?)?$/;
|
|
26
|
+
if (!isoDateRegex.test(val)) return false;
|
|
27
|
+
const date = new Date(val);
|
|
28
|
+
return !isNaN(date.getTime());
|
|
29
|
+
}
|
|
30
|
+
function isDateTimeString(val) {
|
|
31
|
+
if (typeof val !== "string") return false;
|
|
32
|
+
return /T\d{2}:\d{2}:\d{2}/.test(val) && isDateString(val);
|
|
33
|
+
}
|
|
34
|
+
function isStringArray(val) {
|
|
35
|
+
if (!Array.isArray(val)) return false;
|
|
36
|
+
if (val.length === 0) return false;
|
|
37
|
+
return val.every((item) => typeof item === "string");
|
|
38
|
+
}
|
|
39
|
+
function isPrimitiveArray(val) {
|
|
40
|
+
if (!Array.isArray(val)) return false;
|
|
41
|
+
if (val.length === 0) return false;
|
|
42
|
+
return val.every(
|
|
43
|
+
(item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const valueType = computed(() => {
|
|
47
|
+
const detectedType = detectFieldType(modelValue.value);
|
|
48
|
+
return detectedType.type;
|
|
49
|
+
});
|
|
50
|
+
const isEditingKey = ref(false);
|
|
51
|
+
const editingKeyValue = ref(props.fieldKey);
|
|
52
|
+
watch(() => props.fieldKey, (newKey) => {
|
|
53
|
+
editingKeyValue.value = newKey;
|
|
54
|
+
});
|
|
55
|
+
function startEditingKey() {
|
|
56
|
+
if (props.readonly) return;
|
|
57
|
+
editingKeyValue.value = props.fieldKey;
|
|
58
|
+
isEditingKey.value = true;
|
|
59
|
+
}
|
|
60
|
+
function saveKey() {
|
|
61
|
+
const trimmedValue = editingKeyValue.value?.trim();
|
|
62
|
+
if (trimmedValue && trimmedValue !== props.fieldKey) {
|
|
63
|
+
emit("update:fieldKey", trimmedValue);
|
|
64
|
+
}
|
|
65
|
+
isEditingKey.value = false;
|
|
66
|
+
}
|
|
67
|
+
function isValidConversion(fromType, toType) {
|
|
68
|
+
if (fromType === toType) return true;
|
|
69
|
+
if (fromType === "null") return true;
|
|
70
|
+
const conversionRules = {
|
|
71
|
+
// Primitives can convert to other primitives and arrays
|
|
72
|
+
"string": ["number", "boolean", "date", "datetime", "string-array", "null"],
|
|
73
|
+
"number": ["string", "boolean", "null"],
|
|
74
|
+
"boolean": ["string", "number", "null"],
|
|
75
|
+
// Dates can convert to strings and each other
|
|
76
|
+
"date": ["string", "datetime", "null"],
|
|
77
|
+
"datetime": ["string", "date", "null"],
|
|
78
|
+
// String arrays can convert to regular arrays and back to string
|
|
79
|
+
"string-array": ["array", "string", "null"],
|
|
80
|
+
// Arrays can only convert to string-array or null (converting to primitives is unsafe)
|
|
81
|
+
"array": ["string-array", "null"],
|
|
82
|
+
// Objects can only convert to null (converting to primitives is useless)
|
|
83
|
+
"object": ["null"]
|
|
84
|
+
};
|
|
85
|
+
const allowedConversions = conversionRules[fromType] || [];
|
|
86
|
+
return allowedConversions.includes(toType);
|
|
87
|
+
}
|
|
88
|
+
function convertType(newType) {
|
|
89
|
+
const currentType = valueType.value;
|
|
90
|
+
if (!isValidConversion(currentType, newType)) {
|
|
91
|
+
console.warn(`Invalid conversion from ${currentType} to ${newType}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const isCurrentlyArray = Array.isArray(modelValue.value);
|
|
95
|
+
const isConvertingToNonArray = !["array", "string-array"].includes(newType);
|
|
96
|
+
if (newType === "string-array") {
|
|
97
|
+
if (Array.isArray(modelValue.value)) {
|
|
98
|
+
modelValue.value = modelValue.value.map((item) => String(item));
|
|
99
|
+
} else {
|
|
100
|
+
modelValue.value = [String(modelValue.value || "")];
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (currentType === "date" && newType === "datetime" || currentType === "datetime" && newType === "date") {
|
|
105
|
+
const currentValue = modelValue.value;
|
|
106
|
+
if (newType === "datetime") {
|
|
107
|
+
if (typeof currentValue === "string") {
|
|
108
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(currentValue)) {
|
|
109
|
+
modelValue.value = `${currentValue}T00:00:00`;
|
|
110
|
+
} else {
|
|
111
|
+
modelValue.value = currentValue;
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const date = isDateObject(currentValue) ? currentValue : /* @__PURE__ */ new Date();
|
|
115
|
+
modelValue.value = date.toISOString();
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
if (typeof currentValue === "string") {
|
|
119
|
+
modelValue.value = currentValue.split("T")[0];
|
|
120
|
+
} else {
|
|
121
|
+
const date = isDateObject(currentValue) ? currentValue : /* @__PURE__ */ new Date();
|
|
122
|
+
modelValue.value = date.toISOString().split("T")[0];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (isCurrentlyArray && isConvertingToNonArray) {
|
|
128
|
+
const arr = modelValue.value;
|
|
129
|
+
const firstItem = arr.length > 0 && arr[0] !== void 0 ? arr[0] : null;
|
|
130
|
+
if (firstItem !== null) {
|
|
131
|
+
switch (newType) {
|
|
132
|
+
case "string":
|
|
133
|
+
modelValue.value = isDateObject(firstItem) ? firstItem.toISOString() : String(firstItem);
|
|
134
|
+
return;
|
|
135
|
+
case "number":
|
|
136
|
+
modelValue.value = Number(firstItem) || 0;
|
|
137
|
+
return;
|
|
138
|
+
case "boolean":
|
|
139
|
+
modelValue.value = Boolean(firstItem);
|
|
140
|
+
return;
|
|
141
|
+
case "date":
|
|
142
|
+
const dateVal = typeof firstItem === "string" ? new Date(firstItem) : /* @__PURE__ */ new Date();
|
|
143
|
+
modelValue.value = dateVal.toISOString().split("T")[0];
|
|
144
|
+
return;
|
|
145
|
+
case "datetime":
|
|
146
|
+
const dateTimeVal = typeof firstItem === "string" ? new Date(firstItem) : /* @__PURE__ */ new Date();
|
|
147
|
+
modelValue.value = dateTimeVal.toISOString();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
modelValue.value = getDefaultValue(newType);
|
|
153
|
+
}
|
|
154
|
+
function addArrayItem(itemType) {
|
|
155
|
+
if (!Array.isArray(modelValue.value)) return;
|
|
156
|
+
if (itemType) {
|
|
157
|
+
modelValue.value.push(getDefaultValue(itemType));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (modelValue.value.length === 0) {
|
|
161
|
+
modelValue.value.push({});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const firstItem = modelValue.value[0];
|
|
165
|
+
const detectedType = detectFieldType(firstItem);
|
|
166
|
+
modelValue.value.push(getDefaultValue(detectedType.type));
|
|
167
|
+
}
|
|
168
|
+
function removeArrayItem(index) {
|
|
169
|
+
if (!Array.isArray(modelValue.value)) return;
|
|
170
|
+
modelValue.value.splice(index, 1);
|
|
171
|
+
}
|
|
172
|
+
function addArrayItemFromTemplate() {
|
|
173
|
+
if (!Array.isArray(modelValue.value)) return;
|
|
174
|
+
let templateObject = null;
|
|
175
|
+
let maxFields = 0;
|
|
176
|
+
for (const item of modelValue.value) {
|
|
177
|
+
if (typeof item === "object" && !Array.isArray(item) && item !== null) {
|
|
178
|
+
const fieldCount = Object.keys(item).length;
|
|
179
|
+
if (fieldCount > maxFields) {
|
|
180
|
+
maxFields = fieldCount;
|
|
181
|
+
templateObject = item;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (templateObject) {
|
|
186
|
+
const newObject = {};
|
|
187
|
+
for (const key in templateObject) {
|
|
188
|
+
const value = templateObject[key];
|
|
189
|
+
const detectedType = detectFieldType(value);
|
|
190
|
+
newObject[key] = getDefaultValue(detectedType.type);
|
|
191
|
+
}
|
|
192
|
+
modelValue.value.push(newObject);
|
|
193
|
+
} else {
|
|
194
|
+
modelValue.value.push({});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const hasObjectTemplate = computed(() => {
|
|
198
|
+
if (!Array.isArray(modelValue.value) || modelValue.value.length === 0) return false;
|
|
199
|
+
return modelValue.value.some(
|
|
200
|
+
(item) => typeof item === "object" && !Array.isArray(item) && item !== null && Object.keys(item).length > 0
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
function addObjectField(fieldType = "string") {
|
|
204
|
+
if (typeof modelValue.value !== "object" || Array.isArray(modelValue.value) || !modelValue.value || isDateObject(modelValue.value)) return;
|
|
205
|
+
const obj = modelValue.value;
|
|
206
|
+
const newKey = `field_${Object.keys(obj).length + 1}`;
|
|
207
|
+
obj[newKey] = getDefaultValue(fieldType);
|
|
208
|
+
}
|
|
209
|
+
function removeObjectField(key) {
|
|
210
|
+
if (typeof modelValue.value !== "object" || Array.isArray(modelValue.value) || !modelValue.value || isDateObject(modelValue.value)) return;
|
|
211
|
+
const obj = modelValue.value;
|
|
212
|
+
delete obj[key];
|
|
213
|
+
}
|
|
214
|
+
const isOpen = ref(true);
|
|
215
|
+
const isArrayItem = computed(() => {
|
|
216
|
+
return /^\[\d+\]$/.test(props.fieldKey);
|
|
217
|
+
});
|
|
218
|
+
const itemCount = computed(() => {
|
|
219
|
+
if (valueType.value === "array" && Array.isArray(modelValue.value)) {
|
|
220
|
+
return modelValue.value.length;
|
|
221
|
+
} else if (valueType.value === "object" && typeof modelValue.value === "object" && !Array.isArray(modelValue.value) && modelValue.value !== null) {
|
|
222
|
+
return Object.keys(modelValue.value).length;
|
|
223
|
+
}
|
|
224
|
+
return 0;
|
|
225
|
+
});
|
|
226
|
+
const indentClass = computed(() => {
|
|
227
|
+
return props.depth > 0 ? "pl-2" : "";
|
|
228
|
+
});
|
|
229
|
+
const typeOptions = computed(() => {
|
|
230
|
+
const currentType = valueType.value;
|
|
231
|
+
return fieldTypes.value.filter((ft) => isValidConversion(currentType, ft.type)).map((ft) => ({
|
|
232
|
+
label: ft.label,
|
|
233
|
+
icon: ft.icon,
|
|
234
|
+
onSelect: () => convertType(ft.type)
|
|
235
|
+
}));
|
|
236
|
+
});
|
|
237
|
+
const selectedType = computed(() => {
|
|
238
|
+
return {
|
|
239
|
+
icon: getIcon(valueType.value)
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
const addFieldOptions = computed(() => {
|
|
243
|
+
return getTypeMenuItems((type) => addObjectField(type));
|
|
244
|
+
});
|
|
245
|
+
const addArrayItemOptions = computed(() => {
|
|
246
|
+
return getTypeMenuItems((type) => addArrayItem(type));
|
|
247
|
+
});
|
|
248
|
+
</script>
|
|
249
|
+
|
|
250
|
+
<template>
|
|
251
|
+
<div :class="indentClass" class="space-y-1">
|
|
252
|
+
<!-- For Objects and Arrays: Use Collapsible -->
|
|
253
|
+
<template v-if="valueType === 'object' || valueType === 'array'">
|
|
254
|
+
<!-- Edit mode for field key -->
|
|
255
|
+
<UInput
|
|
256
|
+
v-if="isEditingKey"
|
|
257
|
+
v-model="editingKeyValue"
|
|
258
|
+
size="xs"
|
|
259
|
+
autofocus
|
|
260
|
+
class="mb-1"
|
|
261
|
+
@blur="saveKey"
|
|
262
|
+
@keydown.enter="saveKey"
|
|
263
|
+
@keydown.esc="isEditingKey = false"
|
|
264
|
+
/>
|
|
265
|
+
|
|
266
|
+
<!-- Collapsible for non-edit mode -->
|
|
267
|
+
<YamlCollapsible v-else v-model:open="isOpen" :default-open="true" :label="fieldKey">
|
|
268
|
+
<template #badge>
|
|
269
|
+
<UBadge size="xs" variant="soft" color="neutral">{{ itemCount }}</UBadge>
|
|
270
|
+
</template>
|
|
271
|
+
|
|
272
|
+
<template #actions>
|
|
273
|
+
<div class="flex items-center gap-1">
|
|
274
|
+
<!-- Edit key button (hidden for array items) -->
|
|
275
|
+
<UButton
|
|
276
|
+
v-if="!readonly && !isEditingKey && !isArrayItem"
|
|
277
|
+
icon="i-lucide-pencil"
|
|
278
|
+
variant="ghost"
|
|
279
|
+
size="xs"
|
|
280
|
+
color="neutral"
|
|
281
|
+
class="text-muted"
|
|
282
|
+
@click.stop="startEditingKey"
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
<!-- Type selector -->
|
|
286
|
+
<UDropdownMenu
|
|
287
|
+
:items="[typeOptions]"
|
|
288
|
+
:disabled="readonly"
|
|
289
|
+
size="xs"
|
|
290
|
+
>
|
|
291
|
+
<UButton
|
|
292
|
+
:icon="selectedType?.icon || 'i-heroicons-circle-question-mark'"
|
|
293
|
+
variant="soft"
|
|
294
|
+
size="xs"
|
|
295
|
+
:disabled="readonly"
|
|
296
|
+
/>
|
|
297
|
+
</UDropdownMenu>
|
|
298
|
+
|
|
299
|
+
<!-- Remove button -->
|
|
300
|
+
<UButton
|
|
301
|
+
v-if="!readonly"
|
|
302
|
+
icon="i-lucide-trash"
|
|
303
|
+
variant="ghost"
|
|
304
|
+
size="xs"
|
|
305
|
+
color="error"
|
|
306
|
+
@click.stop="emit('remove')"
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
</template>
|
|
310
|
+
|
|
311
|
+
<!-- Content inside collapsible - only arrays and objects -->
|
|
312
|
+
<!-- Array (Complex items - objects/arrays) -->
|
|
313
|
+
<div v-if="valueType === 'array'" class="space-y-2">
|
|
314
|
+
<div
|
|
315
|
+
v-if="Array.isArray(modelValue) && modelValue.length === 0"
|
|
316
|
+
class="text-center py-4 text-sm text-muted border border-dashed border-default rounded-lg"
|
|
317
|
+
>
|
|
318
|
+
<UIcon name="i-lucide-brackets" class="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
319
|
+
<p>Empty array</p>
|
|
320
|
+
<p v-if="!readonly" class="text-xs mt-1">Click "Add Item" below</p>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<template v-else-if="Array.isArray(modelValue)">
|
|
324
|
+
<div
|
|
325
|
+
v-for="(item, index) in modelValue"
|
|
326
|
+
:key="index"
|
|
327
|
+
class="flex items-start gap-2"
|
|
328
|
+
>
|
|
329
|
+
<div v-if="item !== void 0" class="flex-1">
|
|
330
|
+
<YamlFormField
|
|
331
|
+
:model-value="item"
|
|
332
|
+
:field-key="`[${index}]`"
|
|
333
|
+
:readonly="readonly"
|
|
334
|
+
:depth="depth + 1"
|
|
335
|
+
:field-types="fieldTypes"
|
|
336
|
+
@update:model-value="(val) => {
|
|
337
|
+
if (Array.isArray(modelValue)) modelValue[index] = val;
|
|
338
|
+
}"
|
|
339
|
+
@remove="removeArrayItem(index)"
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</template>
|
|
344
|
+
|
|
345
|
+
<div class="flex items-center gap-2">
|
|
346
|
+
<UDropdownMenu
|
|
347
|
+
v-if="!readonly"
|
|
348
|
+
:items="[addArrayItemOptions]"
|
|
349
|
+
size="xs"
|
|
350
|
+
>
|
|
351
|
+
<UButton
|
|
352
|
+
icon="i-lucide-plus"
|
|
353
|
+
label="Add Item"
|
|
354
|
+
variant="link"
|
|
355
|
+
size="xs"
|
|
356
|
+
color="neutral"
|
|
357
|
+
/>
|
|
358
|
+
</UDropdownMenu>
|
|
359
|
+
|
|
360
|
+
<!-- Add Item From Template button (only shown when array has objects) -->
|
|
361
|
+
<UButton
|
|
362
|
+
v-if="!readonly && hasObjectTemplate"
|
|
363
|
+
icon="i-lucide-copy-plus"
|
|
364
|
+
label="From Template"
|
|
365
|
+
variant="link"
|
|
366
|
+
size="xs"
|
|
367
|
+
color="neutral"
|
|
368
|
+
@click="addArrayItemFromTemplate"
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<!-- Object -->
|
|
374
|
+
<div v-else-if="valueType === 'object'" class="space-y-2">
|
|
375
|
+
<div
|
|
376
|
+
v-if="typeof modelValue === 'object' && !Array.isArray(modelValue) && modelValue !== null && !isDateObject(modelValue) && Object.keys(modelValue).length === 0"
|
|
377
|
+
class="text-center py-4 text-sm text-muted border border-dashed border-default rounded-lg"
|
|
378
|
+
>
|
|
379
|
+
<UIcon name="i-lucide-box-transparent" class="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
380
|
+
<p>Empty object</p>
|
|
381
|
+
<p v-if="!readonly" class="text-xs mt-1">Click "Add Field" below</p>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<template v-else-if="typeof modelValue === 'object' && !Array.isArray(modelValue) && modelValue !== null && !isDateObject(modelValue)">
|
|
385
|
+
<template v-for="(value, key) in modelValue" :key="String(key)">
|
|
386
|
+
<YamlFormField
|
|
387
|
+
v-if="value !== void 0"
|
|
388
|
+
:model-value="value"
|
|
389
|
+
:field-key="String(key)"
|
|
390
|
+
:readonly="readonly"
|
|
391
|
+
:depth="depth + 1"
|
|
392
|
+
:field-types="fieldTypes"
|
|
393
|
+
@update:model-value="(val) => {
|
|
394
|
+
if (typeof modelValue === 'object' && !Array.isArray(modelValue) && modelValue !== null && !isDateObject(modelValue)) {
|
|
395
|
+
modelValue[key] = val;
|
|
396
|
+
}
|
|
397
|
+
}"
|
|
398
|
+
@remove="removeObjectField(String(key))"
|
|
399
|
+
@update:field-key="(newKey) => {
|
|
400
|
+
if (newKey !== key && typeof modelValue === 'object' && !Array.isArray(modelValue) && modelValue !== null && !isDateObject(modelValue) && value !== void 0) {
|
|
401
|
+
const obj = modelValue;
|
|
402
|
+
obj[newKey] = value;
|
|
403
|
+
delete obj[key];
|
|
404
|
+
}
|
|
405
|
+
}"
|
|
406
|
+
/>
|
|
407
|
+
</template>
|
|
408
|
+
</template>
|
|
409
|
+
|
|
410
|
+
<UDropdownMenu
|
|
411
|
+
v-if="!readonly"
|
|
412
|
+
:items="[addFieldOptions]"
|
|
413
|
+
size="xs"
|
|
414
|
+
>
|
|
415
|
+
<UButton
|
|
416
|
+
icon="i-lucide-plus"
|
|
417
|
+
label="Add Field"
|
|
418
|
+
variant="link"
|
|
419
|
+
size="xs"
|
|
420
|
+
color="neutral"
|
|
421
|
+
/>
|
|
422
|
+
</UDropdownMenu>
|
|
423
|
+
</div>
|
|
424
|
+
</YamlCollapsible>
|
|
425
|
+
</template>
|
|
426
|
+
|
|
427
|
+
<!-- For Simple Types: Regular Layout -->
|
|
428
|
+
<template v-else>
|
|
429
|
+
<div class="flex items-center gap-1">
|
|
430
|
+
<!-- Field Key (editable) -->
|
|
431
|
+
<div class="flex-1 min-w-0">
|
|
432
|
+
<UInput
|
|
433
|
+
v-if="isEditingKey"
|
|
434
|
+
v-model="editingKeyValue"
|
|
435
|
+
size="xs"
|
|
436
|
+
autofocus
|
|
437
|
+
@blur="saveKey"
|
|
438
|
+
@keydown.enter="saveKey"
|
|
439
|
+
@keydown.esc="isEditingKey = false"
|
|
440
|
+
/>
|
|
441
|
+
<UButton
|
|
442
|
+
v-else
|
|
443
|
+
class="text-xs font-medium text-left justify-start px-0 py-0 truncate"
|
|
444
|
+
:label="fieldKey"
|
|
445
|
+
block
|
|
446
|
+
variant="link"
|
|
447
|
+
color="neutral"
|
|
448
|
+
:disabled="readonly || isArrayItem"
|
|
449
|
+
@click="startEditingKey"
|
|
450
|
+
/>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
<!-- Type selector -->
|
|
454
|
+
<UDropdownMenu
|
|
455
|
+
:items="[typeOptions]"
|
|
456
|
+
:disabled="readonly"
|
|
457
|
+
size="xs"
|
|
458
|
+
>
|
|
459
|
+
<UButton
|
|
460
|
+
:icon="selectedType?.icon || 'i-lucide-circle-question-mark'"
|
|
461
|
+
variant="soft"
|
|
462
|
+
size="xs"
|
|
463
|
+
:disabled="readonly"
|
|
464
|
+
/>
|
|
465
|
+
</UDropdownMenu>
|
|
466
|
+
|
|
467
|
+
<!-- Remove button -->
|
|
468
|
+
<UButton
|
|
469
|
+
v-if="!readonly"
|
|
470
|
+
icon="i-lucide-trash"
|
|
471
|
+
variant="ghost"
|
|
472
|
+
size="xs"
|
|
473
|
+
color="error"
|
|
474
|
+
@click="emit('remove')"
|
|
475
|
+
/>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<!-- Value Input for simple types -->
|
|
479
|
+
<YamlFieldInput
|
|
480
|
+
v-model="modelValue"
|
|
481
|
+
:value-type="valueType"
|
|
482
|
+
:readonly="readonly"
|
|
483
|
+
:field-type="getFieldType(valueType)"
|
|
484
|
+
>
|
|
485
|
+
<!-- Forward all custom field component slots -->
|
|
486
|
+
<template v-for="(_, name) in $slots" #[name]="slotProps">
|
|
487
|
+
<slot :name="name" v-bind="slotProps" />
|
|
488
|
+
</template>
|
|
489
|
+
</YamlFieldInput>
|
|
490
|
+
</template>
|
|
491
|
+
</div>
|
|
492
|
+
</template>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { YamlFieldType } from "../types/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* YAML Form Field - Recursive Component
|
|
4
|
+
*
|
|
5
|
+
* Schema-driven field renderer with support for custom components.
|
|
6
|
+
* Uses centralized field type registry for extensibility.
|
|
7
|
+
*
|
|
8
|
+
* To add custom field types:
|
|
9
|
+
* 1. Define your field type in the fieldTypes prop
|
|
10
|
+
* 2. Provide a custom component via slot: #field-{component}
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* <YamlFormField :fieldTypes="[{ type: 'image', label: 'Image', icon: 'i-lucide-image', defaultValue: '', component: 'image' }]">
|
|
14
|
+
* <template #field-image="{ modelValue, readonly }">
|
|
15
|
+
* <MyImagePicker v-model="modelValue" :disabled="readonly" />
|
|
16
|
+
* </template>
|
|
17
|
+
* </YamlFormField>
|
|
18
|
+
*/
|
|
19
|
+
type YamlValue = string | number | boolean | null | Date | YamlValue[] | {
|
|
20
|
+
[key: string]: YamlValue;
|
|
21
|
+
};
|
|
22
|
+
type __VLS_Props = {
|
|
23
|
+
fieldKey: string;
|
|
24
|
+
readonly?: boolean;
|
|
25
|
+
depth?: number;
|
|
26
|
+
/** Custom field type definitions (merged with defaults) */
|
|
27
|
+
fieldTypes?: YamlFieldType[];
|
|
28
|
+
};
|
|
29
|
+
type __VLS_ModelProps = {
|
|
30
|
+
modelValue: YamlValue;
|
|
31
|
+
};
|
|
32
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
33
|
+
declare var __VLS_147: string, __VLS_148: any;
|
|
34
|
+
type __VLS_Slots = {} & {
|
|
35
|
+
[K in NonNullable<typeof __VLS_147>]?: (props: typeof __VLS_148) => any;
|
|
36
|
+
};
|
|
37
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
38
|
+
remove: () => any;
|
|
39
|
+
"update:modelValue": (value: YamlValue) => any;
|
|
40
|
+
"update:fieldKey": (newKey: string) => any;
|
|
41
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
42
|
+
onRemove?: (() => any) | undefined;
|
|
43
|
+
"onUpdate:modelValue"?: ((value: YamlValue) => any) | undefined;
|
|
44
|
+
"onUpdate:fieldKey"?: ((newKey: string) => any) | undefined;
|
|
45
|
+
}>, {
|
|
46
|
+
readonly: boolean;
|
|
47
|
+
depth: number;
|
|
48
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
49
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
50
|
+
declare const _default: typeof __VLS_export;
|
|
51
|
+
export default _default;
|
|
52
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
53
|
+
new (): {
|
|
54
|
+
$slots: S;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { YamlFieldType } from "../types/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Composable for managing YAML field types
|
|
4
|
+
*/
|
|
5
|
+
export declare function useYamlFieldTypes(customTypes?: YamlFieldType[]): {
|
|
6
|
+
fieldTypes: any;
|
|
7
|
+
getFieldType: (type: string) => YamlFieldType | undefined;
|
|
8
|
+
detectFieldType: (value: any) => YamlFieldType;
|
|
9
|
+
getDefaultValue: (type: string) => any;
|
|
10
|
+
getIcon: (type: string) => string;
|
|
11
|
+
getTypeMenuItems: (onSelect: (type: string) => void) => any;
|
|
12
|
+
DEFAULT_FIELD_TYPES: YamlFieldType[];
|
|
13
|
+
};
|