srcdev-nuxt-forms 0.1.0 → 0.2.0
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/LICENSE +21 -0
- package/assets/styles/forms/index.css +1 -0
- package/assets/styles/forms/themes/_primary.css +2 -0
- package/assets/styles/forms/themes/_secondary.css +2 -0
- package/assets/styles/forms/utils/_a11y.css +5 -0
- package/assets/styles/forms/utils/index.css +1 -0
- package/assets/styles/forms/variables/_theme.css +11 -17
- package/assets/styles/variables/colors/_gray.css +1 -0
- package/components/forms/c12/prop-validators/index.ts +8 -20
- package/components/forms/c12/utils.ts +14 -0
- package/components/forms/c12/validation-patterns/en.json +12 -0
- package/components/forms/form-errors/InputError.vue +132 -0
- package/components/forms/input-button/InputButtonCore.vue +11 -8
- package/components/forms/input-button/variants/InputButtonConfirm.vue +1 -1
- package/components/forms/input-button/variants/InputButtonSubmit.vue +1 -1
- package/components/forms/input-checkbox/InputCheckboxCore.vue +407 -0
- package/components/forms/input-checkbox/InputCheckboxWithLabel.vue +125 -0
- package/components/forms/input-checkbox/variants/MultipleCheckboxes.vue +194 -0
- package/components/forms/input-checkbox/variants/SingleCheckbox.vue +157 -0
- package/components/forms/input-radio/InputRadioCore.vue +226 -0
- package/components/forms/input-radio/InputRadioWithLabel.vue +118 -0
- package/components/forms/input-radio/variants/MultipleRadio.vue +183 -0
- package/components/forms/input-radio/variants/SingleRadio.vue +131 -0
- package/components/forms/input-range/InputRangeCore.vue +171 -0
- package/components/forms/input-range/variants/InputRangeDefault.vue +131 -0
- package/components/forms/input-text/InputTextCore.vue +61 -31
- package/components/forms/input-text/variants/material/InputPasswordMaterial.vue +27 -1
- package/components/forms/input-text/variants/material/InputTextMaterial.vue +1 -8
- package/components/forms/input-text/variants/material/InputTextMaterialCore.vue +83 -28
- package/components/forms/input-textarea/InputTextareaCore.vue +170 -0
- package/components/forms/input-textarea/variants/material/InputTextareaMaterial.vue +75 -0
- package/components/forms/input-textarea/variants/material/InputTextareaMaterialCore.vue +290 -0
- package/components/ui/content-grid/ContentGrid.vue +85 -0
- package/composables/useErrorMessages.ts +17 -5
- package/composables/useFormControl.ts +147 -37
- package/layouts/default.vue +7 -13
- package/nuxt.config.ts +22 -0
- package/package.json +9 -8
- package/pages/forms/examples/buttons/index.vue +14 -13
- package/pages/forms/examples/material/text-fields.vue +320 -84
- package/pages/limit-text.vue +43 -0
- package/server/api/places/list.get.ts +23 -0
- package/server/api/textFields.post.ts +37 -0
- package/server/api/utils/index.get.ts +20 -0
- package/server/data/places/cities.json +37 -0
- package/server/data/places/countries.json +55 -0
- package/server/data/utils/title.json +49 -0
- package/types/types.forms.ts +33 -3
- package/types/types.places.ts +8 -0
- package/pages/forms/examples/material/text-fields-compact.vue +0 -136
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="input-text-wrapper" :class="[{ 'has-left-content': hasLeftContent }]">
|
|
2
|
+
<div class="input-text-wrapper" :class="[{ 'has-left-content': hasLeftContent }, { 'has-right-content': hasRightContent }]">
|
|
3
3
|
<template v-if="hasLeftContent">
|
|
4
4
|
<span class="left-content">
|
|
5
5
|
<slot name="left"></slot>
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
:pattern="componentValidation.pattern"
|
|
15
15
|
:maxlength="componentValidation.maxlength"
|
|
16
16
|
:required
|
|
17
|
-
:class="['input-text', 'text-normal', styleClassPassthrough, { active: isFocused }, { dirty:
|
|
17
|
+
:class="['input-text-core', 'text-normal', styleClassPassthrough, { active: isFocused }, { dirty: fieldIsDirty }, { error: fieldHasError }]"
|
|
18
18
|
v-model="modelValue.data[name]"
|
|
19
19
|
ref="inputField"
|
|
20
|
-
:
|
|
20
|
+
:aria-invalid="fieldHasError"
|
|
21
|
+
:aria-describedby="`${id}-error-message`"
|
|
21
22
|
@focusin="updateFocus(name, true)"
|
|
22
23
|
@focusout="updateFocus(name, false)"
|
|
23
24
|
/>
|
|
@@ -31,19 +32,18 @@
|
|
|
31
32
|
</template>
|
|
32
33
|
|
|
33
34
|
<script setup lang="ts">
|
|
34
|
-
import type { InpuTextC12, IFormData } from '@/types/types.forms';
|
|
35
|
+
import type { InpuTextC12, IFormFieldC12, IFormData } from '@/types/types.forms';
|
|
35
36
|
import { validationConfig } from '@/components/forms/c12/validation-patterns';
|
|
37
|
+
import propValidators from '../c12/prop-validators';
|
|
36
38
|
|
|
37
39
|
const props = defineProps({
|
|
38
40
|
type: {
|
|
39
|
-
// type: String as PropType<"text" | "password" | "tel" | "number" | "email" | "url">, // This breaks props setup in unit tests
|
|
40
41
|
type: String,
|
|
41
42
|
validator(value: string) {
|
|
42
|
-
return
|
|
43
|
+
return propValidators.inputTypesText.includes(value);
|
|
43
44
|
},
|
|
44
45
|
},
|
|
45
46
|
id: {
|
|
46
|
-
// type: String as PropType<string>,
|
|
47
47
|
type: String,
|
|
48
48
|
required: true,
|
|
49
49
|
},
|
|
@@ -79,18 +79,10 @@ const updateFocus = (name: string, isFocused: boolean) => {
|
|
|
79
79
|
modelValue.value.focusedField = isFocused ? name : '';
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
const isPending = computed(() => {
|
|
83
|
-
return modelValue.value.isPending;
|
|
84
|
-
});
|
|
85
|
-
|
|
86
82
|
const isFocused = computed(() => {
|
|
87
83
|
return modelValue.value.focusedField == name.value;
|
|
88
84
|
});
|
|
89
85
|
|
|
90
|
-
const isDirty = computed(() => {
|
|
91
|
-
return modelValue.value.dirtyFields[name.value];
|
|
92
|
-
});
|
|
93
|
-
|
|
94
86
|
const name = computed(() => {
|
|
95
87
|
return props.name !== null ? props.name : props.id;
|
|
96
88
|
});
|
|
@@ -100,32 +92,56 @@ const validatorLocale = toRef(useRuntimeConfig().public.validatorLocale);
|
|
|
100
92
|
const componentValidation = validationConfig[validatorLocale.value][props.validation];
|
|
101
93
|
const inputField = ref<HTMLInputElement | null>(null);
|
|
102
94
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
const fieldIsDirty = computed(() => {
|
|
96
|
+
return modelValue.value!.formFieldsC12[name.value].isDirty;
|
|
97
|
+
});
|
|
98
|
+
const fieldHasError = computed(() => {
|
|
99
|
+
return modelValue.value!.submitAttempted && !modelValue.value!.formFieldsC12[name.value].isValid;
|
|
100
|
+
});
|
|
108
101
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
102
|
+
// const { fieldHasError } = useFormControl(name.value);
|
|
103
|
+
|
|
104
|
+
const formFieldC12 = <IFormFieldC12>{
|
|
105
|
+
label: props.c12.label,
|
|
106
|
+
placeholder: props.c12.placeholder,
|
|
107
|
+
errorMessage: props.c12.errorMessage,
|
|
108
|
+
useCustomError: false,
|
|
109
|
+
customErrors: {},
|
|
110
|
+
isValid: false,
|
|
111
|
+
isDirty: false,
|
|
112
|
+
type: 'string',
|
|
113
|
+
previousValue: null,
|
|
114
114
|
};
|
|
115
|
+
modelValue.value!.formFieldsC12[name.value] = formFieldC12;
|
|
116
|
+
|
|
117
|
+
const { initFormFieldsC12 } = useFormControl();
|
|
118
|
+
initFormFieldsC12(props.name, formFieldC12);
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
const fieldValue = computed(() => {
|
|
121
|
+
return modelValue.value.data[name.value];
|
|
122
|
+
});
|
|
119
123
|
|
|
124
|
+
watch(fieldValue, () => {
|
|
125
|
+
if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
|
|
126
|
+
modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
|
|
127
|
+
}
|
|
128
|
+
modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
|
|
120
129
|
modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
|
|
131
|
+
if (modelValue.value!.formFieldsC12[name.value].useCustomError && modelValue.value.data[props.name] === modelValue.value.formFieldsC12[props.name].previousValue) {
|
|
132
|
+
modelValue.value!.validityState[name.value] = false;
|
|
133
|
+
modelValue.value!.formFieldsC12[name.value].isValid = false;
|
|
134
|
+
modelValue.value.displayErrorMessages = true;
|
|
123
135
|
}
|
|
124
136
|
});
|
|
125
137
|
|
|
126
138
|
const isValid = () => {
|
|
127
139
|
setTimeout(() => {
|
|
128
140
|
modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
|
|
141
|
+
modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
|
|
142
|
+
if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
|
|
143
|
+
modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
|
|
144
|
+
}
|
|
129
145
|
}, 0);
|
|
130
146
|
};
|
|
131
147
|
|
|
@@ -136,10 +152,12 @@ onMounted(() => {
|
|
|
136
152
|
|
|
137
153
|
<style lang="css">
|
|
138
154
|
.input-text-wrapper {
|
|
139
|
-
display: flex;
|
|
140
155
|
align-items: center;
|
|
156
|
+
display: grid;
|
|
157
|
+
grid-template-columns: 1fr;
|
|
141
158
|
|
|
142
159
|
&.has-left-content {
|
|
160
|
+
grid-template-columns: auto 1fr;
|
|
143
161
|
margin-left: var(--_gutter);
|
|
144
162
|
|
|
145
163
|
.left-content {
|
|
@@ -147,5 +165,17 @@ onMounted(() => {
|
|
|
147
165
|
align-items: center;
|
|
148
166
|
}
|
|
149
167
|
}
|
|
168
|
+
|
|
169
|
+
&.has-right-content {
|
|
170
|
+
display: grid;
|
|
171
|
+
grid-template-columns: 1fr auto;
|
|
172
|
+
/* display: flex; */
|
|
173
|
+
margin-right: var(--_gutter);
|
|
174
|
+
|
|
175
|
+
.right-content {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
150
180
|
}
|
|
151
181
|
</style>
|
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
<template #input>
|
|
4
4
|
<InputTextCore :id :name :type :validation :required v-model="modelValue" :c12 :style-class-passthrough="styleClassPassthrough">
|
|
5
5
|
<template #right>
|
|
6
|
-
<InputButtonCore
|
|
6
|
+
<InputButtonCore
|
|
7
|
+
type="button"
|
|
8
|
+
@click.stop.prevent="toggleDisplayPassword"
|
|
9
|
+
:is-pending="false"
|
|
10
|
+
:buttonText
|
|
11
|
+
theme="ghost"
|
|
12
|
+
size="x-small"
|
|
13
|
+
@focusin="updateFocus(name, true)"
|
|
14
|
+
@focusout="updateFocus(name, false)"
|
|
15
|
+
>
|
|
7
16
|
<template #iconOnly>
|
|
8
17
|
<Icon v-if="displayPassword" name="radix-icons:eye-none" class="icon" />
|
|
9
18
|
<Icon v-else name="radix-icons:eye-open" class="icon" />
|
|
@@ -75,6 +84,10 @@ const name = computed(() => {
|
|
|
75
84
|
|
|
76
85
|
const modelValue = defineModel() as Ref<IFormData>;
|
|
77
86
|
|
|
87
|
+
const updateFocus = (name: string, isFocused: boolean) => {
|
|
88
|
+
modelValue.value.focusedField = isFocused ? name : '';
|
|
89
|
+
};
|
|
90
|
+
|
|
78
91
|
const displayPassword = ref(false);
|
|
79
92
|
|
|
80
93
|
const type = computed(() => {
|
|
@@ -82,7 +95,20 @@ const type = computed(() => {
|
|
|
82
95
|
return displayPassword.value ? 'text' : 'password';
|
|
83
96
|
});
|
|
84
97
|
|
|
98
|
+
const buttonText = computed(() => {
|
|
99
|
+
return displayPassword.value ? 'Hide password' : 'Show password';
|
|
100
|
+
});
|
|
101
|
+
|
|
85
102
|
const toggleDisplayPassword = () => {
|
|
86
103
|
displayPassword.value = !displayPassword.value;
|
|
87
104
|
};
|
|
88
105
|
</script>
|
|
106
|
+
|
|
107
|
+
<style lang="css" scoped>
|
|
108
|
+
/* :deep(.input-text-core:not(.active)),
|
|
109
|
+
:deep(.input-text-core:not(.dirty)) {
|
|
110
|
+
+ .right-content {
|
|
111
|
+
display: none !important;
|
|
112
|
+
}
|
|
113
|
+
} */
|
|
114
|
+
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<InputTextMaterialCore
|
|
2
|
+
<InputTextMaterialCore type="text" :id :name :required :c12 :styleClassPassthrough :theme v-model="modelValue">
|
|
3
3
|
<template #input>
|
|
4
4
|
<InputTextCore :id :name type="text" :validation :required v-model="modelValue" :c12 :style-class-passthrough="styleClassPassthrough" />
|
|
5
5
|
</template>
|
|
@@ -33,13 +33,6 @@ const props = defineProps({
|
|
|
33
33
|
return propValidators.theme.includes(value);
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
|
-
type: {
|
|
37
|
-
// type: String as PropType<"text" | "password" | "tel" | "number" | "email" | "url">, // This breaks props setup in unit tests
|
|
38
|
-
type: String,
|
|
39
|
-
validator(value: string) {
|
|
40
|
-
return ['text', 'password', 'tel', 'number', 'email', 'url'].includes(value);
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
36
|
id: {
|
|
44
37
|
// type: String as PropType<string>,
|
|
45
38
|
type: String,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="input-text-material" :class="[`theme-${theme}`, { error: fieldHasError }, { compact: compact }]">
|
|
3
|
-
<label class="label" :class="[{ active: isFocused }, { error: fieldHasError }, { dirty:
|
|
4
|
-
<span>{{
|
|
3
|
+
<label class="input-text-label" :for="id" :class="[{ active: isFocused }, { error: fieldHasError }, { dirty: fieldIsDirty }, { compact: compact }]">
|
|
4
|
+
<span>{{ c12.label }}</span>
|
|
5
5
|
</label>
|
|
6
|
-
<div class="input-text-container" :class="[{ active: isFocused }, { error: fieldHasError }, { dirty:
|
|
6
|
+
<div class="input-text-container" :class="[{ active: isFocused }, { error: fieldHasError }, { dirty: fieldIsDirty }, { compact: compact }]">
|
|
7
7
|
<slot name="input"></slot>
|
|
8
8
|
</div>
|
|
9
|
+
<InputError :errorMessaging :fieldHasError :id :isDetached="false" />
|
|
9
10
|
</div>
|
|
10
11
|
</template>
|
|
11
12
|
|
|
@@ -38,8 +39,9 @@ const props = defineProps({
|
|
|
38
39
|
},
|
|
39
40
|
type: {
|
|
40
41
|
type: String,
|
|
42
|
+
required: true,
|
|
41
43
|
validator(value: string) {
|
|
42
|
-
return
|
|
44
|
+
return propValidators.inputTypesText.includes(value);
|
|
43
45
|
},
|
|
44
46
|
},
|
|
45
47
|
id: {
|
|
@@ -68,8 +70,16 @@ const props = defineProps({
|
|
|
68
70
|
},
|
|
69
71
|
});
|
|
70
72
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
+
const errorMessaging = computed(() => {
|
|
74
|
+
if (
|
|
75
|
+
typeof modelValue.value!.formFieldsC12[props.name] !== 'undefined' &&
|
|
76
|
+
modelValue.value!.formFieldsC12[props.name].useCustomError &&
|
|
77
|
+
modelValue.value.data[props.name] === modelValue.value.formFieldsC12[props.name].previousValue
|
|
78
|
+
) {
|
|
79
|
+
return modelValue.value.formFieldsC12[props.name]?.customErrors || [];
|
|
80
|
+
} else {
|
|
81
|
+
return props.c12.errorMessage;
|
|
82
|
+
}
|
|
73
83
|
});
|
|
74
84
|
|
|
75
85
|
const modelValue = defineModel() as Ref<IFormData>;
|
|
@@ -78,17 +88,58 @@ const isFocused = computed(() => {
|
|
|
78
88
|
return modelValue.value.focusedField == props.name;
|
|
79
89
|
});
|
|
80
90
|
|
|
81
|
-
const
|
|
82
|
-
|
|
91
|
+
const fieldIsDirty = computed(() => {
|
|
92
|
+
if (typeof modelValue.value!.formFieldsC12[props.name] !== 'undefined') {
|
|
93
|
+
return modelValue.value!.formFieldsC12[props.name].isDirty;
|
|
94
|
+
} else {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
83
97
|
});
|
|
84
98
|
|
|
85
|
-
const
|
|
86
|
-
|
|
99
|
+
const fieldHasError = computed(() => {
|
|
100
|
+
if (typeof modelValue.value!.formFieldsC12[props.name] !== 'undefined') {
|
|
101
|
+
return modelValue.value!.submitAttempted && !modelValue.value!.formFieldsC12[props.name].isValid;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
});
|
|
87
105
|
</script>
|
|
88
106
|
|
|
89
107
|
<style lang="css">
|
|
90
108
|
.input-text-material {
|
|
91
|
-
|
|
109
|
+
--_form-theme: var(--theme-form-primary);
|
|
110
|
+
--_focus-colour: var(--theme-form-primary-focus);
|
|
111
|
+
--_gutter: 12px;
|
|
112
|
+
--_border-width: var(--input-border-width-thin);
|
|
113
|
+
--_border-color: var(--_form-theme);
|
|
114
|
+
--_outline-width: var(--input-outline-width-thin);
|
|
115
|
+
|
|
116
|
+
&.theme-secondary {
|
|
117
|
+
--_form-theme: var(--theme-form-secondary);
|
|
118
|
+
--_focus-colour: var(--theme-form-secondary-focus);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&.error {
|
|
122
|
+
--_form-theme: var(--theme-error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
&:has(.input-text:invalid),
|
|
127
|
+
&:has(.input-textarea:invalid) {
|
|
128
|
+
--_form-theme: green;
|
|
129
|
+
}
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
&:not(:placeholder-shown):invalid {
|
|
134
|
+
--_form-theme: green;
|
|
135
|
+
}
|
|
136
|
+
&:has(.text-input:not(:placeholder-shown):invalid) {
|
|
137
|
+
--_form-theme: red;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
.input-text-core {
|
|
92
143
|
background-color: transparent;
|
|
93
144
|
line-height: var(--line-height);
|
|
94
145
|
|
|
@@ -104,43 +155,46 @@ setDefaultError(props.c12.errorMessage);
|
|
|
104
155
|
}
|
|
105
156
|
}
|
|
106
157
|
|
|
107
|
-
label {
|
|
158
|
+
.input-text-label {
|
|
159
|
+
color: var(--_form-theme);
|
|
108
160
|
margin: initial;
|
|
109
161
|
line-height: var(--line-height);
|
|
110
162
|
padding: initial;
|
|
163
|
+
transition: color 0.2s ease-in-out;
|
|
111
164
|
}
|
|
112
165
|
|
|
113
|
-
--_gutter: 12px;
|
|
114
|
-
--_form-theme: var(--theme-form-primary);
|
|
115
|
-
--_border-width: var(--input-border-width-default);
|
|
116
|
-
|
|
117
166
|
display: grid;
|
|
118
167
|
border-radius: 2px;
|
|
119
|
-
border: var(--_border-width) solid var(--
|
|
168
|
+
border: var(--_border-width) solid var(--_border-color);
|
|
120
169
|
|
|
121
170
|
margin-bottom: 20px;
|
|
122
171
|
overflow: hidden;
|
|
123
172
|
|
|
124
|
-
&.theme-secondary {
|
|
125
|
-
--_form-theme: var(--theme-form-secondary);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
173
|
&:focus-within {
|
|
129
|
-
|
|
130
|
-
outline: var(--
|
|
174
|
+
--_border-color: white;
|
|
175
|
+
outline: var(--_outline-width) solid hsl(from var(--_form-theme) h s 50%);
|
|
131
176
|
background-color: hsl(from var(--_form-theme) h s 95%);
|
|
132
177
|
}
|
|
133
178
|
|
|
179
|
+
&:has(.input-text-core:focus-visible),
|
|
180
|
+
&:has(.input-button-core:focus-visible) {
|
|
181
|
+
/* box-shadow: 0 0 2px 3px var(--_focus-colour);
|
|
182
|
+
outline-color: var(--_focus-colour); */
|
|
183
|
+
|
|
184
|
+
outline: var(--focus-visible-outline);
|
|
185
|
+
box-shadow: var(--focus-visible-box-shadow);
|
|
186
|
+
}
|
|
187
|
+
|
|
134
188
|
&.error {
|
|
135
189
|
/* outline: calc(var(--_border-width) * 2) solid var(--theme-error); */
|
|
136
190
|
|
|
137
191
|
border: var(--_border-width) solid var(--theme-error);
|
|
138
|
-
outline: var(--
|
|
192
|
+
outline: var(--_outline-width) solid hsl(from var(--theme-error) h s 75%);
|
|
139
193
|
|
|
140
194
|
background-color: hsl(from var(--theme-error) h s 95%);
|
|
141
195
|
}
|
|
142
196
|
|
|
143
|
-
.label {
|
|
197
|
+
.input-text-label {
|
|
144
198
|
grid-row: 1;
|
|
145
199
|
grid-column: 1;
|
|
146
200
|
|
|
@@ -157,6 +211,7 @@ setDefaultError(props.c12.errorMessage);
|
|
|
157
211
|
&.dirty,
|
|
158
212
|
&.error {
|
|
159
213
|
font-size: 16px;
|
|
214
|
+
height: 1.5em;
|
|
160
215
|
transform: translateY(-2px);
|
|
161
216
|
z-index: auto;
|
|
162
217
|
}
|
|
@@ -177,7 +232,7 @@ setDefaultError(props.c12.errorMessage);
|
|
|
177
232
|
opacity: 1;
|
|
178
233
|
}
|
|
179
234
|
|
|
180
|
-
.input-text {
|
|
235
|
+
.input-text-core {
|
|
181
236
|
font-family: var(--font-family);
|
|
182
237
|
border: 0px solid green;
|
|
183
238
|
padding: calc(var(--_gutter) / 2) var(--_gutter);
|
|
@@ -223,7 +278,7 @@ setDefaultError(props.c12.errorMessage);
|
|
|
223
278
|
}
|
|
224
279
|
}
|
|
225
280
|
|
|
226
|
-
.label {
|
|
281
|
+
.input-text-label {
|
|
227
282
|
&.compact {
|
|
228
283
|
align-content: center;
|
|
229
284
|
font-size: 16px;
|
|
@@ -239,7 +294,7 @@ setDefaultError(props.c12.errorMessage);
|
|
|
239
294
|
&.error {
|
|
240
295
|
font-size: 16px;
|
|
241
296
|
font-weight: 500;
|
|
242
|
-
transform: translateY(-
|
|
297
|
+
transform: translateY(-14px);
|
|
243
298
|
z-index: auto;
|
|
244
299
|
|
|
245
300
|
span {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="input-textarea-wrapper" :class="[{ 'has-left-content': hasLeftContent }, { 'has-right-content': hasRightContent }]">
|
|
3
|
+
<span v-if="hasLeftContent" class="left-content">
|
|
4
|
+
<slot name="left"></slot>
|
|
5
|
+
</span>
|
|
6
|
+
|
|
7
|
+
<textarea
|
|
8
|
+
:id
|
|
9
|
+
:name
|
|
10
|
+
:pattern="componentValidation.pattern"
|
|
11
|
+
:maxlength="componentValidation.maxlength"
|
|
12
|
+
:required
|
|
13
|
+
:class="['input-textarea-core', 'text-normal', styleClassPassthrough, { active: isFocused }, { dirty: fieldIsDirty }, { error: fieldHasError }]"
|
|
14
|
+
v-model="<string>modelValue.data[name]"
|
|
15
|
+
ref="inputField"
|
|
16
|
+
:placeholder="c12.placeholder"
|
|
17
|
+
:aria-invalid="fieldHasError"
|
|
18
|
+
:aria-describedby="`${id}-error-message`"
|
|
19
|
+
@focusin="updateFocus(name, true)"
|
|
20
|
+
@focusout="updateFocus(name, false)"
|
|
21
|
+
></textarea>
|
|
22
|
+
|
|
23
|
+
<span v-if="hasRightContent" class="right-content">
|
|
24
|
+
<slot name="right"></slot>
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
import type { InpuTextC12, IFormFieldC12, IFormData } from '@/types/types.forms';
|
|
31
|
+
import { validationConfig } from '@/components/forms/c12/validation-patterns';
|
|
32
|
+
|
|
33
|
+
const props = defineProps({
|
|
34
|
+
id: {
|
|
35
|
+
type: String,
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
38
|
+
name: {
|
|
39
|
+
type: String,
|
|
40
|
+
required: true,
|
|
41
|
+
},
|
|
42
|
+
validation: {
|
|
43
|
+
type: String,
|
|
44
|
+
default: null,
|
|
45
|
+
},
|
|
46
|
+
required: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
value: false,
|
|
49
|
+
},
|
|
50
|
+
c12: {
|
|
51
|
+
type: Object as PropType<InpuTextC12>,
|
|
52
|
+
required: true,
|
|
53
|
+
},
|
|
54
|
+
styleClassPassthrough: {
|
|
55
|
+
type: String,
|
|
56
|
+
default: '',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const slots = useSlots();
|
|
61
|
+
const hasLeftContent = computed(() => slots.left !== undefined);
|
|
62
|
+
const hasRightContent = computed(() => slots.right !== undefined);
|
|
63
|
+
|
|
64
|
+
const modelValue = defineModel() as Ref<IFormData>;
|
|
65
|
+
|
|
66
|
+
const updateFocus = (name: string, isFocused: boolean) => {
|
|
67
|
+
console.log(`textarea | updateFocus: ${name} ${isFocused}`);
|
|
68
|
+
modelValue.value.focusedField = isFocused ? name : '';
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isFocused = computed(() => {
|
|
72
|
+
return modelValue.value.focusedField == name.value;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const name = computed(() => {
|
|
76
|
+
return props.name !== null ? props.name : props.id;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const validatorLocale = toRef(useRuntimeConfig().public.validatorLocale);
|
|
80
|
+
|
|
81
|
+
const componentValidation = validationConfig[validatorLocale.value][props.validation];
|
|
82
|
+
const inputField = ref<HTMLInputElement | null>(null);
|
|
83
|
+
|
|
84
|
+
const fieldIsDirty = computed(() => {
|
|
85
|
+
return modelValue.value!.formFieldsC12[name.value].isDirty;
|
|
86
|
+
});
|
|
87
|
+
const fieldHasError = computed(() => {
|
|
88
|
+
return modelValue.value!.submitAttempted && !modelValue.value!.formFieldsC12[name.value].isValid;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// const { fieldHasError } = useFormControl(name.value);
|
|
92
|
+
|
|
93
|
+
const formFieldC12 = <IFormFieldC12>{
|
|
94
|
+
label: props.c12.label,
|
|
95
|
+
placeholder: props.c12.placeholder,
|
|
96
|
+
errorMessage: props.c12.errorMessage,
|
|
97
|
+
useCustomError: false,
|
|
98
|
+
customErrors: {},
|
|
99
|
+
isValid: false,
|
|
100
|
+
isDirty: false,
|
|
101
|
+
type: 'string',
|
|
102
|
+
previousValue: null,
|
|
103
|
+
};
|
|
104
|
+
modelValue.value!.formFieldsC12[name.value] = formFieldC12;
|
|
105
|
+
|
|
106
|
+
const { initFormFieldsC12 } = useFormControl();
|
|
107
|
+
initFormFieldsC12(props.name, formFieldC12);
|
|
108
|
+
|
|
109
|
+
const fieldValue = computed(() => {
|
|
110
|
+
return modelValue.value.data[name.value];
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
watch(fieldValue, () => {
|
|
114
|
+
if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
|
|
115
|
+
modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
|
|
116
|
+
}
|
|
117
|
+
modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
|
|
118
|
+
modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
|
|
119
|
+
|
|
120
|
+
if (modelValue.value!.formFieldsC12[name.value].useCustomError && modelValue.value.data[props.name] === modelValue.value.formFieldsC12[props.name].previousValue) {
|
|
121
|
+
modelValue.value!.validityState[name.value] = false;
|
|
122
|
+
modelValue.value!.formFieldsC12[name.value].isValid = false;
|
|
123
|
+
modelValue.value.displayErrorMessages = true;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const isValid = () => {
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
|
|
130
|
+
modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
|
|
131
|
+
if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
|
|
132
|
+
modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
|
|
133
|
+
}
|
|
134
|
+
}, 0);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
onMounted(() => {
|
|
138
|
+
isValid();
|
|
139
|
+
});
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<style lang="css">
|
|
143
|
+
.input-textarea-wrapper {
|
|
144
|
+
align-items: center;
|
|
145
|
+
display: grid;
|
|
146
|
+
grid-template-columns: 1fr;
|
|
147
|
+
|
|
148
|
+
&.has-left-content {
|
|
149
|
+
grid-template-columns: auto 1fr;
|
|
150
|
+
margin-left: var(--_gutter);
|
|
151
|
+
|
|
152
|
+
.left-content {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
&.has-right-content {
|
|
159
|
+
display: grid;
|
|
160
|
+
grid-template-columns: 1fr auto;
|
|
161
|
+
/* display: flex; */
|
|
162
|
+
margin-right: var(--_gutter);
|
|
163
|
+
|
|
164
|
+
.right-content {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<InputTextareaMaterialCore :type :id :name :required :c12 :styleClassPassthrough :theme v-model="modelValue">
|
|
3
|
+
<template #input>
|
|
4
|
+
<InputTextareaCore :id :name :validation :required v-model="modelValue" :c12 :style-class-passthrough="styleClassPassthrough" />
|
|
5
|
+
</template>
|
|
6
|
+
</InputTextareaMaterialCore>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import type { InpuTextC12, IFormData } from '@/types/types.forms';
|
|
11
|
+
|
|
12
|
+
import propValidators from '../../../c12/prop-validators';
|
|
13
|
+
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
size: {
|
|
16
|
+
type: String as PropType<string>,
|
|
17
|
+
default: 'normal',
|
|
18
|
+
validator(value: string) {
|
|
19
|
+
return propValidators.size.includes(value);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
weight: {
|
|
23
|
+
type: String as PropType<string>,
|
|
24
|
+
default: 'wght-400',
|
|
25
|
+
validator(value: string) {
|
|
26
|
+
return propValidators.weight.includes(value);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
theme: {
|
|
30
|
+
type: String as PropType<string>,
|
|
31
|
+
default: 'primary',
|
|
32
|
+
validator(value: string) {
|
|
33
|
+
return propValidators.theme.includes(value);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
type: {
|
|
37
|
+
// type: String as PropType<"text" | "password" | "tel" | "number" | "email" | "url">, // This breaks props setup in unit tests
|
|
38
|
+
type: String,
|
|
39
|
+
validator(value: string) {
|
|
40
|
+
return ['text', 'password', 'tel', 'number', 'email', 'url'].includes(value);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
id: {
|
|
44
|
+
// type: String as PropType<string>,
|
|
45
|
+
type: String,
|
|
46
|
+
required: true,
|
|
47
|
+
},
|
|
48
|
+
name: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: null,
|
|
51
|
+
},
|
|
52
|
+
validation: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: '',
|
|
55
|
+
},
|
|
56
|
+
required: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
value: false,
|
|
59
|
+
},
|
|
60
|
+
c12: {
|
|
61
|
+
type: Object as PropType<InpuTextC12>,
|
|
62
|
+
required: true,
|
|
63
|
+
},
|
|
64
|
+
styleClassPassthrough: {
|
|
65
|
+
type: String,
|
|
66
|
+
default: '',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const name = computed(() => {
|
|
71
|
+
return props.name !== null ? props.name : props.id;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const modelValue = defineModel() as Ref<IFormData>;
|
|
75
|
+
</script>
|