@bitrix24/b24ui-nuxt 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nuxt/b24ui/advice.ts +52 -0
- package/.nuxt/b24ui/alert.ts +118 -0
- package/.nuxt/b24ui/avatar-group.ts +52 -0
- package/.nuxt/b24ui/avatar.ts +63 -0
- package/.nuxt/b24ui/badge.ts +256 -0
- package/.nuxt/b24ui/button-group.ts +27 -0
- package/.nuxt/b24ui/button.ts +342 -0
- package/.nuxt/b24ui/checkbox.ts +128 -0
- package/.nuxt/b24ui/chip.ts +205 -0
- package/.nuxt/b24ui/container.ts +3 -0
- package/.nuxt/b24ui/content/description-list.ts +62 -0
- package/.nuxt/b24ui/countdown.ts +94 -0
- package/.nuxt/b24ui/form-field.ts +57 -0
- package/.nuxt/b24ui/form.ts +3 -0
- package/.nuxt/b24ui/index.ts +28 -0
- package/.nuxt/b24ui/input.ts +472 -0
- package/.nuxt/b24ui/kbd.ts +31 -0
- package/.nuxt/b24ui/link.ts +20 -0
- package/.nuxt/b24ui/progress.ts +303 -0
- package/.nuxt/b24ui/radio-group.ts +135 -0
- package/.nuxt/b24ui/range.ts +172 -0
- package/.nuxt/b24ui/select.ts +550 -0
- package/.nuxt/b24ui/separator.ts +176 -0
- package/.nuxt/b24ui/skeleton.ts +3 -0
- package/.nuxt/b24ui/switch.ts +134 -0
- package/.nuxt/b24ui/tabs.ts +341 -0
- package/.nuxt/b24ui/textarea.ts +332 -0
- package/.nuxt/b24ui/toast.ts +89 -0
- package/.nuxt/b24ui/toaster.ts +91 -0
- package/.nuxt/b24ui/tooltip.ts +16 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/cli/commands/make/component.mjs +95 -0
- package/cli/commands/make/index.mjs +14 -0
- package/cli/commands/make/locale.mjs +64 -0
- package/cli/index.mjs +15 -0
- package/cli/package.json +13 -0
- package/cli/templates.mjs +240 -0
- package/cli/utils.mjs +31 -0
- package/dist/meta.cjs +23610 -0
- package/dist/meta.d.cts +23608 -0
- package/dist/meta.d.mts +23608 -0
- package/dist/meta.d.ts +23608 -0
- package/dist/meta.mjs +23608 -0
- package/dist/module.cjs +54 -0
- package/dist/module.d.cts +14 -0
- package/dist/module.d.mts +14 -0
- package/dist/module.d.ts +14 -0
- package/dist/module.json +13 -0
- package/dist/module.mjs +51 -0
- package/dist/runtime/components/Advice.vue +115 -0
- package/dist/runtime/components/Alert.vue +136 -0
- package/dist/runtime/components/App.vue +52 -0
- package/dist/runtime/components/Avatar.vue +118 -0
- package/dist/runtime/components/AvatarGroup.vue +99 -0
- package/dist/runtime/components/Badge.vue +114 -0
- package/dist/runtime/components/Button.vue +177 -0
- package/dist/runtime/components/ButtonGroup.vue +58 -0
- package/dist/runtime/components/Checkbox.vue +110 -0
- package/dist/runtime/components/Chip.vue +81 -0
- package/dist/runtime/components/Container.vue +36 -0
- package/dist/runtime/components/Countdown.vue +498 -0
- package/dist/runtime/components/Form.vue +271 -0
- package/dist/runtime/components/FormField.vue +128 -0
- package/dist/runtime/components/Input.vue +224 -0
- package/dist/runtime/components/Kbd.vue +50 -0
- package/dist/runtime/components/Link.vue +219 -0
- package/dist/runtime/components/LinkBase.vue +63 -0
- package/dist/runtime/components/Progress.vue +182 -0
- package/dist/runtime/components/RadioGroup.vue +178 -0
- package/dist/runtime/components/Range.vue +114 -0
- package/dist/runtime/components/Select.vue +328 -0
- package/dist/runtime/components/Separator.vue +82 -0
- package/dist/runtime/components/Skeleton.vue +31 -0
- package/dist/runtime/components/Switch.vue +133 -0
- package/dist/runtime/components/Tabs.vue +127 -0
- package/dist/runtime/components/Textarea.vue +216 -0
- package/dist/runtime/components/Toast.vue +168 -0
- package/dist/runtime/components/Toaster.vue +143 -0
- package/dist/runtime/components/Tooltip.vue +94 -0
- package/dist/runtime/components/content/DescriptionList.vue +220 -0
- package/dist/runtime/composables/defineLocale.d.ts +9 -0
- package/dist/runtime/composables/defineLocale.js +4 -0
- package/dist/runtime/composables/defineShortcuts.d.ts +15 -0
- package/dist/runtime/composables/defineShortcuts.js +135 -0
- package/dist/runtime/composables/useAvatarGroup.d.ts +10 -0
- package/dist/runtime/composables/useAvatarGroup.js +10 -0
- package/dist/runtime/composables/useButtonGroup.d.ts +17 -0
- package/dist/runtime/composables/useButtonGroup.js +10 -0
- package/dist/runtime/composables/useComponentIcons.d.ts +20 -0
- package/dist/runtime/composables/useComponentIcons.js +25 -0
- package/dist/runtime/composables/useFormField.d.ts +42 -0
- package/dist/runtime/composables/useFormField.js +65 -0
- package/dist/runtime/composables/useKbd.d.ts +35 -0
- package/dist/runtime/composables/useKbd.js +52 -0
- package/dist/runtime/composables/useLocale.d.ts +4 -0
- package/dist/runtime/composables/useLocale.js +10 -0
- package/dist/runtime/composables/useToast.d.ts +12 -0
- package/dist/runtime/composables/useToast.js +62 -0
- package/dist/runtime/dictionary/icons.d.ts +20 -0
- package/dist/runtime/dictionary/icons.js +35 -0
- package/dist/runtime/index.css +1 -0
- package/dist/runtime/keyframes.css +1 -0
- package/dist/runtime/locale/en.d.ts +2 -0
- package/dist/runtime/locale/en.js +48 -0
- package/dist/runtime/locale/es.d.ts +2 -0
- package/dist/runtime/locale/es.js +48 -0
- package/dist/runtime/locale/index.d.ts +3 -0
- package/dist/runtime/locale/index.js +3 -0
- package/dist/runtime/locale/ru.d.ts +2 -0
- package/dist/runtime/locale/ru.js +48 -0
- package/dist/runtime/plugins/colors.d.ts +2 -0
- package/dist/runtime/plugins/colors.js +40 -0
- package/dist/runtime/types/app.config.d.ts +6 -0
- package/dist/runtime/types/form.d.ts +84 -0
- package/dist/runtime/types/form.js +12 -0
- package/dist/runtime/types/icons.d.ts +3 -0
- package/dist/runtime/types/icons.js +0 -0
- package/dist/runtime/types/index.d.ts +33 -0
- package/dist/runtime/types/index.js +33 -0
- package/dist/runtime/types/locale.d.ts +50 -0
- package/dist/runtime/types/locale.js +0 -0
- package/dist/runtime/types/utils.d.ts +22 -0
- package/dist/runtime/types/utils.js +0 -0
- package/dist/runtime/utils/form.d.ts +17 -0
- package/dist/runtime/utils/form.js +153 -0
- package/dist/runtime/utils/fuse.d.ts +4 -0
- package/dist/runtime/utils/fuse.js +63 -0
- package/dist/runtime/utils/index.d.ts +6 -0
- package/dist/runtime/utils/index.js +63 -0
- package/dist/runtime/utils/link.d.ts +29 -0
- package/dist/runtime/utils/link.js +4 -0
- package/dist/runtime/utils/locale.d.ts +15 -0
- package/dist/runtime/utils/locale.js +25 -0
- package/dist/runtime/utils/tv.d.ts +1 -0
- package/dist/runtime/utils/tv.js +4 -0
- package/dist/runtime/vue/components/Link.vue +203 -0
- package/dist/runtime/vue/plugins/color-mode.d.ts +4 -0
- package/dist/runtime/vue/plugins/color-mode.js +6 -0
- package/dist/runtime/vue/plugins/head.d.ts +4 -0
- package/dist/runtime/vue/plugins/head.js +6 -0
- package/dist/runtime/vue/stubs.d.ts +15 -0
- package/dist/runtime/vue/stubs.js +27 -0
- package/dist/shared/b24ui-nuxt.CNGvMe2S.mjs +4074 -0
- package/dist/shared/b24ui-nuxt.D22QQtm8.cjs +4079 -0
- package/dist/types.d.mts +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/unplugin.cjs +213 -0
- package/dist/unplugin.d.cts +22 -0
- package/dist/unplugin.d.mts +22 -0
- package/dist/unplugin.d.ts +22 -0
- package/dist/unplugin.mjs +202 -0
- package/dist/vite.cjs +21 -0
- package/dist/vite.d.cts +12 -0
- package/dist/vite.d.mts +12 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.mjs +19 -0
- package/package.json +166 -0
- package/vue-plugin.d.ts +5 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
3
|
+
import _appConfig from '#build/app.config'
|
|
4
|
+
import theme from '#build/b24ui/form'
|
|
5
|
+
import { tv } from '../utils/tv'
|
|
6
|
+
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId } from '../types/form'
|
|
7
|
+
import type { DeepReadonly } from 'vue'
|
|
8
|
+
|
|
9
|
+
const appConfigForm = _appConfig as AppConfig & { b24ui: { form: Partial<typeof theme> } }
|
|
10
|
+
|
|
11
|
+
const form = tv({ extend: tv(theme), ...(appConfigForm.b24ui?.form || {}) })
|
|
12
|
+
|
|
13
|
+
export interface FormProps<T extends object> {
|
|
14
|
+
id?: string | number
|
|
15
|
+
schema?: FormSchema<T>
|
|
16
|
+
state: Partial<T>
|
|
17
|
+
validate?: (state: Partial<T>) => Promise<FormError[]> | FormError[]
|
|
18
|
+
validateOn?: FormInputEvents[]
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
validateOnInputDelay?: number
|
|
21
|
+
class?: any
|
|
22
|
+
onSubmit?: ((event: FormSubmitEvent<T>) => void | Promise<void>) | (() => void | Promise<void>)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FormEmits<T extends object> {
|
|
26
|
+
(e: 'submit', payload: FormSubmitEvent<T>): void
|
|
27
|
+
(e: 'error', payload: FormErrorEvent): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FormSlots {
|
|
31
|
+
default(props?: {}): any
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<script lang="ts" setup generic="T extends object">
|
|
36
|
+
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
|
|
37
|
+
import { useEventBus } from '@vueuse/core'
|
|
38
|
+
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
|
|
39
|
+
import { validateSchema } from '../utils/form'
|
|
40
|
+
import { FormValidationException } from '../types/form'
|
|
41
|
+
|
|
42
|
+
const props = withDefaults(defineProps<FormProps<T>>(), {
|
|
43
|
+
validateOn() {
|
|
44
|
+
return ['input', 'blur', 'change'] as FormInputEvents[]
|
|
45
|
+
},
|
|
46
|
+
validateOnInputDelay: 300
|
|
47
|
+
})
|
|
48
|
+
const emits = defineEmits<FormEmits<T>>()
|
|
49
|
+
defineSlots<FormSlots>()
|
|
50
|
+
|
|
51
|
+
const formId = props.id ?? useId() as string
|
|
52
|
+
|
|
53
|
+
const bus = useEventBus<FormEvent<T>>(`form-${formId}`)
|
|
54
|
+
const parentBus = inject(
|
|
55
|
+
formBusInjectionKey,
|
|
56
|
+
undefined
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
provide(formBusInjectionKey, bus)
|
|
60
|
+
|
|
61
|
+
const nestedForms = ref<Map<string | number, { validate: typeof _validate }>>(new Map())
|
|
62
|
+
|
|
63
|
+
onMounted(async () => {
|
|
64
|
+
bus.on(async (event) => {
|
|
65
|
+
if (event.type === 'attach') {
|
|
66
|
+
nestedForms.value.set(event.formId, { validate: event.validate })
|
|
67
|
+
} else if (event.type === 'detach') {
|
|
68
|
+
nestedForms.value.delete(event.formId)
|
|
69
|
+
} else if (props.validateOn?.includes(event.type)) {
|
|
70
|
+
if (event.type !== 'input') {
|
|
71
|
+
await _validate({ name: event.name, silent: true, nested: false })
|
|
72
|
+
} else if (event.eager || blurredFields.has(event.name)) {
|
|
73
|
+
await _validate({ name: event.name, silent: true, nested: false })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (event.type === 'blur') {
|
|
78
|
+
blurredFields.add(event.name)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (event.type === 'change' || event.type === 'input' || event.type === 'blur' || event.type === 'focus') {
|
|
82
|
+
touchedFields.add(event.name)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (event.type === 'change' || event.type === 'input') {
|
|
86
|
+
dirtyFields.add(event.name)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
onUnmounted(() => {
|
|
92
|
+
bus.reset()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
onMounted(async () => {
|
|
96
|
+
if (parentBus) {
|
|
97
|
+
await nextTick()
|
|
98
|
+
parentBus.emit({ type: 'attach', validate: _validate, formId })
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
onUnmounted(() => {
|
|
103
|
+
if (parentBus) {
|
|
104
|
+
parentBus.emit({ type: 'detach', formId })
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const errors = ref<FormErrorWithId[]>([])
|
|
109
|
+
provide('form-errors', errors)
|
|
110
|
+
|
|
111
|
+
const inputs = ref<{ [P in keyof T]?: { id?: string, pattern?: RegExp } }>({})
|
|
112
|
+
provide(formInputsInjectionKey, inputs as any)
|
|
113
|
+
|
|
114
|
+
const dirtyFields = new Set<keyof T>()
|
|
115
|
+
const touchedFields = new Set<keyof T>()
|
|
116
|
+
const blurredFields = new Set<keyof T>()
|
|
117
|
+
|
|
118
|
+
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
|
119
|
+
return errs.map(err => ({
|
|
120
|
+
...err,
|
|
121
|
+
id: inputs.value[err.name]?.id
|
|
122
|
+
}))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const transformedState = ref<T | null>(null)
|
|
126
|
+
|
|
127
|
+
async function getErrors(): Promise<FormErrorWithId[]> {
|
|
128
|
+
let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
|
|
129
|
+
|
|
130
|
+
if (props.schema) {
|
|
131
|
+
const { errors, result } = await validateSchema(props.state, props.schema as FormSchema<typeof props.state>)
|
|
132
|
+
if (errors) {
|
|
133
|
+
errs = errs.concat(errors)
|
|
134
|
+
} else {
|
|
135
|
+
transformedState.value = result
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return resolveErrorIds(errs)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function _validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise<T | false> {
|
|
143
|
+
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof T)[]
|
|
144
|
+
|
|
145
|
+
const nestedValidatePromises = !names && opts.nested
|
|
146
|
+
? Array.from(nestedForms.value.values()).map(
|
|
147
|
+
({ validate }) => validate(opts).then(() => undefined).catch((error: Error) => {
|
|
148
|
+
if (!(error instanceof FormValidationException)) {
|
|
149
|
+
throw error
|
|
150
|
+
}
|
|
151
|
+
return error
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
: []
|
|
155
|
+
|
|
156
|
+
if (names) {
|
|
157
|
+
const otherErrors = errors.value.filter(error => !names.some((name) => {
|
|
158
|
+
const pattern = inputs.value?.[name]?.pattern
|
|
159
|
+
return name === error.name || (pattern && error.name.match(pattern))
|
|
160
|
+
}))
|
|
161
|
+
|
|
162
|
+
const pathErrors = (await getErrors()).filter(error => names.some((name) => {
|
|
163
|
+
const pattern = inputs.value?.[name]?.pattern
|
|
164
|
+
return name === error.name || (pattern && error.name.match(pattern))
|
|
165
|
+
}))
|
|
166
|
+
|
|
167
|
+
errors.value = otherErrors.concat(pathErrors)
|
|
168
|
+
} else {
|
|
169
|
+
errors.value = await getErrors()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const childErrors = (await Promise.all(nestedValidatePromises)).filter(val => val !== undefined)
|
|
173
|
+
|
|
174
|
+
if (errors.value.length + childErrors.length > 0) {
|
|
175
|
+
if (opts.silent) return false
|
|
176
|
+
throw new FormValidationException(formId, errors.value, childErrors)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (opts.transform) {
|
|
180
|
+
Object.assign(props.state, transformedState.value)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return props.state as T
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const loading = ref(false)
|
|
187
|
+
provide(formLoadingInjectionKey, readonly(loading))
|
|
188
|
+
|
|
189
|
+
async function onSubmitWrapper(payload: Event) {
|
|
190
|
+
loading.value = true
|
|
191
|
+
|
|
192
|
+
const event = payload as FormSubmitEvent<any>
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
event.data = await _validate({ nested: true, transform: true })
|
|
196
|
+
await props.onSubmit?.(event)
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!(error instanceof FormValidationException)) {
|
|
199
|
+
throw error
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const errorEvent: FormErrorEvent = {
|
|
203
|
+
...event,
|
|
204
|
+
errors: error.errors,
|
|
205
|
+
children: error.children
|
|
206
|
+
}
|
|
207
|
+
emits('error', errorEvent)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
loading.value = false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const disabled = computed(() => props.disabled || loading.value)
|
|
214
|
+
|
|
215
|
+
provide(formOptionsInjectionKey, computed(() => ({
|
|
216
|
+
disabled: disabled.value,
|
|
217
|
+
validateOnInputDelay: props.validateOnInputDelay
|
|
218
|
+
})))
|
|
219
|
+
|
|
220
|
+
defineExpose<Form<T>>({
|
|
221
|
+
validate: _validate,
|
|
222
|
+
errors,
|
|
223
|
+
|
|
224
|
+
setErrors(errs: FormError[], name?: keyof T) {
|
|
225
|
+
if (name) {
|
|
226
|
+
errors.value = errors.value
|
|
227
|
+
.filter(error => error.name !== name)
|
|
228
|
+
.concat(resolveErrorIds(errs))
|
|
229
|
+
} else {
|
|
230
|
+
errors.value = resolveErrorIds(errs)
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async submit() {
|
|
235
|
+
await onSubmitWrapper(new Event('submit'))
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
getErrors(name?: keyof T) {
|
|
239
|
+
if (name) {
|
|
240
|
+
return errors.value.filter(err => err.name === name)
|
|
241
|
+
}
|
|
242
|
+
return errors.value
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
clear(name?: string) {
|
|
246
|
+
if (name) {
|
|
247
|
+
errors.value = errors.value.filter(err => err.name !== name)
|
|
248
|
+
} else {
|
|
249
|
+
errors.value = []
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
disabled,
|
|
254
|
+
dirty: computed(() => !!dirtyFields.size),
|
|
255
|
+
|
|
256
|
+
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof T>>,
|
|
257
|
+
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof T>>,
|
|
258
|
+
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof T>>
|
|
259
|
+
})
|
|
260
|
+
</script>
|
|
261
|
+
|
|
262
|
+
<template>
|
|
263
|
+
<component
|
|
264
|
+
:is="parentBus ? 'div' : 'form'"
|
|
265
|
+
:id="formId"
|
|
266
|
+
:class="form({ class: props.class })"
|
|
267
|
+
@submit.prevent="onSubmitWrapper"
|
|
268
|
+
>
|
|
269
|
+
<slot />
|
|
270
|
+
</component>
|
|
271
|
+
</template>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { VariantProps } from 'tailwind-variants'
|
|
3
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
4
|
+
import _appConfig from '#build/app.config'
|
|
5
|
+
import theme from '#build/b24ui/form-field'
|
|
6
|
+
import { tv } from '../utils/tv'
|
|
7
|
+
|
|
8
|
+
const appConfigFormField = _appConfig as AppConfig & { b24ui: { formField: Partial<typeof theme> } }
|
|
9
|
+
|
|
10
|
+
const formField = tv({ extend: tv(theme), ...(appConfigFormField.b24ui?.formField || {}) })
|
|
11
|
+
|
|
12
|
+
type FormFieldVariants = VariantProps<typeof formField>
|
|
13
|
+
|
|
14
|
+
export interface FormFieldProps {
|
|
15
|
+
/**
|
|
16
|
+
* The element or component this component should render as.
|
|
17
|
+
* @defaultValue 'div'
|
|
18
|
+
*/
|
|
19
|
+
as?: any
|
|
20
|
+
/** The name of the FormField. Also used to match form errors. */
|
|
21
|
+
name?: string
|
|
22
|
+
/** A regular expression to match form error names. */
|
|
23
|
+
errorPattern?: RegExp
|
|
24
|
+
label?: string
|
|
25
|
+
description?: string
|
|
26
|
+
help?: string
|
|
27
|
+
error?: string | boolean
|
|
28
|
+
hint?: string
|
|
29
|
+
size?: FormFieldVariants['size']
|
|
30
|
+
required?: boolean
|
|
31
|
+
eagerValidation?: boolean
|
|
32
|
+
validateOnInputDelay?: number
|
|
33
|
+
class?: any
|
|
34
|
+
b24ui?: Partial<typeof formField.slots>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FormFieldSlots {
|
|
38
|
+
label(props: { label?: string }): any
|
|
39
|
+
hint(props: { hint?: string }): any
|
|
40
|
+
description(props: { description?: string }): any
|
|
41
|
+
help(props: { help?: string }): any
|
|
42
|
+
error(props: { error?: string | boolean }): any
|
|
43
|
+
default(props: { error?: string | boolean }): any
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<script setup lang="ts">
|
|
48
|
+
import { computed, ref, inject, provide, type Ref, useId } from 'vue'
|
|
49
|
+
import { Primitive, Label } from 'reka-ui'
|
|
50
|
+
import { formFieldInjectionKey, inputIdInjectionKey } from '../composables/useFormField'
|
|
51
|
+
import type { FormError, FormFieldInjectedOptions } from '../types/form'
|
|
52
|
+
import WarningIcon from '@bitrix24/b24icons-vue/main/WarningIcon'
|
|
53
|
+
|
|
54
|
+
const props = defineProps<FormFieldProps>()
|
|
55
|
+
const slots = defineSlots<FormFieldSlots>()
|
|
56
|
+
|
|
57
|
+
const b24ui = computed(() => formField({
|
|
58
|
+
size: props.size,
|
|
59
|
+
required: props.required,
|
|
60
|
+
useDescription: Boolean(props.description) || !!slots.description
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
|
|
64
|
+
|
|
65
|
+
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern)))?.message)
|
|
66
|
+
|
|
67
|
+
const id = ref(useId())
|
|
68
|
+
// Copies id's initial value to bind aria-attributes such as aria-describedby.
|
|
69
|
+
// This is required for the RadioGroup component which unsets the id value.
|
|
70
|
+
const ariaId = id.value
|
|
71
|
+
|
|
72
|
+
provide(inputIdInjectionKey, id)
|
|
73
|
+
|
|
74
|
+
provide(formFieldInjectionKey, computed(() => ({
|
|
75
|
+
error: error.value,
|
|
76
|
+
name: props.name,
|
|
77
|
+
size: props.size,
|
|
78
|
+
eagerValidation: props.eagerValidation,
|
|
79
|
+
validateOnInputDelay: props.validateOnInputDelay,
|
|
80
|
+
errorPattern: props.errorPattern,
|
|
81
|
+
hint: props.hint,
|
|
82
|
+
description: props.description,
|
|
83
|
+
ariaId
|
|
84
|
+
}) as FormFieldInjectedOptions<FormFieldProps>))
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<Primitive :as="as" :class="b24ui.root({ class: [props.class, props.b24ui?.root] })">
|
|
89
|
+
<div :class="b24ui.wrapper({ class: props.b24ui?.wrapper })">
|
|
90
|
+
<div v-if="label || !!slots.label" :class="b24ui.labelWrapper({ class: props.b24ui?.labelWrapper })">
|
|
91
|
+
<Label :for="id" :class="b24ui.label({ class: props.b24ui?.label })">
|
|
92
|
+
<slot name="label" :label="label">
|
|
93
|
+
{{ label }}
|
|
94
|
+
</slot>
|
|
95
|
+
</Label>
|
|
96
|
+
<span v-if="hint || !!slots.hint" :id="`${ariaId}-hint`" :class="b24ui.hint({ class: props.b24ui?.hint })">
|
|
97
|
+
<slot name="hint" :hint="hint">
|
|
98
|
+
{{ hint }}
|
|
99
|
+
</slot>
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<p v-if="description || !!slots.description" :id="`${ariaId}-description`" :class="b24ui.description({ class: props.b24ui?.description })">
|
|
104
|
+
<slot name="description" :description="description">
|
|
105
|
+
{{ description }}
|
|
106
|
+
</slot>
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div :class="[(label || !!slots.label || description || !!slots.description) && b24ui.container({ class: props.b24ui?.container })]">
|
|
111
|
+
<slot :error="error" />
|
|
112
|
+
|
|
113
|
+
<div v-if="(typeof error === 'string' && error) || !!slots.error" :id="`${ariaId}-error`" :class="b24ui.error({ class: props.b24ui?.error })">
|
|
114
|
+
<slot name="error" :error="error">
|
|
115
|
+
<div class="flex flex-row flex-nowrap gap-0.5">
|
|
116
|
+
<WarningIcon :class="b24ui.errorIcon()" />
|
|
117
|
+
<div>{{ error }}</div>
|
|
118
|
+
</div>
|
|
119
|
+
</slot>
|
|
120
|
+
</div>
|
|
121
|
+
<p v-else-if="help || !!slots.help" :class="b24ui.help({ class: props.b24ui?.help })">
|
|
122
|
+
<slot name="help" :help="help">
|
|
123
|
+
{{ help }}
|
|
124
|
+
</slot>
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
</Primitive>
|
|
128
|
+
</template>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { InputHTMLAttributes } from 'vue'
|
|
3
|
+
import type { VariantProps } from 'tailwind-variants'
|
|
4
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
5
|
+
import _appConfig from '#build/app.config'
|
|
6
|
+
import theme from '#build/b24ui/input'
|
|
7
|
+
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
|
8
|
+
import { tv } from '../utils/tv'
|
|
9
|
+
import type { AvatarProps } from '../types'
|
|
10
|
+
import type { PartialString } from '../types/utils'
|
|
11
|
+
|
|
12
|
+
const appConfigInput = _appConfig as AppConfig & { b24ui: { input: Partial<typeof theme> } }
|
|
13
|
+
|
|
14
|
+
const input = tv({ extend: tv(theme), ...(appConfigInput.b24ui?.input || {}) })
|
|
15
|
+
|
|
16
|
+
type InputVariants = VariantProps<typeof input>
|
|
17
|
+
|
|
18
|
+
export interface InputProps extends UseComponentIconsProps {
|
|
19
|
+
/**
|
|
20
|
+
* The element or component this component should render as.
|
|
21
|
+
* @defaultValue 'div'
|
|
22
|
+
*/
|
|
23
|
+
as?: any
|
|
24
|
+
id?: string
|
|
25
|
+
name?: string
|
|
26
|
+
type?: InputHTMLAttributes['type']
|
|
27
|
+
/** The placeholder text when the input is empty. */
|
|
28
|
+
placeholder?: string
|
|
29
|
+
color?: InputVariants['color']
|
|
30
|
+
size?: InputVariants['size']
|
|
31
|
+
/** Removes padding from input. */
|
|
32
|
+
noPadding?: boolean
|
|
33
|
+
/** removes all borders (rings). */
|
|
34
|
+
noBorder?: boolean
|
|
35
|
+
/** removes all borders (rings) except the bottom one. */
|
|
36
|
+
underline?: boolean
|
|
37
|
+
/** Rounds the corners of the button. */
|
|
38
|
+
rounded?: boolean
|
|
39
|
+
required?: boolean
|
|
40
|
+
autocomplete?: InputHTMLAttributes['autocomplete']
|
|
41
|
+
autofocus?: boolean
|
|
42
|
+
autofocusDelay?: number
|
|
43
|
+
disabled?: boolean
|
|
44
|
+
tag?: string
|
|
45
|
+
tagColor?: InputVariants['tagColor']
|
|
46
|
+
/** Highlight the ring color like a focus state. */
|
|
47
|
+
highlight?: boolean
|
|
48
|
+
class?: any
|
|
49
|
+
b24ui?: PartialString<typeof input.slots>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface InputEmits {
|
|
53
|
+
(e: 'update:modelValue', payload: string | number): void
|
|
54
|
+
(e: 'blur', event: FocusEvent): void
|
|
55
|
+
(e: 'change', event: Event): void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface InputSlots {
|
|
59
|
+
leading(props?: {}): any
|
|
60
|
+
default(props?: {}): any
|
|
61
|
+
trailing(props?: {}): any
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<script setup lang="ts">
|
|
66
|
+
import { ref, computed, onMounted } from 'vue'
|
|
67
|
+
import { Primitive } from 'reka-ui'
|
|
68
|
+
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
69
|
+
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
70
|
+
import { useFormField } from '../composables/useFormField'
|
|
71
|
+
import { looseToNumber } from '../utils'
|
|
72
|
+
import B24Avatar from './Avatar.vue'
|
|
73
|
+
|
|
74
|
+
defineOptions({ inheritAttrs: false })
|
|
75
|
+
|
|
76
|
+
const props = withDefaults(defineProps<InputProps>(), {
|
|
77
|
+
type: 'text',
|
|
78
|
+
autocomplete: 'off',
|
|
79
|
+
autofocusDelay: 0
|
|
80
|
+
})
|
|
81
|
+
const emits = defineEmits<InputEmits>()
|
|
82
|
+
const slots = defineSlots<InputSlots>()
|
|
83
|
+
|
|
84
|
+
const [modelValue, modelModifiers] = defineModel<string | number>()
|
|
85
|
+
|
|
86
|
+
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true })
|
|
87
|
+
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
|
88
|
+
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
|
|
89
|
+
|
|
90
|
+
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
|
|
91
|
+
|
|
92
|
+
const isTag = computed(() => {
|
|
93
|
+
return props.tag
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const b24ui = computed(() => input({
|
|
97
|
+
type: props.type as InputVariants['type'],
|
|
98
|
+
color: color.value,
|
|
99
|
+
size: inputSize?.value,
|
|
100
|
+
loading: props.loading,
|
|
101
|
+
tagColor: props.tagColor,
|
|
102
|
+
highlight: highlight.value,
|
|
103
|
+
rounded: Boolean(props.rounded),
|
|
104
|
+
noPadding: Boolean(props.noPadding),
|
|
105
|
+
noBorder: Boolean(props.noBorder),
|
|
106
|
+
underline: Boolean(props.underline),
|
|
107
|
+
leading: Boolean(isLeading.value || !!props.avatar || !!slots.leading),
|
|
108
|
+
trailing: Boolean(isTrailing.value || !!slots.trailing),
|
|
109
|
+
buttonGroup: orientation.value
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
113
|
+
|
|
114
|
+
function autoFocus() {
|
|
115
|
+
if (props.autofocus) {
|
|
116
|
+
inputRef.value?.focus()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Custom function to handle the v-model properties
|
|
121
|
+
function updateInput(value: string) {
|
|
122
|
+
if (modelModifiers.trim) {
|
|
123
|
+
value = value.trim()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (modelModifiers.number || props.type === 'number') {
|
|
127
|
+
value = looseToNumber(value)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
modelValue.value = value
|
|
131
|
+
emitFormInput()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function onInput(event: Event) {
|
|
135
|
+
if (!modelModifiers.lazy) {
|
|
136
|
+
updateInput((event.target as HTMLInputElement).value)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function onChange(event: Event) {
|
|
141
|
+
const value = (event.target as HTMLInputElement).value
|
|
142
|
+
|
|
143
|
+
if (modelModifiers.lazy) {
|
|
144
|
+
updateInput(value)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
|
|
148
|
+
if (modelModifiers.trim) {
|
|
149
|
+
(event.target as HTMLInputElement).value = value.trim()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
emitFormChange()
|
|
153
|
+
emits('change', event)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onBlur(event: FocusEvent) {
|
|
157
|
+
emitFormBlur()
|
|
158
|
+
emits('blur', event)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
defineExpose({
|
|
162
|
+
inputRef
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
onMounted(() => {
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
autoFocus()
|
|
168
|
+
}, props.autofocusDelay)
|
|
169
|
+
})
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<template>
|
|
173
|
+
<Primitive :as="as" :class="b24ui.root({ class: [props.class, props.b24ui?.root] })">
|
|
174
|
+
<div v-if="isTag" :class="b24ui.tag({ class: props.b24ui?.tag })">
|
|
175
|
+
{{ props.tag }}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<input
|
|
179
|
+
:id="id"
|
|
180
|
+
ref="inputRef"
|
|
181
|
+
:type="type"
|
|
182
|
+
:value="modelValue"
|
|
183
|
+
:name="name"
|
|
184
|
+
:placeholder="placeholder"
|
|
185
|
+
:class="b24ui.base({ class: props.b24ui?.base })"
|
|
186
|
+
:disabled="disabled"
|
|
187
|
+
:required="required"
|
|
188
|
+
:autocomplete="autocomplete"
|
|
189
|
+
v-bind="{ ...$attrs, ...ariaAttrs }"
|
|
190
|
+
@input="onInput"
|
|
191
|
+
@blur="onBlur"
|
|
192
|
+
@change="onChange"
|
|
193
|
+
@focus="emitFormFocus"
|
|
194
|
+
>
|
|
195
|
+
|
|
196
|
+
<slot />
|
|
197
|
+
|
|
198
|
+
<span v-if="isLeading || !!avatar || !!slots.leading" :class="b24ui.leading({ class: props.b24ui?.leading })">
|
|
199
|
+
<slot name="leading">
|
|
200
|
+
<Component
|
|
201
|
+
:is="leadingIconName"
|
|
202
|
+
v-if="isLeading && leadingIconName"
|
|
203
|
+
:class="b24ui.leadingIcon({ class: props.b24ui?.leadingIcon })"
|
|
204
|
+
/>
|
|
205
|
+
<B24Avatar
|
|
206
|
+
v-else-if="!!avatar"
|
|
207
|
+
:size="((props.b24ui?.leadingAvatarSize || b24ui.leadingAvatarSize()) as AvatarProps['size'])"
|
|
208
|
+
v-bind="avatar"
|
|
209
|
+
:class="b24ui.leadingAvatar({ class: props.b24ui?.leadingAvatar })"
|
|
210
|
+
/>
|
|
211
|
+
</slot>
|
|
212
|
+
</span>
|
|
213
|
+
|
|
214
|
+
<span v-if="isTrailing || !!slots.trailing" :class="b24ui.trailing({ class: props.b24ui?.trailing })">
|
|
215
|
+
<slot name="trailing">
|
|
216
|
+
<Component
|
|
217
|
+
:is="trailingIconName"
|
|
218
|
+
v-if="trailingIconName"
|
|
219
|
+
:class="b24ui.trailingIcon({ class: props.b24ui?.trailingIcon })"
|
|
220
|
+
/>
|
|
221
|
+
</slot>
|
|
222
|
+
</span>
|
|
223
|
+
</Primitive>
|
|
224
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { VariantProps } from 'tailwind-variants'
|
|
3
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
4
|
+
import _appConfig from '#build/app.config'
|
|
5
|
+
import theme from '#build/b24ui/kbd'
|
|
6
|
+
import type { KbdKey } from '../composables/useKbd'
|
|
7
|
+
import { tv } from '../utils/tv'
|
|
8
|
+
|
|
9
|
+
const appConfigKbd = _appConfig as AppConfig & { b24ui: { kbd: Partial<typeof theme> } }
|
|
10
|
+
|
|
11
|
+
const kbd = tv({ extend: tv(theme), ...(appConfigKbd.b24ui?.kbd || {}) })
|
|
12
|
+
|
|
13
|
+
type KbdVariants = VariantProps<typeof kbd>
|
|
14
|
+
|
|
15
|
+
export interface KbdProps {
|
|
16
|
+
/**
|
|
17
|
+
* The element or component this component should render as.
|
|
18
|
+
* @defaultValue 'kbd'
|
|
19
|
+
*/
|
|
20
|
+
as?: any
|
|
21
|
+
value?: KbdKey | string
|
|
22
|
+
depth?: KbdVariants['depth']
|
|
23
|
+
size?: KbdVariants['size']
|
|
24
|
+
class?: any
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface KbdSlots {
|
|
28
|
+
default(props?: {}): any
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import { Primitive } from 'reka-ui'
|
|
34
|
+
import { useKbd } from '../composables/useKbd'
|
|
35
|
+
|
|
36
|
+
const props = withDefaults(defineProps<KbdProps>(), {
|
|
37
|
+
as: 'kbd'
|
|
38
|
+
})
|
|
39
|
+
defineSlots<KbdSlots>()
|
|
40
|
+
|
|
41
|
+
const { getKbdKey } = useKbd()
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<Primitive :as="as" :class="kbd({ depth, size, class: props.class })">
|
|
46
|
+
<slot>
|
|
47
|
+
{{ getKbdKey(value) }}
|
|
48
|
+
</slot>
|
|
49
|
+
</Primitive>
|
|
50
|
+
</template>
|