@dragonmastery/zinia-forms-core 0.3.1 → 0.3.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/dist/index.d.ts +90 -2
- package/dist/index.js +445 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -139,6 +139,87 @@ interface CheckboxFieldProps<FormType> {
|
|
|
139
139
|
variant?: string;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Option item for combobox fields - can be a string or label/value object
|
|
144
|
+
*/
|
|
145
|
+
type ComboboxOption = string | {
|
|
146
|
+
label: string;
|
|
147
|
+
value: string;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Props for the ComboboxField component
|
|
151
|
+
*/
|
|
152
|
+
interface ComboboxFieldProps<FormType, P extends Path<FormType> = Path<FormType>> {
|
|
153
|
+
/**
|
|
154
|
+
* Field name (path in the form state)
|
|
155
|
+
* Accepts both static paths and dynamic paths for use in array fields
|
|
156
|
+
*/
|
|
157
|
+
name: FlexiblePath<FormType>;
|
|
158
|
+
/**
|
|
159
|
+
* Field label
|
|
160
|
+
*/
|
|
161
|
+
label?: string;
|
|
162
|
+
/**
|
|
163
|
+
* Whether to hide the label
|
|
164
|
+
*/
|
|
165
|
+
hideLabel?: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Field description
|
|
168
|
+
*/
|
|
169
|
+
description?: string;
|
|
170
|
+
/**
|
|
171
|
+
* Whether the field is required
|
|
172
|
+
*/
|
|
173
|
+
required?: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* Placeholder text
|
|
176
|
+
*/
|
|
177
|
+
placeholder?: string;
|
|
178
|
+
/**
|
|
179
|
+
* Whether the field is disabled
|
|
180
|
+
*/
|
|
181
|
+
disabled?: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Whether the field is readonly
|
|
184
|
+
*/
|
|
185
|
+
readonly?: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* CSS classes to apply to the field
|
|
188
|
+
*/
|
|
189
|
+
class?: string | string[];
|
|
190
|
+
/**
|
|
191
|
+
* Field size
|
|
192
|
+
*/
|
|
193
|
+
size?: string;
|
|
194
|
+
/**
|
|
195
|
+
* Field variant
|
|
196
|
+
*/
|
|
197
|
+
variant?: string;
|
|
198
|
+
/**
|
|
199
|
+
* Combobox options (array of strings or label/value objects)
|
|
200
|
+
* If not provided, will use options from schema metadata
|
|
201
|
+
*
|
|
202
|
+
* For async loading, use form-level dataLoaders and pass options via extraData:
|
|
203
|
+
* ```ts
|
|
204
|
+
* useForm(schema, {
|
|
205
|
+
* dataLoaders: { products: loadProducts },
|
|
206
|
+
* });
|
|
207
|
+
* // Then: :options="form.extraData.products || []"
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
options?: ComboboxOption[];
|
|
211
|
+
/**
|
|
212
|
+
* Whether to allow creating new values that don't exist in the options
|
|
213
|
+
* Default: true
|
|
214
|
+
*/
|
|
215
|
+
allowCreate?: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Custom filter function for options
|
|
218
|
+
* Default: filters by case-insensitive substring match
|
|
219
|
+
*/
|
|
220
|
+
filterFn?: (query: string, option: ComboboxOption) => boolean;
|
|
221
|
+
}
|
|
222
|
+
|
|
142
223
|
interface CurrencyFieldProps<FormType> {
|
|
143
224
|
name: FlexiblePath<FormType>;
|
|
144
225
|
label?: string;
|
|
@@ -811,7 +892,7 @@ interface FieldMetadata {
|
|
|
811
892
|
/** The data type of the field */
|
|
812
893
|
type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'enum' | 'date' | 'union';
|
|
813
894
|
/** The input type for rendering the field */
|
|
814
|
-
inputType?: 'text' | 'email' | 'url' | 'tel' | 'number' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'password' | 'array' | 'currency' | 'date' | 'time' | 'datetime-local' | 'file' | 'range' | 'search';
|
|
895
|
+
inputType?: 'text' | 'email' | 'url' | 'tel' | 'number' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'password' | 'array' | 'currency' | 'date' | 'time' | 'datetime-local' | 'file' | 'range' | 'search' | 'combobox';
|
|
815
896
|
/** The display label for the field */
|
|
816
897
|
label?: string;
|
|
817
898
|
/** Placeholder text for the field */
|
|
@@ -870,7 +951,7 @@ interface FieldMetadata {
|
|
|
870
951
|
*/
|
|
871
952
|
interface SchemaFieldMetadata {
|
|
872
953
|
/** The input type for rendering the field */
|
|
873
|
-
inputType?: 'text' | 'email' | 'tel' | 'number' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'password' | 'currency' | 'date' | 'time' | 'datetime-local';
|
|
954
|
+
inputType?: 'text' | 'email' | 'tel' | 'number' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'password' | 'currency' | 'date' | 'time' | 'datetime-local' | 'combobox';
|
|
874
955
|
/** Placeholder text for the field */
|
|
875
956
|
placeholder?: string;
|
|
876
957
|
/** The display label for the field */
|
|
@@ -928,6 +1009,7 @@ interface StyleCreators {
|
|
|
928
1009
|
createSearchField: <FormType>() => FunctionalComponent<SearchFieldProps<FormType>, {}, any, {}>;
|
|
929
1010
|
createTimeField: <FormType>() => FunctionalComponent<TimeFieldProps<FormType>, {}, any, {}>;
|
|
930
1011
|
createDateTimeLocalField: <FormType>() => FunctionalComponent<DateTimeLocalFieldProps<FormType>, {}, any, {}>;
|
|
1012
|
+
createComboboxField: <FormType>() => FunctionalComponent<ComboboxFieldProps<FormType>, {}, any, {}>;
|
|
931
1013
|
createArrayField: <FormType, ItemType = any>() => FunctionalComponent<ArrayFieldProps<FormType, ItemType>, {}, {
|
|
932
1014
|
itemRenderer: (props: {
|
|
933
1015
|
item: ItemType;
|
|
@@ -1107,6 +1189,12 @@ declare function useForm<T extends z.ZodObject<any>, CalcType = (values: z.infer
|
|
|
1107
1189
|
resetField: (path: string) => void;
|
|
1108
1190
|
hasError: (path: string) => boolean;
|
|
1109
1191
|
getError: (path: string) => string;
|
|
1192
|
+
setFocus: (path: string, value: boolean) => void;
|
|
1193
|
+
isFocused: (path: string) => boolean;
|
|
1194
|
+
setDisplayText: (path: string, text: string) => void;
|
|
1195
|
+
getDisplayText: (path: string) => string;
|
|
1196
|
+
setSelectedIndex: (path: string, index: number) => void;
|
|
1197
|
+
getSelectedIndex: (path: string) => number;
|
|
1110
1198
|
};
|
|
1111
1199
|
ZiniaForm: vue.FunctionalComponent<FormProps<z.TypeOf<T>>, {}, any, {}>;
|
|
1112
1200
|
ZiniaSubmitButton: vue.FunctionalComponent<SubmitButtonProps<z.TypeOf<T>>, {}, any, {}>;
|
package/dist/index.js
CHANGED
|
@@ -151,6 +151,7 @@ function generateFieldComponents(schema, fieldsMetadata, styleName) {
|
|
|
151
151
|
const SearchField = styleCreators.createSearchField();
|
|
152
152
|
const TimeField = styleCreators.createTimeField();
|
|
153
153
|
const DateTimeLocalField = styleCreators.createDateTimeLocalField();
|
|
154
|
+
const ComboboxField = styleCreators.createComboboxField();
|
|
154
155
|
const ArrayField = styleCreators.createArrayField();
|
|
155
156
|
const TransferListField = styleCreators.createTransferListField();
|
|
156
157
|
const ToppingsField = styleCreators.createToppingsField();
|
|
@@ -241,6 +242,15 @@ function generateFieldComponents(schema, fieldsMetadata, styleName) {
|
|
|
241
242
|
typedSelectFields[typedPath] = selectFieldComponent;
|
|
242
243
|
return;
|
|
243
244
|
// Skip the type-based switch below
|
|
245
|
+
case "combobox":
|
|
246
|
+
const comboboxFieldComponent = (props, context) => {
|
|
247
|
+
const metadataProps = createMetadataProps(fieldMeta);
|
|
248
|
+
return ComboboxField({ ...metadataProps, ...props, name: typedPath }, context || { attrs: {} });
|
|
249
|
+
};
|
|
250
|
+
fields[typedPath] = comboboxFieldComponent;
|
|
251
|
+
components[componentName] = comboboxFieldComponent;
|
|
252
|
+
return;
|
|
253
|
+
// Skip the type-based switch below
|
|
244
254
|
case "text":
|
|
245
255
|
const textFieldComponent = (props, context) => {
|
|
246
256
|
const metadataProps = createMetadataProps(fieldMeta);
|
|
@@ -665,6 +675,36 @@ function hasFieldError(formState, path) {
|
|
|
665
675
|
function getFieldError(formState, path) {
|
|
666
676
|
return formState.errors[path] || "";
|
|
667
677
|
}
|
|
678
|
+
function setFieldFocus(formState, path, value) {
|
|
679
|
+
if (value) {
|
|
680
|
+
formState.focused[path] = true;
|
|
681
|
+
} else {
|
|
682
|
+
delete formState.focused[path];
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function isFieldFocused(formState, path) {
|
|
686
|
+
return !!formState.focused[path];
|
|
687
|
+
}
|
|
688
|
+
function setFieldDisplayText(formState, path, text) {
|
|
689
|
+
if (text) {
|
|
690
|
+
formState.displayText[path] = text;
|
|
691
|
+
} else {
|
|
692
|
+
delete formState.displayText[path];
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function getFieldDisplayText(formState, path) {
|
|
696
|
+
return formState.displayText[path] || "";
|
|
697
|
+
}
|
|
698
|
+
function setFieldSelectedIndex(formState, path, index) {
|
|
699
|
+
if (index >= 0) {
|
|
700
|
+
formState.selectedIndex[path] = index;
|
|
701
|
+
} else {
|
|
702
|
+
delete formState.selectedIndex[path];
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function getFieldSelectedIndex(formState, path) {
|
|
706
|
+
return formState.selectedIndex[path] ?? -1;
|
|
707
|
+
}
|
|
668
708
|
|
|
669
709
|
// src/registry/metadataRegistry.ts
|
|
670
710
|
var globalRegistry = {};
|
|
@@ -1335,6 +1375,9 @@ function useFormState(initialData, options) {
|
|
|
1335
1375
|
touched: {},
|
|
1336
1376
|
dirty: {},
|
|
1337
1377
|
errors: {},
|
|
1378
|
+
focused: {},
|
|
1379
|
+
displayText: {},
|
|
1380
|
+
selectedIndex: {},
|
|
1338
1381
|
// Form status
|
|
1339
1382
|
isSubmitting: false,
|
|
1340
1383
|
hasAttemptedSubmit: false,
|
|
@@ -1405,6 +1448,9 @@ function useFormState(initialData, options) {
|
|
|
1405
1448
|
state.touched = {};
|
|
1406
1449
|
state.dirty = {};
|
|
1407
1450
|
state.errors = {};
|
|
1451
|
+
state.focused = {};
|
|
1452
|
+
state.displayText = {};
|
|
1453
|
+
state.selectedIndex = {};
|
|
1408
1454
|
}
|
|
1409
1455
|
function setSubmitting(value) {
|
|
1410
1456
|
state.isSubmitting = value;
|
|
@@ -1797,7 +1843,13 @@ function useForm(schema, options) {
|
|
|
1797
1843
|
isDirtyField: (path) => isDirtyField(formState.state, path),
|
|
1798
1844
|
resetField: (path) => resetField(formState.state, initialFormData, path, schema, schemaCache),
|
|
1799
1845
|
hasError: (path) => hasFieldError(formState.state, path),
|
|
1800
|
-
getError: (path) => getFieldError(formState.state, path)
|
|
1846
|
+
getError: (path) => getFieldError(formState.state, path),
|
|
1847
|
+
setFocus: (path, value) => setFieldFocus(formState.state, path, value),
|
|
1848
|
+
isFocused: (path) => isFieldFocused(formState.state, path),
|
|
1849
|
+
setDisplayText: (path, text) => setFieldDisplayText(formState.state, path, text),
|
|
1850
|
+
getDisplayText: (path) => getFieldDisplayText(formState.state, path),
|
|
1851
|
+
setSelectedIndex: (path, index) => setFieldSelectedIndex(formState.state, path, index),
|
|
1852
|
+
getSelectedIndex: (path) => getFieldSelectedIndex(formState.state, path)
|
|
1801
1853
|
};
|
|
1802
1854
|
if (options.autoProvide !== false) {
|
|
1803
1855
|
provide(ZINIA_FORM_SCHEMA_KEY, schema);
|
|
@@ -3818,6 +3870,397 @@ function createDaisyUICheckboxField() {
|
|
|
3818
3870
|
];
|
|
3819
3871
|
return DaisyUICheckboxField;
|
|
3820
3872
|
}
|
|
3873
|
+
|
|
3874
|
+
// src/fields/types/ComboboxFieldProps.ts
|
|
3875
|
+
var COMBOBOX_FIELD_PROP_NAMES = [
|
|
3876
|
+
"name",
|
|
3877
|
+
"options",
|
|
3878
|
+
"placeholder",
|
|
3879
|
+
"disabled",
|
|
3880
|
+
"readonly",
|
|
3881
|
+
"class",
|
|
3882
|
+
"label",
|
|
3883
|
+
"hideLabel",
|
|
3884
|
+
"description",
|
|
3885
|
+
"required",
|
|
3886
|
+
"size",
|
|
3887
|
+
"variant",
|
|
3888
|
+
"allowCreate",
|
|
3889
|
+
"filterFn"
|
|
3890
|
+
];
|
|
3891
|
+
|
|
3892
|
+
// src/utils/propNormalization.ts
|
|
3893
|
+
function camelToKebab(str) {
|
|
3894
|
+
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();
|
|
3895
|
+
}
|
|
3896
|
+
function getPropVariants(propsDefinition) {
|
|
3897
|
+
const variants = /* @__PURE__ */ new Set();
|
|
3898
|
+
if (Array.isArray(propsDefinition)) {
|
|
3899
|
+
propsDefinition.forEach((propName) => {
|
|
3900
|
+
variants.add(propName);
|
|
3901
|
+
variants.add(camelToKebab(propName));
|
|
3902
|
+
});
|
|
3903
|
+
} else {
|
|
3904
|
+
Object.keys(propsDefinition).forEach((propName) => {
|
|
3905
|
+
variants.add(propName);
|
|
3906
|
+
variants.add(camelToKebab(propName));
|
|
3907
|
+
});
|
|
3908
|
+
}
|
|
3909
|
+
return variants;
|
|
3910
|
+
}
|
|
3911
|
+
function filterComponentPropsFromAttrs(propsDefinition, attrs) {
|
|
3912
|
+
const propVariants = getPropVariants(propsDefinition);
|
|
3913
|
+
const filtered = {};
|
|
3914
|
+
for (const key in attrs) {
|
|
3915
|
+
if (!propVariants.has(key)) {
|
|
3916
|
+
filtered[key] = attrs[key];
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
return filtered;
|
|
3920
|
+
}
|
|
3921
|
+
function normalizePropsFromAttrs(props, propsDefinition, attrs) {
|
|
3922
|
+
const propVariants = getPropVariants(propsDefinition);
|
|
3923
|
+
const normalized = { ...props };
|
|
3924
|
+
for (const key in attrs) {
|
|
3925
|
+
if (propVariants.has(key)) {
|
|
3926
|
+
const camelCaseKey = Array.isArray(propsDefinition) ? propsDefinition.find((p) => camelToKebab(p) === key) || key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : Object.keys(propsDefinition).find((p) => camelToKebab(p) === key) || key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
3927
|
+
if (!(camelCaseKey in normalized) || normalized[camelCaseKey] === void 0) {
|
|
3928
|
+
normalized[camelCaseKey] = attrs[key];
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
return normalized;
|
|
3933
|
+
}
|
|
3934
|
+
function createDaisyUIComboboxField() {
|
|
3935
|
+
const propsDefinition = [...COMBOBOX_FIELD_PROP_NAMES];
|
|
3936
|
+
const DaisyUIComboboxField = (props, { attrs }) => {
|
|
3937
|
+
const formState = inject(ZINIA_FORM_KEY);
|
|
3938
|
+
if (!formState) {
|
|
3939
|
+
console.error("ComboboxField: formState is required but was not injected");
|
|
3940
|
+
return null;
|
|
3941
|
+
}
|
|
3942
|
+
const fieldMetadata = formState.fieldsMetadata[props.name] || {};
|
|
3943
|
+
const normalizedProps = normalizePropsFromAttrs(props, propsDefinition, attrs);
|
|
3944
|
+
const normalizeOption = (option) => {
|
|
3945
|
+
if (typeof option === "string") {
|
|
3946
|
+
return { label: option, value: option };
|
|
3947
|
+
}
|
|
3948
|
+
return option;
|
|
3949
|
+
};
|
|
3950
|
+
const getOptions = computed(() => {
|
|
3951
|
+
if (normalizedProps.options) {
|
|
3952
|
+
return normalizedProps.options.map(normalizeOption);
|
|
3953
|
+
}
|
|
3954
|
+
const schemaOptions = fieldMetadata?.options || [];
|
|
3955
|
+
return schemaOptions.map((opt) => ({
|
|
3956
|
+
label: opt.label || String(opt.value),
|
|
3957
|
+
value: String(opt.value)
|
|
3958
|
+
}));
|
|
3959
|
+
});
|
|
3960
|
+
const filteredOptions = computed(() => {
|
|
3961
|
+
const query = String(displayText.value || "").toLowerCase();
|
|
3962
|
+
if (!query) {
|
|
3963
|
+
return getOptions.value;
|
|
3964
|
+
}
|
|
3965
|
+
const filterFn = normalizedProps.filterFn || ((q, opt) => {
|
|
3966
|
+
const normalized = normalizeOption(opt);
|
|
3967
|
+
return normalized.label.toLowerCase().includes(q.toLowerCase()) || normalized.value.toLowerCase().includes(q.toLowerCase());
|
|
3968
|
+
});
|
|
3969
|
+
return getOptions.value.filter((opt) => {
|
|
3970
|
+
const originalOpt = opt.label === opt.value ? opt.label : opt;
|
|
3971
|
+
return filterFn(query, originalOpt);
|
|
3972
|
+
});
|
|
3973
|
+
});
|
|
3974
|
+
const isNewValue = computed(() => {
|
|
3975
|
+
if (!normalizedProps.allowCreate) return false;
|
|
3976
|
+
const currentText = String(displayText.value || "").trim().toLowerCase();
|
|
3977
|
+
if (!currentText) return false;
|
|
3978
|
+
return !getOptions.value.some(
|
|
3979
|
+
(opt) => opt.value.toLowerCase() === currentText || opt.label.toLowerCase() === currentText
|
|
3980
|
+
);
|
|
3981
|
+
});
|
|
3982
|
+
const hasErrors = formState.hasError(props.name);
|
|
3983
|
+
const isTouched2 = formState.isTouched(props.name);
|
|
3984
|
+
const comboboxRoot = ref(null);
|
|
3985
|
+
const inputElement = ref(null);
|
|
3986
|
+
const selectedIndex = computed({
|
|
3987
|
+
get: () => formState.getSelectedIndex(props.name),
|
|
3988
|
+
set: (value) => formState.setSelectedIndex(props.name, value)
|
|
3989
|
+
});
|
|
3990
|
+
const isFocused = computed(() => formState.isFocused(props.name));
|
|
3991
|
+
const displayText = computed({
|
|
3992
|
+
get: () => formState.getDisplayText(props.name),
|
|
3993
|
+
set: (value) => formState.setDisplayText(props.name, value)
|
|
3994
|
+
});
|
|
3995
|
+
const currentFormValue = computed(() => formState.getValue(props.name));
|
|
3996
|
+
const getLabelForValue = (value) => {
|
|
3997
|
+
if (!value) return "";
|
|
3998
|
+
const valueStr = String(value);
|
|
3999
|
+
const option = getOptions.value.find((opt) => opt.value === valueStr);
|
|
4000
|
+
return option ? option.label : valueStr;
|
|
4001
|
+
};
|
|
4002
|
+
const displayLabelForValue = computed(() => getLabelForValue(currentFormValue.value));
|
|
4003
|
+
if (!isFocused.value && displayText.value !== displayLabelForValue.value) {
|
|
4004
|
+
displayText.value = displayLabelForValue.value;
|
|
4005
|
+
}
|
|
4006
|
+
const showDropdown = computed(() => {
|
|
4007
|
+
if (!isFocused.value) return false;
|
|
4008
|
+
if (getOptions.value.length > 0) return true;
|
|
4009
|
+
if (normalizedProps.allowCreate) {
|
|
4010
|
+
return String(formState.getValue(props.name) || "").trim().length > 0;
|
|
4011
|
+
}
|
|
4012
|
+
return false;
|
|
4013
|
+
});
|
|
4014
|
+
const inputClass = [
|
|
4015
|
+
"input",
|
|
4016
|
+
"w-full",
|
|
4017
|
+
"pr-10",
|
|
4018
|
+
props.size ? `input-${props.size}` : "",
|
|
4019
|
+
props.variant ? `input-${props.variant}` : "",
|
|
4020
|
+
isTouched2 && !hasErrors ? "input-success" : "",
|
|
4021
|
+
isTouched2 && hasErrors ? "input-error" : "",
|
|
4022
|
+
props.class
|
|
4023
|
+
].filter(Boolean).join(" ");
|
|
4024
|
+
const htmlAttrs = filterComponentPropsFromAttrs(propsDefinition, attrs);
|
|
4025
|
+
const inputProps = {
|
|
4026
|
+
type: "text",
|
|
4027
|
+
value: displayText.value,
|
|
4028
|
+
onInput: (event) => {
|
|
4029
|
+
const target = event.target;
|
|
4030
|
+
const inputValue = target.value;
|
|
4031
|
+
displayText.value = inputValue;
|
|
4032
|
+
formState.setSelectedIndex(props.name, -1);
|
|
4033
|
+
if (!isFocused.value) {
|
|
4034
|
+
formState.setFocus(props.name, true);
|
|
4035
|
+
}
|
|
4036
|
+
formState.setValue(props.name, inputValue);
|
|
4037
|
+
},
|
|
4038
|
+
onBlur: (event) => {
|
|
4039
|
+
const relatedTarget = event.relatedTarget;
|
|
4040
|
+
if (comboboxRoot.value?.contains(relatedTarget)) {
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
4043
|
+
const displayValue = String(displayText.value || "").trim();
|
|
4044
|
+
const matchingOption = getOptions.value.find(
|
|
4045
|
+
(opt) => opt.label.toLowerCase() === displayValue.toLowerCase() || opt.value.toLowerCase() === displayValue.toLowerCase()
|
|
4046
|
+
);
|
|
4047
|
+
if (matchingOption) {
|
|
4048
|
+
formState.setValue(props.name, matchingOption.value);
|
|
4049
|
+
displayText.value = matchingOption.label;
|
|
4050
|
+
} else if (normalizedProps.allowCreate && displayValue) {
|
|
4051
|
+
formState.setValue(props.name, displayValue);
|
|
4052
|
+
displayText.value = displayValue;
|
|
4053
|
+
} else if (!displayValue) {
|
|
4054
|
+
formState.setValue(props.name, "");
|
|
4055
|
+
displayText.value = "";
|
|
4056
|
+
} else {
|
|
4057
|
+
formState.setValue(props.name, displayValue);
|
|
4058
|
+
}
|
|
4059
|
+
formState.touchField(props.name);
|
|
4060
|
+
formState.validateField(props.name);
|
|
4061
|
+
formState.setFocus(props.name, false);
|
|
4062
|
+
},
|
|
4063
|
+
onFocus: () => {
|
|
4064
|
+
formState.setFocus(props.name, true);
|
|
4065
|
+
const labelForValue = displayLabelForValue.value;
|
|
4066
|
+
if (labelForValue && displayText.value !== labelForValue) {
|
|
4067
|
+
displayText.value = labelForValue;
|
|
4068
|
+
}
|
|
4069
|
+
},
|
|
4070
|
+
onKeydown: (event) => {
|
|
4071
|
+
const { key } = event;
|
|
4072
|
+
switch (key) {
|
|
4073
|
+
case "ArrowDown":
|
|
4074
|
+
event.preventDefault();
|
|
4075
|
+
event.stopPropagation();
|
|
4076
|
+
formState.setFocus(props.name, true);
|
|
4077
|
+
const totalOptions = filteredOptions.value.length + (isNewValue.value ? 1 : 0);
|
|
4078
|
+
if (totalOptions > 0) {
|
|
4079
|
+
const currentIndex = selectedIndex.value;
|
|
4080
|
+
const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % totalOptions;
|
|
4081
|
+
selectedIndex.value = newIndex;
|
|
4082
|
+
nextTick(() => {
|
|
4083
|
+
const selectedElement = comboboxRoot.value?.querySelector(
|
|
4084
|
+
newIndex === filteredOptions.value.length ? "#option-new" : `#option-${newIndex}`
|
|
4085
|
+
);
|
|
4086
|
+
if (selectedElement) {
|
|
4087
|
+
selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
4088
|
+
}
|
|
4089
|
+
});
|
|
4090
|
+
}
|
|
4091
|
+
break;
|
|
4092
|
+
case "ArrowUp":
|
|
4093
|
+
event.preventDefault();
|
|
4094
|
+
event.stopPropagation();
|
|
4095
|
+
formState.setFocus(props.name, true);
|
|
4096
|
+
const totalOptionsUp = filteredOptions.value.length + (isNewValue.value ? 1 : 0);
|
|
4097
|
+
if (totalOptionsUp > 0) {
|
|
4098
|
+
const currentIndex = selectedIndex.value;
|
|
4099
|
+
const newIndex = currentIndex < 0 ? totalOptionsUp - 1 : (currentIndex - 1 + totalOptionsUp) % totalOptionsUp;
|
|
4100
|
+
selectedIndex.value = newIndex;
|
|
4101
|
+
nextTick(() => {
|
|
4102
|
+
const selectedElement = comboboxRoot.value?.querySelector(
|
|
4103
|
+
newIndex === filteredOptions.value.length ? "#option-new" : `#option-${newIndex}`
|
|
4104
|
+
);
|
|
4105
|
+
if (selectedElement) {
|
|
4106
|
+
selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
4107
|
+
}
|
|
4108
|
+
});
|
|
4109
|
+
}
|
|
4110
|
+
break;
|
|
4111
|
+
case "Enter":
|
|
4112
|
+
event.preventDefault();
|
|
4113
|
+
if (selectedIndex.value >= 0 && selectedIndex.value < filteredOptions.value.length) {
|
|
4114
|
+
const option = filteredOptions.value[selectedIndex.value];
|
|
4115
|
+
formState.setValue(props.name, option.value);
|
|
4116
|
+
displayText.value = option.label;
|
|
4117
|
+
formState.touchField(props.name);
|
|
4118
|
+
formState.validateField(props.name);
|
|
4119
|
+
formState.setSelectedIndex(props.name, -1);
|
|
4120
|
+
formState.setFocus(props.name, false);
|
|
4121
|
+
} else if (selectedIndex.value === filteredOptions.value.length && isNewValue.value) {
|
|
4122
|
+
const displayValue = String(displayText.value || "").trim();
|
|
4123
|
+
formState.setValue(props.name, displayValue);
|
|
4124
|
+
formState.touchField(props.name);
|
|
4125
|
+
formState.validateField(props.name);
|
|
4126
|
+
formState.setSelectedIndex(props.name, -1);
|
|
4127
|
+
formState.setFocus(props.name, false);
|
|
4128
|
+
} else if (isNewValue.value && selectedIndex.value < 0) {
|
|
4129
|
+
const displayValue = String(displayText.value || "").trim();
|
|
4130
|
+
formState.setValue(props.name, displayValue);
|
|
4131
|
+
formState.touchField(props.name);
|
|
4132
|
+
formState.validateField(props.name);
|
|
4133
|
+
formState.setSelectedIndex(props.name, -1);
|
|
4134
|
+
formState.setFocus(props.name, false);
|
|
4135
|
+
}
|
|
4136
|
+
break;
|
|
4137
|
+
case "Escape":
|
|
4138
|
+
inputElement.value?.blur();
|
|
4139
|
+
break;
|
|
4140
|
+
}
|
|
4141
|
+
},
|
|
4142
|
+
name: props.name,
|
|
4143
|
+
...htmlAttrs
|
|
4144
|
+
};
|
|
4145
|
+
return /* @__PURE__ */ jsxs("label", { ref: comboboxRoot, class: "floating-label w-full", children: [
|
|
4146
|
+
!props.hideLabel && /* @__PURE__ */ jsxs("span", { children: [
|
|
4147
|
+
props.label || fieldMetadata.label,
|
|
4148
|
+
fieldMetadata.isRequired && /* @__PURE__ */ jsx("span", { class: "text-error", children: " *" })
|
|
4149
|
+
] }),
|
|
4150
|
+
/* @__PURE__ */ jsxs("div", { class: "relative", role: "combobox", "aria-expanded": showDropdown.value, "aria-haspopup": "listbox", children: [
|
|
4151
|
+
/* @__PURE__ */ jsx(
|
|
4152
|
+
"input",
|
|
4153
|
+
{
|
|
4154
|
+
ref: inputElement,
|
|
4155
|
+
class: inputClass,
|
|
4156
|
+
placeholder: props.placeholder || "Search or type new value",
|
|
4157
|
+
disabled: props.disabled,
|
|
4158
|
+
autocomplete: "off",
|
|
4159
|
+
"data-testid": `${formState.storeName}-combobox-field-${String(props.name)}`,
|
|
4160
|
+
...inputProps
|
|
4161
|
+
}
|
|
4162
|
+
),
|
|
4163
|
+
/* @__PURE__ */ jsx("div", { class: "absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none", children: /* @__PURE__ */ jsx(
|
|
4164
|
+
"svg",
|
|
4165
|
+
{
|
|
4166
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
4167
|
+
class: "h-5 w-5 text-base-content/50",
|
|
4168
|
+
fill: "none",
|
|
4169
|
+
viewBox: "0 0 24 24",
|
|
4170
|
+
stroke: "currentColor",
|
|
4171
|
+
"stroke-width": "2",
|
|
4172
|
+
children: /* @__PURE__ */ jsx("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M8 9l4-4 4 4m0 6l-4 4-4-4" })
|
|
4173
|
+
}
|
|
4174
|
+
) }),
|
|
4175
|
+
showDropdown.value && /* @__PURE__ */ jsx("div", { class: "absolute z-50 w-full mt-1 bg-base-100 border border-base-300 rounded-box shadow-lg max-h-60 overflow-auto", children: filteredOptions.value.length > 0 || isNewValue.value ? /* @__PURE__ */ jsxs("ul", { id: "combobox-listbox", class: "menu w-full", role: "listbox", children: [
|
|
4176
|
+
filteredOptions.value.map((option, index) => /* @__PURE__ */ jsx(
|
|
4177
|
+
"li",
|
|
4178
|
+
{
|
|
4179
|
+
id: `option-${index}`,
|
|
4180
|
+
role: "option",
|
|
4181
|
+
"aria-selected": index === selectedIndex.value,
|
|
4182
|
+
children: /* @__PURE__ */ jsx(
|
|
4183
|
+
"a",
|
|
4184
|
+
{
|
|
4185
|
+
href: "#",
|
|
4186
|
+
class: `w-full ${index === selectedIndex.value ? "menu-focus" : ""}`,
|
|
4187
|
+
onClick: (e) => {
|
|
4188
|
+
e.preventDefault();
|
|
4189
|
+
e.stopPropagation();
|
|
4190
|
+
formState.setValue(props.name, option.value);
|
|
4191
|
+
displayText.value = option.label;
|
|
4192
|
+
formState.touchField(props.name);
|
|
4193
|
+
formState.validateField(props.name);
|
|
4194
|
+
formState.setSelectedIndex(props.name, -1);
|
|
4195
|
+
formState.setFocus(props.name, false);
|
|
4196
|
+
},
|
|
4197
|
+
children: option.label
|
|
4198
|
+
}
|
|
4199
|
+
)
|
|
4200
|
+
},
|
|
4201
|
+
option.value
|
|
4202
|
+
)),
|
|
4203
|
+
isNewValue.value && /* @__PURE__ */ jsx(
|
|
4204
|
+
"li",
|
|
4205
|
+
{
|
|
4206
|
+
id: `option-new`,
|
|
4207
|
+
role: "option",
|
|
4208
|
+
"aria-selected": selectedIndex.value === filteredOptions.value.length,
|
|
4209
|
+
children: /* @__PURE__ */ jsx(
|
|
4210
|
+
"a",
|
|
4211
|
+
{
|
|
4212
|
+
href: "#",
|
|
4213
|
+
class: `w-full ${selectedIndex.value === filteredOptions.value.length ? "menu-focus" : ""}`,
|
|
4214
|
+
onClick: (e) => {
|
|
4215
|
+
e.preventDefault();
|
|
4216
|
+
e.stopPropagation();
|
|
4217
|
+
const displayValue = String(displayText.value || "").trim();
|
|
4218
|
+
formState.setValue(props.name, displayValue);
|
|
4219
|
+
formState.touchField(props.name);
|
|
4220
|
+
formState.validateField(props.name);
|
|
4221
|
+
formState.setSelectedIndex(props.name, -1);
|
|
4222
|
+
formState.setFocus(props.name, false);
|
|
4223
|
+
},
|
|
4224
|
+
children: /* @__PURE__ */ jsxs("div", { class: "flex items-start gap-2", children: [
|
|
4225
|
+
/* @__PURE__ */ jsx(
|
|
4226
|
+
"svg",
|
|
4227
|
+
{
|
|
4228
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
4229
|
+
class: "h-5 w-5 text-success mt-0.5 flex-shrink-0",
|
|
4230
|
+
fill: "none",
|
|
4231
|
+
viewBox: "0 0 24 24",
|
|
4232
|
+
stroke: "currentColor",
|
|
4233
|
+
"stroke-width": "2",
|
|
4234
|
+
children: /* @__PURE__ */ jsx("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M12 4v16m8-8H4" })
|
|
4235
|
+
}
|
|
4236
|
+
),
|
|
4237
|
+
/* @__PURE__ */ jsxs("div", { class: "text-sm", children: [
|
|
4238
|
+
/* @__PURE__ */ jsx("p", { class: "font-medium text-success", children: "New value" }),
|
|
4239
|
+
/* @__PURE__ */ jsxs("p", { class: "text-base-content/70", children: [
|
|
4240
|
+
'"',
|
|
4241
|
+
displayText.value,
|
|
4242
|
+
'" will be saved.'
|
|
4243
|
+
] })
|
|
4244
|
+
] })
|
|
4245
|
+
] })
|
|
4246
|
+
}
|
|
4247
|
+
)
|
|
4248
|
+
}
|
|
4249
|
+
)
|
|
4250
|
+
] }) : null })
|
|
4251
|
+
] }),
|
|
4252
|
+
props.description && /* @__PURE__ */ jsx("p", { class: "text-sm mt-1", children: props.description }),
|
|
4253
|
+
isTouched2 && hasErrors && /* @__PURE__ */ jsx("p", { class: "text-error text-xs", children: formState.getError(props.name) }),
|
|
4254
|
+
isNewValue.value && displayText.value && !hasErrors && /* @__PURE__ */ jsxs("p", { class: "text-success text-xs", children: [
|
|
4255
|
+
'\u2713 New value "',
|
|
4256
|
+
displayText.value,
|
|
4257
|
+
'" will be saved.'
|
|
4258
|
+
] })
|
|
4259
|
+
] });
|
|
4260
|
+
};
|
|
4261
|
+
DaisyUIComboboxField.props = propsDefinition;
|
|
4262
|
+
return DaisyUIComboboxField;
|
|
4263
|
+
}
|
|
3821
4264
|
function createDaisyUICurrencyField() {
|
|
3822
4265
|
const DaisyUICurrencyField = (props, { attrs }) => {
|
|
3823
4266
|
const formState = inject(ZINIA_FORM_KEY);
|
|
@@ -4774,49 +5217,6 @@ var SELECT_FIELD_PROP_NAMES = [
|
|
|
4774
5217
|
"valueToLabel",
|
|
4775
5218
|
"useSchemaOptions"
|
|
4776
5219
|
];
|
|
4777
|
-
|
|
4778
|
-
// src/utils/propNormalization.ts
|
|
4779
|
-
function camelToKebab(str) {
|
|
4780
|
-
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();
|
|
4781
|
-
}
|
|
4782
|
-
function getPropVariants(propsDefinition) {
|
|
4783
|
-
const variants = /* @__PURE__ */ new Set();
|
|
4784
|
-
if (Array.isArray(propsDefinition)) {
|
|
4785
|
-
propsDefinition.forEach((propName) => {
|
|
4786
|
-
variants.add(propName);
|
|
4787
|
-
variants.add(camelToKebab(propName));
|
|
4788
|
-
});
|
|
4789
|
-
} else {
|
|
4790
|
-
Object.keys(propsDefinition).forEach((propName) => {
|
|
4791
|
-
variants.add(propName);
|
|
4792
|
-
variants.add(camelToKebab(propName));
|
|
4793
|
-
});
|
|
4794
|
-
}
|
|
4795
|
-
return variants;
|
|
4796
|
-
}
|
|
4797
|
-
function filterComponentPropsFromAttrs(propsDefinition, attrs) {
|
|
4798
|
-
const propVariants = getPropVariants(propsDefinition);
|
|
4799
|
-
const filtered = {};
|
|
4800
|
-
for (const key in attrs) {
|
|
4801
|
-
if (!propVariants.has(key)) {
|
|
4802
|
-
filtered[key] = attrs[key];
|
|
4803
|
-
}
|
|
4804
|
-
}
|
|
4805
|
-
return filtered;
|
|
4806
|
-
}
|
|
4807
|
-
function normalizePropsFromAttrs(props, propsDefinition, attrs) {
|
|
4808
|
-
const propVariants = getPropVariants(propsDefinition);
|
|
4809
|
-
const normalized = { ...props };
|
|
4810
|
-
for (const key in attrs) {
|
|
4811
|
-
if (propVariants.has(key)) {
|
|
4812
|
-
const camelCaseKey = Array.isArray(propsDefinition) ? propsDefinition.find((p) => camelToKebab(p) === key) || key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : Object.keys(propsDefinition).find((p) => camelToKebab(p) === key) || key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
4813
|
-
if (!(camelCaseKey in normalized) || normalized[camelCaseKey] === void 0) {
|
|
4814
|
-
normalized[camelCaseKey] = attrs[key];
|
|
4815
|
-
}
|
|
4816
|
-
}
|
|
4817
|
-
}
|
|
4818
|
-
return normalized;
|
|
4819
|
-
}
|
|
4820
5220
|
function createDaisyUISelectField() {
|
|
4821
5221
|
const propsDefinition = [...SELECT_FIELD_PROP_NAMES];
|
|
4822
5222
|
const DaisyUISelectField = (props, { attrs }) => {
|
|
@@ -7375,6 +7775,7 @@ var daisyUIStyle = {
|
|
|
7375
7775
|
createSearchField: createDaisyUISearchField,
|
|
7376
7776
|
createTimeField: createDaisyUITimeField,
|
|
7377
7777
|
createDateTimeLocalField: createDaisyUIDateTimeLocalField,
|
|
7778
|
+
createComboboxField: createDaisyUIComboboxField,
|
|
7378
7779
|
createArrayField: createDaisyUIArrayField,
|
|
7379
7780
|
createTransferListField: createDaisyUITransferListField,
|
|
7380
7781
|
createToppingsField: createDaisyUIToppingsField,
|