@brickflow/ui 0.0.56 → 0.0.57
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/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/components/Form/index.vue +46 -1
- package/dist/runtime/components/Form/login.demo.vue +12 -1
- package/dist/runtime/components/Input/index.vue +144 -22
- package/dist/runtime/components/Input/label.demo.vue +15 -3
- package/dist/runtime/components/Input/mask.demo.vue +73 -0
- package/dist/runtime/components/Input/variants.demo.vue +16 -0
- package/dist/runtime/components/InputNumber/denominator.demo.vue +1 -1
- package/dist/runtime/components/InputNumber/index.vue +25 -5
- package/dist/runtime/components/TransitionSize/duration.demo.vue +88 -0
- package/dist/runtime/components/TransitionSize/flex.demo.vue +112 -0
- package/dist/runtime/components/TransitionSize/height.demo.vue +88 -0
- package/dist/runtime/components/TransitionSize/index.vue +69 -27
- package/dist/runtime/components/TransitionSize/width.demo.vue +88 -0
- package/dist/runtime/composables/useBrickflow.d.ts +2 -0
- package/dist/runtime/composables/useFieldRules.d.ts +16 -0
- package/dist/runtime/composables/useFieldRules.js +42 -0
- package/dist/runtime/utils/rules.d.ts +34 -0
- package/dist/runtime/utils/rules.js +68 -0
- package/dist/types.d.mts +2 -0
- package/package.json +2 -1
package/dist/module.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
export { BrickflowConfig } from '../dist/runtime/composables/useBrickflow.js';
|
|
3
3
|
export { BrickflowI18n, BrickflowRouteLocationParam } from '../dist/runtime/composables/useTranslate.js';
|
|
4
|
+
export { Rule, RuleMessages, rule } from '../dist/runtime/utils/rules.js';
|
|
4
5
|
|
|
5
6
|
interface ModuleOptions {
|
|
6
7
|
componentPrefix?: string;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { mkdir, access, readFile, writeFile, readdir } from 'node:fs/promises';
|
|
|
5
5
|
import { join, parse, basename, extname, dirname, resolve, relative } from 'node:path';
|
|
6
6
|
import { isBrickflowUiStyleReference, getBrickflowUiStyleReferencePath, defineBrickflowUi } from '../dist/runtime/tailwind.js';
|
|
7
7
|
import { generateFonts, FontAssetType, OtherAssetType } from 'fantasticon';
|
|
8
|
+
export { rule } from '../dist/runtime/utils/rules.js';
|
|
8
9
|
|
|
9
10
|
const getIconFontCssPath$1 = (outputDir, name) => join(outputDir, `${name}.minify.css`);
|
|
10
11
|
async function generateIcons({ inputDir, name, outputDir }) {
|
|
@@ -977,6 +978,7 @@ const module$1 = defineNuxtModule({
|
|
|
977
978
|
viteConfig.plugins.push(tailwindcss());
|
|
978
979
|
});
|
|
979
980
|
addImports([
|
|
981
|
+
{ from: resolver.resolve("./runtime/utils/rules"), name: "rule" },
|
|
980
982
|
...["provideBrickflow", "useBrickflow", "BrickflowConfig"].map((name) => ({
|
|
981
983
|
from: resolver.resolve("./runtime/composables/useBrickflow"),
|
|
982
984
|
name,
|
|
@@ -1,5 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { provide } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { formRulesKey } from '../../composables/useFieldRules'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
onInvalid?: (event: SubmitEvent) => void
|
|
8
|
+
onSubmit?: (event: SubmitEvent) => void
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const fields = new Set<{ focus: () => void; validate: () => boolean }>()
|
|
12
|
+
provide(formRulesKey, {
|
|
13
|
+
register(field: { focus: () => void; validate: () => boolean }) {
|
|
14
|
+
fields.add(field)
|
|
15
|
+
return () => fields.delete(field)
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function handleSubmit(event: SubmitEvent): void {
|
|
20
|
+
event.preventDefault()
|
|
21
|
+
if (validate()) {
|
|
22
|
+
props.onSubmit?.(event)
|
|
23
|
+
} else {
|
|
24
|
+
props.onInvalid?.(event)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validate(): boolean {
|
|
29
|
+
let firstInvalid: undefined | { focus: () => void }
|
|
30
|
+
for (const field of fields) {
|
|
31
|
+
if (!field.validate() && !firstInvalid) {
|
|
32
|
+
firstInvalid = field
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
firstInvalid?.focus()
|
|
36
|
+
return !firstInvalid
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
defineExpose({ validate })
|
|
40
|
+
</script>
|
|
41
|
+
|
|
1
42
|
<template>
|
|
2
|
-
<form
|
|
43
|
+
<form
|
|
44
|
+
class="flex flex-col gap-4"
|
|
45
|
+
novalidate
|
|
46
|
+
@submit="handleSubmit"
|
|
47
|
+
>
|
|
3
48
|
<slot />
|
|
4
49
|
</form>
|
|
5
50
|
</template>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<script lang="ts" setup>
|
|
8
8
|
import { ref } from 'vue'
|
|
9
9
|
|
|
10
|
+
import { rule } from '../../utils/rules'
|
|
10
11
|
import Button from '../Button/index.vue'
|
|
11
12
|
import Input from '../Input/index.vue'
|
|
12
13
|
import Form from './index.vue'
|
|
@@ -14,6 +15,12 @@
|
|
|
14
15
|
const email = ref('')
|
|
15
16
|
const password = ref('')
|
|
16
17
|
const submittedEmail = ref('')
|
|
18
|
+
const emailRules = [
|
|
19
|
+
rule.required(),
|
|
20
|
+
rule.email(),
|
|
21
|
+
rule.regex(/\.com$/i, () => 'CUSTOM RULE: Use a .com email address'),
|
|
22
|
+
]
|
|
23
|
+
const passwordRules = [rule.required(), rule.minLength(8)]
|
|
17
24
|
|
|
18
25
|
function handleSubmit(): void {
|
|
19
26
|
submittedEmail.value = email.value
|
|
@@ -28,15 +35,19 @@
|
|
|
28
35
|
<Input
|
|
29
36
|
:value="email"
|
|
30
37
|
label="Email"
|
|
31
|
-
placeholder="example@
|
|
38
|
+
placeholder="example@oyes.com"
|
|
32
39
|
type="email"
|
|
40
|
+
:rules="emailRules"
|
|
41
|
+
description="Only .com"
|
|
33
42
|
autocomplete="email"
|
|
34
43
|
@input="email = $event"
|
|
35
44
|
/>
|
|
36
45
|
<Input
|
|
37
46
|
:value="password"
|
|
47
|
+
placeholder="**********"
|
|
38
48
|
label="Password"
|
|
39
49
|
type="password"
|
|
50
|
+
:rules="passwordRules"
|
|
40
51
|
autocomplete="current-password"
|
|
41
52
|
@input="password = $event"
|
|
42
53
|
/>
|
|
@@ -31,10 +31,16 @@
|
|
|
31
31
|
</script>
|
|
32
32
|
|
|
33
33
|
<script setup lang="ts">
|
|
34
|
+
import type { MaskaDetail, MaskInput, MaskInputOptions, MaskType } from 'maska'
|
|
35
|
+
|
|
34
36
|
import { twMerge } from 'tailwind-merge'
|
|
35
|
-
import { computed, ref, shallowRef, useAttrs, useId, useSlots, watch } from 'vue'
|
|
37
|
+
import { computed, nextTick, ref, shallowRef, useAttrs, useId, useSlots, watch } from 'vue'
|
|
38
|
+
|
|
39
|
+
import type { Rule } from '../../utils/rules'
|
|
36
40
|
|
|
41
|
+
import { useFieldRules } from '../../composables/useFieldRules'
|
|
37
42
|
import Icon from '../Icon/index.vue'
|
|
43
|
+
import TransitionSize from '../TransitionSize/index.vue'
|
|
38
44
|
|
|
39
45
|
defineOptions({ inheritAttrs: false })
|
|
40
46
|
|
|
@@ -47,14 +53,17 @@
|
|
|
47
53
|
disabled?: boolean
|
|
48
54
|
icon?: string
|
|
49
55
|
label?: string
|
|
56
|
+
mask?: MaskInputOptions | MaskType
|
|
50
57
|
onBlur?: (value: string, target: HTMLInputElement) => void
|
|
51
58
|
onChange?: (value: string, target: HTMLInputElement) => void
|
|
52
59
|
onFocus?: (value: string, target: HTMLInputElement | null) => void
|
|
53
60
|
onIconClick?: () => void
|
|
54
61
|
onInput?: (value: string, target: HTMLInputElement | null) => void
|
|
62
|
+
onMask?: (detail: MaskaDetail, target: HTMLInputElement) => void
|
|
55
63
|
onTrailingIconClick?: () => void
|
|
56
64
|
placeholder?: string
|
|
57
65
|
readonly?: boolean
|
|
66
|
+
rules?: Rule<string>[]
|
|
58
67
|
size?: InputSize
|
|
59
68
|
trailingIcon?: string
|
|
60
69
|
type?: 'decimal' | 'email' | 'numeric' | 'password' | 'search' | 'tel' | 'text' | 'url'
|
|
@@ -69,14 +78,17 @@
|
|
|
69
78
|
disabled: false,
|
|
70
79
|
icon: undefined,
|
|
71
80
|
label: undefined,
|
|
81
|
+
mask: undefined,
|
|
72
82
|
onBlur: undefined,
|
|
73
83
|
onChange: undefined,
|
|
74
84
|
onFocus: undefined,
|
|
75
85
|
onIconClick: undefined,
|
|
76
86
|
onInput: undefined,
|
|
87
|
+
onMask: undefined,
|
|
77
88
|
onTrailingIconClick: undefined,
|
|
78
89
|
placeholder: '',
|
|
79
90
|
readonly: false,
|
|
91
|
+
rules: undefined,
|
|
80
92
|
size: 'md',
|
|
81
93
|
trailingIcon: undefined,
|
|
82
94
|
type: 'text',
|
|
@@ -86,10 +98,13 @@
|
|
|
86
98
|
)
|
|
87
99
|
|
|
88
100
|
const model = shallowRef('')
|
|
101
|
+
const maskedValue = shallowRef<null | string>(null)
|
|
102
|
+
let maskInput: MaskInput | undefined
|
|
89
103
|
|
|
90
104
|
const attrs = useAttrs()
|
|
91
105
|
const slots = useSlots()
|
|
92
106
|
const generatedId = useId()
|
|
107
|
+
const baseRef = ref<HTMLInputElement | null>(null)
|
|
93
108
|
const inputRef = ref<HTMLInputElement | null>(null)
|
|
94
109
|
const widthMeasureRef = ref<HTMLSpanElement | null>(null)
|
|
95
110
|
const measuredWidth = ref(0)
|
|
@@ -106,12 +121,27 @@
|
|
|
106
121
|
const fieldId = computed(() => String(attrs.id ?? generatedId))
|
|
107
122
|
const descriptionId = computed(() => `${fieldId.value}-description`)
|
|
108
123
|
const currentValue = computed(() => (props.value === undefined ? model.value : (props.value ?? '')))
|
|
109
|
-
const
|
|
124
|
+
const {
|
|
125
|
+
error,
|
|
126
|
+
input: validateInput,
|
|
127
|
+
validate,
|
|
128
|
+
} = useFieldRules(
|
|
129
|
+
currentValue,
|
|
130
|
+
computed(() => props.rules),
|
|
131
|
+
focus,
|
|
132
|
+
)
|
|
133
|
+
const displayValue = computed(() =>
|
|
134
|
+
props.mask === null || props.mask === undefined
|
|
135
|
+
? currentValue.value
|
|
136
|
+
: (maskedValue.value ?? currentValue.value),
|
|
137
|
+
)
|
|
138
|
+
const widthMeasureText = computed(() => displayValue.value || props.placeholder || ' ')
|
|
110
139
|
const isEmptyField = computed(() => {
|
|
111
|
-
return Boolean(
|
|
140
|
+
return Boolean(displayValue.value)
|
|
112
141
|
})
|
|
113
142
|
|
|
114
|
-
const
|
|
143
|
+
const hasError = computed(() => Boolean(error.value))
|
|
144
|
+
const hasDescription = computed(() => Boolean(props.description || slots.description || hasError.value))
|
|
115
145
|
const inputType = computed(() => {
|
|
116
146
|
return props.type === 'password' && !showPassword.value ? 'password' : 'text'
|
|
117
147
|
})
|
|
@@ -132,7 +162,12 @@
|
|
|
132
162
|
])
|
|
133
163
|
const labelClasses = computed(() => twMerge(UI_STYLE.label, sizeClass.value.label, variantClass.value.label))
|
|
134
164
|
const descriptionClasses = computed(() =>
|
|
135
|
-
twMerge(
|
|
165
|
+
twMerge(
|
|
166
|
+
UI_STYLE.description,
|
|
167
|
+
sizeClass.value.description,
|
|
168
|
+
variantClass.value.description,
|
|
169
|
+
hasError.value && UI_STYLE.state.errorDescription,
|
|
170
|
+
),
|
|
136
171
|
)
|
|
137
172
|
const leadingIconClasses = computed(() =>
|
|
138
173
|
twMerge(
|
|
@@ -164,6 +199,64 @@
|
|
|
164
199
|
{ flush: 'post', immediate: true },
|
|
165
200
|
)
|
|
166
201
|
|
|
202
|
+
watch(
|
|
203
|
+
[() => props.mask, inputRef],
|
|
204
|
+
async ([mask, input], _previous, onCleanup) => {
|
|
205
|
+
maskInput?.destroy()
|
|
206
|
+
maskInput = undefined
|
|
207
|
+
maskedValue.value = null
|
|
208
|
+
|
|
209
|
+
if (mask === null || mask === undefined || !input) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let canceled = false
|
|
214
|
+
onCleanup(() => {
|
|
215
|
+
canceled = true
|
|
216
|
+
maskInput?.destroy()
|
|
217
|
+
maskInput = undefined
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const { MaskInput } = await import('maska')
|
|
221
|
+
if (canceled) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const options: MaskInputOptions = typeof mask === 'object' && !Array.isArray(mask) ? mask : { mask }
|
|
226
|
+
const onMaska = options.onMaska
|
|
227
|
+
|
|
228
|
+
maskInput = new MaskInput(input, {
|
|
229
|
+
...options,
|
|
230
|
+
onMaska: (detail) => {
|
|
231
|
+
maskedValue.value = detail.masked
|
|
232
|
+
model.value = detail.masked
|
|
233
|
+
if (Array.isArray(onMaska)) {
|
|
234
|
+
onMaska.forEach((callback) => callback(detail))
|
|
235
|
+
} else {
|
|
236
|
+
onMaska?.(detail)
|
|
237
|
+
}
|
|
238
|
+
props.onMask?.(detail, input)
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
},
|
|
242
|
+
{ flush: 'post', immediate: true },
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
watch(
|
|
246
|
+
() => props.value,
|
|
247
|
+
async () => {
|
|
248
|
+
if (!maskInput || !inputRef.value) {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
maskedValue.value = null
|
|
252
|
+
await nextTick()
|
|
253
|
+
if (maskInput && inputRef.value) {
|
|
254
|
+
maskInput.updateValue(inputRef.value)
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
{ flush: 'post' },
|
|
258
|
+
)
|
|
259
|
+
|
|
167
260
|
function clear(): void {
|
|
168
261
|
setValue('')
|
|
169
262
|
focus()
|
|
@@ -189,30 +282,46 @@
|
|
|
189
282
|
}
|
|
190
283
|
|
|
191
284
|
function handleInput(event: Event): void {
|
|
285
|
+
if (props.mask !== null && props.mask !== undefined && event instanceof CustomEvent) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
192
288
|
setValue((event.target as HTMLInputElement).value)
|
|
193
289
|
}
|
|
194
290
|
|
|
195
291
|
function setValue(value: string): void {
|
|
196
292
|
model.value = value
|
|
293
|
+
validateInput(value)
|
|
294
|
+
if (props.mask !== null && props.mask !== undefined) {
|
|
295
|
+
maskedValue.value = value
|
|
296
|
+
}
|
|
197
297
|
props.onInput?.(value, inputRef.value)
|
|
198
298
|
}
|
|
199
299
|
|
|
200
|
-
defineExpose({ clear, fieldId, focus, hasFocus, inputRef })
|
|
300
|
+
defineExpose({ clear, fieldId, focus, hasFocus, inputRef, validate })
|
|
201
301
|
</script>
|
|
202
302
|
|
|
203
303
|
<template>
|
|
204
|
-
<div
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
304
|
+
<div
|
|
305
|
+
ref="baseRef"
|
|
306
|
+
:class="[classes, $attrs.class]"
|
|
307
|
+
>
|
|
308
|
+
<TransitionSize
|
|
309
|
+
:flexParent="baseRef"
|
|
310
|
+
flexDirection="end"
|
|
209
311
|
>
|
|
210
|
-
<
|
|
211
|
-
|
|
312
|
+
<label
|
|
313
|
+
v-if="props.label || $slots.label"
|
|
314
|
+
:for="fieldId"
|
|
315
|
+
:class="labelClasses"
|
|
316
|
+
>
|
|
317
|
+
<slot name="label">{{ props.label }}</slot>
|
|
318
|
+
</label>
|
|
319
|
+
</TransitionSize>
|
|
212
320
|
|
|
213
321
|
<label
|
|
214
322
|
:for="fieldId"
|
|
215
323
|
:class="fieldWrapClasses"
|
|
324
|
+
:aria-invalid="hasError ? 'true' : undefined"
|
|
216
325
|
:style="props.autoWidth ? { maxWidth: '100%', width: 'fit-content' } : undefined"
|
|
217
326
|
>
|
|
218
327
|
<span
|
|
@@ -244,7 +353,7 @@
|
|
|
244
353
|
v-bind="{ ...$attrs, class: undefined, id: undefined }"
|
|
245
354
|
:id="fieldId"
|
|
246
355
|
ref="inputRef"
|
|
247
|
-
:value="
|
|
356
|
+
:value="displayValue"
|
|
248
357
|
:type="inputType"
|
|
249
358
|
:inputmode="props.type === 'password' ? undefined : props.type"
|
|
250
359
|
:autocomplete="props.autocomplete"
|
|
@@ -252,6 +361,7 @@
|
|
|
252
361
|
:readonly="props.readonly"
|
|
253
362
|
:placeholder="props.placeholder"
|
|
254
363
|
:aria-describedby="hasDescription ? descriptionId : undefined"
|
|
364
|
+
:aria-invalid="hasError ? 'true' : undefined"
|
|
255
365
|
:class="fieldClasses"
|
|
256
366
|
:style="fieldStyle"
|
|
257
367
|
@input="handleInput"
|
|
@@ -307,13 +417,25 @@
|
|
|
307
417
|
</slot>
|
|
308
418
|
</label>
|
|
309
419
|
|
|
310
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
420
|
+
<TransitionSize :flexParent="baseRef">
|
|
421
|
+
<div
|
|
422
|
+
v-if="hasDescription"
|
|
423
|
+
:id="descriptionId"
|
|
424
|
+
:class="descriptionClasses"
|
|
425
|
+
>
|
|
426
|
+
<slot
|
|
427
|
+
v-if="hasError"
|
|
428
|
+
name="error"
|
|
429
|
+
>
|
|
430
|
+
{{ error }}
|
|
431
|
+
</slot>
|
|
432
|
+
<slot
|
|
433
|
+
v-else
|
|
434
|
+
name="description"
|
|
435
|
+
>
|
|
436
|
+
{{ props.description }}
|
|
437
|
+
</slot>
|
|
438
|
+
</div>
|
|
439
|
+
</TransitionSize>
|
|
318
440
|
</div>
|
|
319
441
|
</template>
|
|
@@ -5,16 +5,28 @@
|
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
<script lang="ts" setup>
|
|
8
|
+
import { shallowRef } from 'vue'
|
|
9
|
+
|
|
8
10
|
import Input from './index.vue'
|
|
11
|
+
|
|
12
|
+
const active = shallowRef(true)
|
|
9
13
|
</script>
|
|
10
14
|
|
|
11
15
|
<template>
|
|
12
16
|
<div class="max-w-sm">
|
|
13
17
|
<Input
|
|
14
|
-
label="Email address"
|
|
15
|
-
description="We will use this address to contact you."
|
|
16
|
-
placeholder="name@example.com"
|
|
18
|
+
:label="active ? 'Email address' : undefined"
|
|
19
|
+
:description="active ? 'We will use this address to contact you.' : undefined"
|
|
20
|
+
:placeholder="active ? 'name@example.com' : undefined"
|
|
17
21
|
type="email"
|
|
18
22
|
/>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
class="mt-4 w-30 cursor-pointer rounded-xl px-3 py-2"
|
|
26
|
+
:class="active ? 'bg-blue-500' : 'bg-blue-800'"
|
|
27
|
+
@click="active = !active"
|
|
28
|
+
>
|
|
29
|
+
{{ active ? 'Hide' : 'Show' }}
|
|
30
|
+
</button>
|
|
19
31
|
</div>
|
|
20
32
|
</template>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export const uiDemo = {
|
|
3
|
+
description:
|
|
4
|
+
'Use Maska tokens in the mask prop. Pass an options object for custom tokens or an array for alternative masks.',
|
|
5
|
+
title: 'Mask',
|
|
6
|
+
}
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script lang="ts" setup>
|
|
10
|
+
import { ref } from 'vue'
|
|
11
|
+
|
|
12
|
+
import Input from './index.vue'
|
|
13
|
+
|
|
14
|
+
const phone = ref('')
|
|
15
|
+
const digits = ref('')
|
|
16
|
+
const alternativeMasks = ['###-###', '###-###-###']
|
|
17
|
+
const customMask = {
|
|
18
|
+
mask: 'AA-####',
|
|
19
|
+
tokens: {
|
|
20
|
+
A: { pattern: /[a-z]/i, transform: (character: string) => character.toUpperCase() },
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<div class="flex max-w-sm flex-col gap-5">
|
|
27
|
+
<p class="text-sm text-zinc-400">
|
|
28
|
+
Default tokens:
|
|
29
|
+
<code>#</code>
|
|
30
|
+
is a digit,
|
|
31
|
+
<code>@</code>
|
|
32
|
+
is a letter, and
|
|
33
|
+
<code>*</code>
|
|
34
|
+
is a letter or digit. Other characters are literals; prefix a token with
|
|
35
|
+
<code>!</code>
|
|
36
|
+
to display it literally. See
|
|
37
|
+
<a
|
|
38
|
+
class="text-blue-400 underline"
|
|
39
|
+
href="https://beholdr.github.io/maska/v3/#/tokens"
|
|
40
|
+
target="_blank"
|
|
41
|
+
rel="noopener noreferrer"
|
|
42
|
+
>
|
|
43
|
+
Maska token documentation
|
|
44
|
+
</a>
|
|
45
|
+
.
|
|
46
|
+
</p>
|
|
47
|
+
<Input
|
|
48
|
+
:value="phone"
|
|
49
|
+
mask="+## (###) ###-##-##"
|
|
50
|
+
label="Phone: +## (###) ###-##-##"
|
|
51
|
+
placeholder="+48 (123) 456-78-90"
|
|
52
|
+
type="tel"
|
|
53
|
+
@input="(value) => (phone = value)"
|
|
54
|
+
@mask="(detail) => (digits = detail.unmasked)"
|
|
55
|
+
/>
|
|
56
|
+
<div class="text-sm text-zinc-400">Masked: {{ phone }} · Raw: {{ digits }}</div>
|
|
57
|
+
<Input
|
|
58
|
+
mask="@@-####"
|
|
59
|
+
label="Letters and digits: @@-####"
|
|
60
|
+
placeholder="AB-1234"
|
|
61
|
+
/>
|
|
62
|
+
<Input
|
|
63
|
+
:mask="alternativeMasks"
|
|
64
|
+
label="Alternative lengths: ###-### or ###-###-###"
|
|
65
|
+
placeholder="123-456-789"
|
|
66
|
+
/>
|
|
67
|
+
<Input
|
|
68
|
+
:mask="customMask"
|
|
69
|
+
label="Custom uppercase token: AA-####"
|
|
70
|
+
placeholder="AB-1234"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
@@ -5,11 +5,19 @@
|
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
<script lang="ts" setup>
|
|
8
|
+
import { onMounted, ref } from 'vue'
|
|
9
|
+
|
|
8
10
|
import { uiConfig } from '#brickflow-ui'
|
|
9
11
|
|
|
12
|
+
import { rule } from '../../utils/rules'
|
|
10
13
|
import Input from './index.vue'
|
|
11
14
|
|
|
12
15
|
const inputConfig = uiConfig.input
|
|
16
|
+
const errorInputs = ref<InstanceType<typeof Input>[]>([])
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
errorInputs.value.forEach((input) => input.validate())
|
|
20
|
+
})
|
|
13
21
|
</script>
|
|
14
22
|
|
|
15
23
|
<template>
|
|
@@ -24,6 +32,14 @@
|
|
|
24
32
|
:label="`Variant ${variant}`"
|
|
25
33
|
placeholder="Type here"
|
|
26
34
|
/>
|
|
35
|
+
<Input
|
|
36
|
+
ref="errorInputs"
|
|
37
|
+
:variant="variant"
|
|
38
|
+
:label="`Error ${variant}`"
|
|
39
|
+
:rules="[rule.required(), rule.minLength(4)]"
|
|
40
|
+
description="This description is hidden while the field has an error"
|
|
41
|
+
placeholder="Required field"
|
|
42
|
+
/>
|
|
27
43
|
<Input
|
|
28
44
|
:variant="variant"
|
|
29
45
|
:label="`Disabled ${variant}`"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts" generic="T extends boolean = false">
|
|
2
|
-
import { shallowRef, watch } from 'vue'
|
|
2
|
+
import { computed, ref, shallowRef, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
import type { Rule } from '../../utils/rules'
|
|
3
5
|
|
|
4
6
|
import { isEmpty } from '../../utils/checkup.js'
|
|
5
7
|
import Input from '../Input/index.vue'
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
onTrailingIconClick?: () => void
|
|
29
31
|
placeholder?: string
|
|
30
32
|
readonly?: boolean
|
|
33
|
+
rules?: Rule<null | number>[]
|
|
31
34
|
size?: InputProps['size']
|
|
32
35
|
step?: number
|
|
33
36
|
trailingIcon?: string
|
|
@@ -55,6 +58,7 @@
|
|
|
55
58
|
onTrailingIconClick: undefined,
|
|
56
59
|
placeholder: undefined,
|
|
57
60
|
readonly: false,
|
|
61
|
+
rules: undefined,
|
|
58
62
|
size: undefined,
|
|
59
63
|
step: 1,
|
|
60
64
|
trailingIcon: undefined,
|
|
@@ -64,7 +68,17 @@
|
|
|
64
68
|
|
|
65
69
|
type InputProps = InstanceType<typeof Input>['$props']
|
|
66
70
|
|
|
71
|
+
const inputRef = ref<InstanceType<typeof Input> | null>(null)
|
|
67
72
|
const inputValue = shallowRef(prepareValue(props.value))
|
|
73
|
+
const inputRules = computed(() =>
|
|
74
|
+
props.rules?.map(
|
|
75
|
+
(rule): Rule<string> =>
|
|
76
|
+
(value) =>
|
|
77
|
+
rule(value === '' ? null : Number(value)),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
defineExpose({ validate: () => inputRef.value?.validate() ?? true })
|
|
68
82
|
|
|
69
83
|
watch(
|
|
70
84
|
() => props.value,
|
|
@@ -99,6 +113,10 @@
|
|
|
99
113
|
const allowedRegexp = /^-?\d*(?:\.\d*)?$/
|
|
100
114
|
const validNumberRegexp = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/
|
|
101
115
|
|
|
116
|
+
function emitInput(value: typeof props.value): void {
|
|
117
|
+
props.onInput?.(value)
|
|
118
|
+
}
|
|
119
|
+
|
|
102
120
|
function handleInput(rawValue: string, target: HTMLInputElement | null): void {
|
|
103
121
|
if (!target) {
|
|
104
122
|
return
|
|
@@ -109,7 +127,7 @@
|
|
|
109
127
|
if (isEmpty(nextValue)) {
|
|
110
128
|
if (props.nullable) {
|
|
111
129
|
inputValue.value = ''
|
|
112
|
-
|
|
130
|
+
emitInput(null as typeof props.value)
|
|
113
131
|
}
|
|
114
132
|
return
|
|
115
133
|
}
|
|
@@ -128,7 +146,7 @@
|
|
|
128
146
|
if (isEmpty(nextValue) || !validNumberRegexp.test(nextValue)) {
|
|
129
147
|
if (isEmpty(nextValue) && props.nullable) {
|
|
130
148
|
inputValue.value = ''
|
|
131
|
-
|
|
149
|
+
emitInput(null as typeof props.value)
|
|
132
150
|
}
|
|
133
151
|
return
|
|
134
152
|
}
|
|
@@ -143,7 +161,7 @@
|
|
|
143
161
|
nextValueNumber = props.max
|
|
144
162
|
}
|
|
145
163
|
|
|
146
|
-
|
|
164
|
+
emitInput(nextValueNumber)
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
function handleKeydown(event: KeyboardEvent): void {
|
|
@@ -174,7 +192,7 @@
|
|
|
174
192
|
}
|
|
175
193
|
|
|
176
194
|
target.value = prepareValue(nextValue)
|
|
177
|
-
|
|
195
|
+
emitInput(nextValue)
|
|
178
196
|
}
|
|
179
197
|
|
|
180
198
|
function prepareValue(value: typeof props.value): string {
|
|
@@ -184,6 +202,7 @@
|
|
|
184
202
|
|
|
185
203
|
<template>
|
|
186
204
|
<Input
|
|
205
|
+
ref="inputRef"
|
|
187
206
|
v-bind="{
|
|
188
207
|
...$attrs,
|
|
189
208
|
}"
|
|
@@ -197,6 +216,7 @@
|
|
|
197
216
|
:icon="props.icon"
|
|
198
217
|
:placeholder="props.placeholder"
|
|
199
218
|
:readonly="props.readonly"
|
|
219
|
+
:rules="inputRules"
|
|
200
220
|
:size="props.size"
|
|
201
221
|
:trailingIcon="props.trailingIcon"
|
|
202
222
|
:type="props.denominator === 0 ? 'numeric' : 'decimal'"
|