@dolanske/vui 0.3.4 → 0.5.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 +673 -673
- package/README.md +41 -40
- package/dist/components/Dropdown/DropdownItem.vue.d.ts +1 -0
- package/dist/components/Flex/Flex.vue.d.ts +3 -1
- package/dist/components/Grid/Grid.vue.d.ts +3 -1
- package/dist/components/Tabs/Tabs.vue.d.ts +4 -0
- package/dist/style.css +1 -1
- package/dist/vui.js +1547 -1534
- package/package.json +68 -68
- package/src/App.vue +176 -175
- package/src/components/Accordion/Accordion.vue +91 -91
- package/src/components/Accordion/AccordionGroup.vue +43 -43
- package/src/components/Accordion/accordion.scss +81 -80
- package/src/components/Alert/Alert.vue +53 -53
- package/src/components/Alert/alert.scss +80 -80
- package/src/components/Avatar/Avatar.vue +50 -50
- package/src/components/Avatar/avatar.scss +52 -52
- package/src/components/Badge/Badge.vue +21 -21
- package/src/components/Badge/badge.scss +89 -89
- package/src/components/Breadcrumbs/BreadcrumbItem.vue +26 -26
- package/src/components/Breadcrumbs/Breadcrumbs.vue +33 -33
- package/src/components/Breadcrumbs/breadcrumbs.scss +30 -30
- package/src/components/Button/Button.vue +90 -90
- package/src/components/Button/button.scss +178 -176
- package/src/components/ButtonGroup/ButtonGroup.vue +25 -25
- package/src/components/ButtonGroup/button-group.scss +51 -51
- package/src/components/Calendar/Calendar.vue +63 -60
- package/src/components/Calendar/calendar.scss +60 -56
- package/src/components/Card/Card.vue +48 -48
- package/src/components/Card/card.scss +53 -53
- package/src/components/Checkbox/Checkbox.vue +51 -52
- package/src/components/Checkbox/checkbox.scss +75 -66
- package/src/components/CopyClipboard/CopyClipboard.vue +82 -82
- package/src/components/CopyClipboard/copy-clipboard.scss +17 -17
- package/src/components/Divider/Divider.vue +44 -44
- package/src/components/Divider/divider.scss +35 -35
- package/src/components/Drawer/Drawer.vue +97 -97
- package/src/components/Drawer/drawer.scss +36 -36
- package/src/components/Dropdown/Dropdown.vue +111 -111
- package/src/components/Dropdown/DropdownItem.vue +33 -29
- package/src/components/Dropdown/DropdownTitle.vue +8 -8
- package/src/components/Dropdown/dropdown-item.scss +77 -0
- package/src/components/Dropdown/dropdown.scss +39 -117
- package/src/components/Flex/Flex.vue +113 -106
- package/src/components/Grid/Grid.vue +60 -54
- package/src/components/Input/Counter.vue +70 -70
- package/src/components/Input/Dropzone.vue +65 -65
- package/src/components/Input/File.vue +15 -15
- package/src/components/Input/Input.vue +121 -121
- package/src/components/Input/Password.vue +47 -47
- package/src/components/Input/Textarea.vue +76 -76
- package/src/components/Input/input.scss +208 -208
- package/src/components/Kbd/Kbd.vue +48 -48
- package/src/components/Kbd/KbdGroup.vue +31 -31
- package/src/components/Kbd/kbd.scss +18 -18
- package/src/components/Modal/Confirm.vue +56 -56
- package/src/components/Modal/Modal.vue +91 -91
- package/src/components/Modal/modal.scss +49 -49
- package/src/components/OTP/OTP.vue +133 -133
- package/src/components/OTP/OTPItem.vue +37 -37
- package/src/components/OTP/otp.scss +83 -83
- package/src/components/Pagination/Pagination.vue +74 -74
- package/src/components/Pagination/pagination.ts +78 -78
- package/src/components/Popout/Popout.vue +42 -42
- package/src/components/Popout/popout.scss +8 -8
- package/src/components/Progress/Progress.vue +90 -90
- package/src/components/Progress/progress.scss +41 -41
- package/src/components/Radio/Radio.vue +36 -36
- package/src/components/Radio/RadioGroup.vue +40 -40
- package/src/components/Radio/radio.scss +68 -59
- package/src/components/Select/Select.vue +180 -180
- package/src/components/Select/select.scss +44 -44
- package/src/components/Sheet/Sheet.vue +92 -92
- package/src/components/Sheet/sheet.scss +60 -60
- package/src/components/Sidebar/Sidebar.vue +102 -0
- package/src/components/Sidebar/sidebar.scss +123 -0
- package/src/components/Skeleton/Skeleton.vue +43 -43
- package/src/components/Skeleton/skeleton.scss +14 -14
- package/src/components/Spinner/Spinner.vue +42 -42
- package/src/components/Spinner/spinner.scss +46 -46
- package/src/components/Switch/Switch.vue +30 -30
- package/src/components/Switch/switch.scss +60 -52
- package/src/components/Table/Cell.vue +23 -23
- package/src/components/Table/Header.vue +59 -59
- package/src/components/Table/Row.vue +9 -9
- package/src/components/Table/SelectAll.vue +23 -23
- package/src/components/Table/SelectRow.vue +29 -29
- package/src/components/Table/Table.vue +66 -66
- package/src/components/Table/table.scss +134 -134
- package/src/components/Table/table.ts +244 -244
- package/src/components/Tabs/Tab.vue +27 -27
- package/src/components/Tabs/Tabs.vue +89 -82
- package/src/components/Tabs/tabs.scss +80 -79
- package/src/components/Toast/Toasts.vue +47 -47
- package/src/components/Toast/toast.scss +41 -41
- package/src/components/Toast/toast.ts +68 -68
- package/src/components/Tooltip/Tooltip.vue +86 -86
- package/src/components/Tooltip/tooltip.scss +4 -4
- package/src/index.scss +1 -1
- package/src/index.ts +119 -119
- package/src/internal/Backdrop/Backdrop.vue +22 -22
- package/src/internal/Backdrop/backdrop.scss +28 -28
- package/src/main.ts +5 -5
- package/src/shared/helpers.ts +74 -74
- package/src/shared/types.ts +29 -29
- package/src/style/animation.scss +21 -21
- package/src/style/core.scss +150 -148
- package/src/style/layout.scss +168 -136
- package/src/style/media-query.scss +29 -29
- package/src/style/reset.scss +135 -135
- package/src/style/{fonts.scss → text.scss} +74 -53
- package/src/style/tooltip.scss +128 -128
- package/src/style/typography.scss +338 -338
- package/src/style/utils.scss +36 -36
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
<script setup lang='ts'>
|
|
2
|
-
import type { Variants } from '../Button/Button.vue'
|
|
3
|
-
import type { ModalProps } from './Modal.vue'
|
|
4
|
-
import Button from '../Button/Button.vue'
|
|
5
|
-
import Flex from '../Flex/Flex.vue'
|
|
6
|
-
import Modal from './Modal.vue'
|
|
7
|
-
|
|
8
|
-
type Props = {
|
|
9
|
-
title?: string
|
|
10
|
-
content?: string
|
|
11
|
-
confirmText?: string
|
|
12
|
-
confirmVariant?: Variants
|
|
13
|
-
cancelText?: string
|
|
14
|
-
showCancel?: boolean
|
|
15
|
-
} & Partial<ModalProps>
|
|
16
|
-
|
|
17
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
18
|
-
cancelText: 'Cancel',
|
|
19
|
-
confirmText: 'Ok',
|
|
20
|
-
size: 's',
|
|
21
|
-
canDismiss: true,
|
|
22
|
-
showCancel: true,
|
|
23
|
-
confirmVariant: 'default',
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
const emits = defineEmits<{
|
|
27
|
-
cancel: []
|
|
28
|
-
confirm: []
|
|
29
|
-
}>()
|
|
30
|
-
|
|
31
|
-
const open = defineModel<boolean>()
|
|
32
|
-
</script>
|
|
33
|
-
|
|
34
|
-
<template>
|
|
35
|
-
<pre>{{ $props }}</pre>
|
|
36
|
-
<Modal
|
|
37
|
-
v-bind="props"
|
|
38
|
-
v-model="open"
|
|
39
|
-
>
|
|
40
|
-
<template #default>
|
|
41
|
-
<div class="typeset">
|
|
42
|
-
<slot />
|
|
43
|
-
</div>
|
|
44
|
-
</template>
|
|
45
|
-
<template #footer>
|
|
46
|
-
<Flex justify-end>
|
|
47
|
-
<Button v-if="props.showCancel" plain @click="emits('cancel'), open = false">
|
|
48
|
-
{{ props.cancelText }}
|
|
49
|
-
</Button>
|
|
50
|
-
<Button :variant="props.confirmVariant" @click="emits('confirm'), open = false">
|
|
51
|
-
{{ props.confirmText }}
|
|
52
|
-
</Button>
|
|
53
|
-
</Flex>
|
|
54
|
-
</template>
|
|
55
|
-
</Modal>
|
|
56
|
-
</template>
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import type { Variants } from '../Button/Button.vue'
|
|
3
|
+
import type { ModalProps } from './Modal.vue'
|
|
4
|
+
import Button from '../Button/Button.vue'
|
|
5
|
+
import Flex from '../Flex/Flex.vue'
|
|
6
|
+
import Modal from './Modal.vue'
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
title?: string
|
|
10
|
+
content?: string
|
|
11
|
+
confirmText?: string
|
|
12
|
+
confirmVariant?: Variants
|
|
13
|
+
cancelText?: string
|
|
14
|
+
showCancel?: boolean
|
|
15
|
+
} & Partial<ModalProps>
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
18
|
+
cancelText: 'Cancel',
|
|
19
|
+
confirmText: 'Ok',
|
|
20
|
+
size: 's',
|
|
21
|
+
canDismiss: true,
|
|
22
|
+
showCancel: true,
|
|
23
|
+
confirmVariant: 'default',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const emits = defineEmits<{
|
|
27
|
+
cancel: []
|
|
28
|
+
confirm: []
|
|
29
|
+
}>()
|
|
30
|
+
|
|
31
|
+
const open = defineModel<boolean>()
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<pre>{{ $props }}</pre>
|
|
36
|
+
<Modal
|
|
37
|
+
v-bind="props"
|
|
38
|
+
v-model="open"
|
|
39
|
+
>
|
|
40
|
+
<template #default>
|
|
41
|
+
<div class="typeset">
|
|
42
|
+
<slot />
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
<template #footer>
|
|
46
|
+
<Flex justify-end>
|
|
47
|
+
<Button v-if="props.showCancel" plain @click="emits('cancel'), open = false">
|
|
48
|
+
{{ props.cancelText }}
|
|
49
|
+
</Button>
|
|
50
|
+
<Button :variant="props.confirmVariant" @click="emits('confirm'), open = false">
|
|
51
|
+
{{ props.confirmText }}
|
|
52
|
+
</Button>
|
|
53
|
+
</Flex>
|
|
54
|
+
</template>
|
|
55
|
+
</Modal>
|
|
56
|
+
</template>
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
<script setup lang='ts'>
|
|
2
|
-
import type { Sizes } from '../../shared/types'
|
|
3
|
-
import type { Props as CardProps } from '../Card/Card.vue'
|
|
4
|
-
import { useAttrs } from 'vue'
|
|
5
|
-
import Backdrop from '../../internal/Backdrop/Backdrop.vue'
|
|
6
|
-
import Button from '../Button/Button.vue'
|
|
7
|
-
import Card from '../Card/Card.vue'
|
|
8
|
-
import './modal.scss'
|
|
9
|
-
|
|
10
|
-
export interface ModalProps {
|
|
11
|
-
size?: Sizes | 'full'
|
|
12
|
-
/**
|
|
13
|
-
* Modal wraps a floating card. You can optinally pass in any props you'd pass
|
|
14
|
-
* into the <Card /> component.
|
|
15
|
-
*/
|
|
16
|
-
card?: CardProps
|
|
17
|
-
/**
|
|
18
|
-
* Modal will not overflow the screen, but its card's content will be scrollable instead.
|
|
19
|
-
*/
|
|
20
|
-
scrollable?: boolean
|
|
21
|
-
/**
|
|
22
|
-
* Modal appears in the center of the screen
|
|
23
|
-
*/
|
|
24
|
-
centered?: boolean
|
|
25
|
-
/**
|
|
26
|
-
* Wether modal can be closed by clicking the X button
|
|
27
|
-
*/
|
|
28
|
-
canDismiss?: boolean
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const {
|
|
32
|
-
size = 'm',
|
|
33
|
-
card = {},
|
|
34
|
-
scrollable,
|
|
35
|
-
centered,
|
|
36
|
-
canDismiss = true,
|
|
37
|
-
} = defineProps<ModalProps>()
|
|
38
|
-
|
|
39
|
-
const open = defineModel<boolean>()
|
|
40
|
-
|
|
41
|
-
function close() {
|
|
42
|
-
open.value = false
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const attrs = useAttrs()
|
|
46
|
-
</script>
|
|
47
|
-
|
|
48
|
-
<template>
|
|
49
|
-
<Teleport to="body">
|
|
50
|
-
<Transition appear name="modal">
|
|
51
|
-
<Backdrop v-if="open" @close="close">
|
|
52
|
-
<div class="vui-modal" :class="[`vui-modal-size-${size}`, { scrollable, centered }]" v-bind="attrs">
|
|
53
|
-
<Button
|
|
54
|
-
v-if="canDismiss"
|
|
55
|
-
class="vui-modal-close"
|
|
56
|
-
plain
|
|
57
|
-
square
|
|
58
|
-
icon="ph:x"
|
|
59
|
-
@click="open = false"
|
|
60
|
-
/>
|
|
61
|
-
<Card v-bind="card">
|
|
62
|
-
<template v-if="$slots.header" #header>
|
|
63
|
-
<slot name="header" :close />
|
|
64
|
-
</template>
|
|
65
|
-
<template v-if="$slots.default" #default>
|
|
66
|
-
<div>
|
|
67
|
-
<slot name="default" :close />
|
|
68
|
-
</div>
|
|
69
|
-
</template>
|
|
70
|
-
<template v-if="$slots.footer" #footer>
|
|
71
|
-
<slot name="footer" :close />
|
|
72
|
-
</template>
|
|
73
|
-
</Card>
|
|
74
|
-
</div>
|
|
75
|
-
</Backdrop>
|
|
76
|
-
</Transition>
|
|
77
|
-
</Teleport>
|
|
78
|
-
</template>
|
|
79
|
-
|
|
80
|
-
<style scoped>
|
|
81
|
-
.modal-enter-active,
|
|
82
|
-
.modal-leave-active {
|
|
83
|
-
transition: var(--transition);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.modal-enter-from,
|
|
87
|
-
.modal-leave-to {
|
|
88
|
-
opacity: 0;
|
|
89
|
-
transform: scale(0.95);
|
|
90
|
-
}
|
|
91
|
-
</style>
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import type { Sizes } from '../../shared/types'
|
|
3
|
+
import type { Props as CardProps } from '../Card/Card.vue'
|
|
4
|
+
import { useAttrs } from 'vue'
|
|
5
|
+
import Backdrop from '../../internal/Backdrop/Backdrop.vue'
|
|
6
|
+
import Button from '../Button/Button.vue'
|
|
7
|
+
import Card from '../Card/Card.vue'
|
|
8
|
+
import './modal.scss'
|
|
9
|
+
|
|
10
|
+
export interface ModalProps {
|
|
11
|
+
size?: Sizes | 'full'
|
|
12
|
+
/**
|
|
13
|
+
* Modal wraps a floating card. You can optinally pass in any props you'd pass
|
|
14
|
+
* into the <Card /> component.
|
|
15
|
+
*/
|
|
16
|
+
card?: CardProps
|
|
17
|
+
/**
|
|
18
|
+
* Modal will not overflow the screen, but its card's content will be scrollable instead.
|
|
19
|
+
*/
|
|
20
|
+
scrollable?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* Modal appears in the center of the screen
|
|
23
|
+
*/
|
|
24
|
+
centered?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* Wether modal can be closed by clicking the X button
|
|
27
|
+
*/
|
|
28
|
+
canDismiss?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
size = 'm',
|
|
33
|
+
card = {},
|
|
34
|
+
scrollable,
|
|
35
|
+
centered,
|
|
36
|
+
canDismiss = true,
|
|
37
|
+
} = defineProps<ModalProps>()
|
|
38
|
+
|
|
39
|
+
const open = defineModel<boolean>()
|
|
40
|
+
|
|
41
|
+
function close() {
|
|
42
|
+
open.value = false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const attrs = useAttrs()
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<Teleport to="body">
|
|
50
|
+
<Transition appear name="modal">
|
|
51
|
+
<Backdrop v-if="open" @close="close">
|
|
52
|
+
<div class="vui-modal" :class="[`vui-modal-size-${size}`, { scrollable, centered }]" v-bind="attrs">
|
|
53
|
+
<Button
|
|
54
|
+
v-if="canDismiss"
|
|
55
|
+
class="vui-modal-close"
|
|
56
|
+
plain
|
|
57
|
+
square
|
|
58
|
+
icon="ph:x"
|
|
59
|
+
@click="open = false"
|
|
60
|
+
/>
|
|
61
|
+
<Card v-bind="card">
|
|
62
|
+
<template v-if="$slots.header" #header>
|
|
63
|
+
<slot name="header" :close />
|
|
64
|
+
</template>
|
|
65
|
+
<template v-if="$slots.default" #default>
|
|
66
|
+
<div>
|
|
67
|
+
<slot name="default" :close />
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
<template v-if="$slots.footer" #footer>
|
|
71
|
+
<slot name="footer" :close />
|
|
72
|
+
</template>
|
|
73
|
+
</Card>
|
|
74
|
+
</div>
|
|
75
|
+
</Backdrop>
|
|
76
|
+
</Transition>
|
|
77
|
+
</Teleport>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<style scoped>
|
|
81
|
+
.modal-enter-active,
|
|
82
|
+
.modal-leave-active {
|
|
83
|
+
transition: var(--transition);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.modal-enter-from,
|
|
87
|
+
.modal-leave-to {
|
|
88
|
+
opacity: 0;
|
|
89
|
+
transform: scale(0.95);
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
.vui-modal {
|
|
2
|
-
width: 100%;
|
|
3
|
-
margin-inline: auto;
|
|
4
|
-
height: 100%;
|
|
5
|
-
position: relative;
|
|
6
|
-
|
|
7
|
-
.vui-modal-close {
|
|
8
|
-
position: absolute;
|
|
9
|
-
top: 16px;
|
|
10
|
-
right: 16px;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
&.centered {
|
|
14
|
-
display: flex;
|
|
15
|
-
align-items: center;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
&.vui-modal-size-s {
|
|
19
|
-
max-width: 440px;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
&.vui-modal-size-m {
|
|
23
|
-
max-width: 620px;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
&.vui-modal-size-l {
|
|
27
|
-
max-width: 728px;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
&.scrollable {
|
|
31
|
-
& > .vui-card {
|
|
32
|
-
display: flex;
|
|
33
|
-
flex-direction: column;
|
|
34
|
-
inline-size: 100%;
|
|
35
|
-
max-height: 100%;
|
|
36
|
-
|
|
37
|
-
.vui-card-content {
|
|
38
|
-
flex: 1 1 0%;
|
|
39
|
-
overflow-y: auto;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
& > .vui-card {
|
|
45
|
-
width: 100%;
|
|
46
|
-
margin-bottom: var(--backdrop-offset);
|
|
47
|
-
box-shadow: var(--box-shadow-strong);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
1
|
+
.vui-modal {
|
|
2
|
+
width: 100%;
|
|
3
|
+
margin-inline: auto;
|
|
4
|
+
height: 100%;
|
|
5
|
+
position: relative;
|
|
6
|
+
|
|
7
|
+
.vui-modal-close {
|
|
8
|
+
position: absolute;
|
|
9
|
+
top: 16px;
|
|
10
|
+
right: 16px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&.centered {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
&.vui-modal-size-s {
|
|
19
|
+
max-width: 440px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&.vui-modal-size-m {
|
|
23
|
+
max-width: 620px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&.vui-modal-size-l {
|
|
27
|
+
max-width: 728px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&.scrollable {
|
|
31
|
+
& > .vui-card {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
inline-size: 100%;
|
|
35
|
+
max-height: 100%;
|
|
36
|
+
|
|
37
|
+
.vui-card-content {
|
|
38
|
+
flex: 1 1 0%;
|
|
39
|
+
overflow-y: auto;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
& > .vui-card {
|
|
45
|
+
width: 100%;
|
|
46
|
+
margin-bottom: var(--backdrop-offset);
|
|
47
|
+
box-shadow: var(--box-shadow-strong);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
<script setup lang='ts'>
|
|
2
|
-
import type { ModelRef, Ref } from 'vue'
|
|
3
|
-
import { computed, provide, ref, toRef, useTemplateRef, watch } from 'vue'
|
|
4
|
-
import { setCharAt } from '../../shared/helpers'
|
|
5
|
-
import './otp.scss'
|
|
6
|
-
|
|
7
|
-
export interface OtpContext {
|
|
8
|
-
otpValue: ModelRef<string>
|
|
9
|
-
cursorIndex: Ref<number>
|
|
10
|
-
redacted: Ref<boolean>
|
|
11
|
-
register: () => void
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface Props {
|
|
15
|
-
mode?: 'num' | 'char' | 'both'
|
|
16
|
-
redacted?: boolean
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const {
|
|
20
|
-
mode = 'both',
|
|
21
|
-
redacted = false,
|
|
22
|
-
} = defineProps<Props>()
|
|
23
|
-
|
|
24
|
-
const emits = defineEmits<{
|
|
25
|
-
change: [value?: string]
|
|
26
|
-
complete: [value: string]
|
|
27
|
-
}>()
|
|
28
|
-
|
|
29
|
-
const otpValue = defineModel<string>({
|
|
30
|
-
default: '',
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
const cursorIndex = ref<number>(-1)
|
|
34
|
-
const regexNumbers = '^\\d+$'
|
|
35
|
-
const regexChars = '^[a-z]+$'
|
|
36
|
-
const regexBoth = '^[a-z0-9]+$'
|
|
37
|
-
|
|
38
|
-
const pattern = computed(() => {
|
|
39
|
-
if (mode === 'num')
|
|
40
|
-
return new RegExp(regexNumbers)
|
|
41
|
-
else if (mode === 'char')
|
|
42
|
-
return new RegExp(regexChars, 'i')
|
|
43
|
-
else return new RegExp(regexBoth, 'i')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const maxLen = ref(0)
|
|
47
|
-
|
|
48
|
-
const input = useTemplateRef('inputRef')
|
|
49
|
-
|
|
50
|
-
provide('otp-context', {
|
|
51
|
-
otpValue,
|
|
52
|
-
cursorIndex,
|
|
53
|
-
redacted: toRef(() => redacted),
|
|
54
|
-
// Called by all OTPItem child components to properly set max length of the input.
|
|
55
|
-
register: () => maxLen.value++,
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
watch(otpValue, value => emits('change', value))
|
|
59
|
-
|
|
60
|
-
function setOtpValue(value: string) {
|
|
61
|
-
otpValue.value = value
|
|
62
|
-
if (input.value) {
|
|
63
|
-
input.value.value = value
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function updateValue(e: KeyboardEvent) {
|
|
68
|
-
const key = e.key
|
|
69
|
-
|
|
70
|
-
// Capping at length 0 prevents all non-character keyboard inputs
|
|
71
|
-
if (pattern.value.test(key) && key.length === 1) {
|
|
72
|
-
const newValue = setCharAt(otpValue.value, key, cursorIndex.value)
|
|
73
|
-
|
|
74
|
-
if (newValue.length <= maxLen.value) {
|
|
75
|
-
setOtpValue(newValue)
|
|
76
|
-
|
|
77
|
-
if (cursorIndex.value < maxLen.value - 1)
|
|
78
|
-
cursorIndex.value++
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else if (key === 'ArrowLeft' && cursorIndex.value > 0) {
|
|
82
|
-
cursorIndex.value--
|
|
83
|
-
}
|
|
84
|
-
else if (key === 'ArrowRight' && cursorIndex.value < otpValue.value.length) {
|
|
85
|
-
cursorIndex.value++
|
|
86
|
-
}
|
|
87
|
-
else if (key === 'Backspace') {
|
|
88
|
-
// If we press backspace multiple times make sure to traverse back by 1
|
|
89
|
-
if (otpValue.value.charAt(cursorIndex.value) === '' && cursorIndex.value > 0) {
|
|
90
|
-
cursorIndex.value--
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const newValue = setCharAt(otpValue.value, '', cursorIndex.value)
|
|
94
|
-
setOtpValue(newValue)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function handlePaste(e: any) {
|
|
99
|
-
const clipboard = e.clipboardData?.getData('text/plain')
|
|
100
|
-
if (clipboard) {
|
|
101
|
-
const clipboardTrim = clipboard.trim().slice(0, maxLen.value - cursorIndex.value)
|
|
102
|
-
|
|
103
|
-
if (!pattern.value.test(clipboardTrim)) {
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const currentTrimStart = otpValue.value.slice(0, cursorIndex.value)
|
|
108
|
-
const currentTrimEnd = otpValue.value.slice(cursorIndex.value + clipboardTrim.length)
|
|
109
|
-
const newValue = (currentTrimStart + clipboardTrim + currentTrimEnd).trim()
|
|
110
|
-
setOtpValue(newValue)
|
|
111
|
-
cursorIndex.value = Math.min(newValue.length, maxLen.value - 1)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
</script>
|
|
115
|
-
|
|
116
|
-
<template>
|
|
117
|
-
<div class="vui-otp">
|
|
118
|
-
<input
|
|
119
|
-
ref="inputRef"
|
|
120
|
-
type="text"
|
|
121
|
-
class="vui-otp-input"
|
|
122
|
-
contenteditable="true"
|
|
123
|
-
@keydown="updateValue"
|
|
124
|
-
@blur="cursorIndex = -1"
|
|
125
|
-
@focus="cursorIndex = Math.min(otpValue.length, maxLen - 1)"
|
|
126
|
-
@paste="handlePaste"
|
|
127
|
-
>
|
|
128
|
-
|
|
129
|
-
<div class="vui-otp-items">
|
|
130
|
-
<slot />
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
</template>
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import type { ModelRef, Ref } from 'vue'
|
|
3
|
+
import { computed, provide, ref, toRef, useTemplateRef, watch } from 'vue'
|
|
4
|
+
import { setCharAt } from '../../shared/helpers'
|
|
5
|
+
import './otp.scss'
|
|
6
|
+
|
|
7
|
+
export interface OtpContext {
|
|
8
|
+
otpValue: ModelRef<string>
|
|
9
|
+
cursorIndex: Ref<number>
|
|
10
|
+
redacted: Ref<boolean>
|
|
11
|
+
register: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
mode?: 'num' | 'char' | 'both'
|
|
16
|
+
redacted?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
mode = 'both',
|
|
21
|
+
redacted = false,
|
|
22
|
+
} = defineProps<Props>()
|
|
23
|
+
|
|
24
|
+
const emits = defineEmits<{
|
|
25
|
+
change: [value?: string]
|
|
26
|
+
complete: [value: string]
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const otpValue = defineModel<string>({
|
|
30
|
+
default: '',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const cursorIndex = ref<number>(-1)
|
|
34
|
+
const regexNumbers = '^\\d+$'
|
|
35
|
+
const regexChars = '^[a-z]+$'
|
|
36
|
+
const regexBoth = '^[a-z0-9]+$'
|
|
37
|
+
|
|
38
|
+
const pattern = computed(() => {
|
|
39
|
+
if (mode === 'num')
|
|
40
|
+
return new RegExp(regexNumbers)
|
|
41
|
+
else if (mode === 'char')
|
|
42
|
+
return new RegExp(regexChars, 'i')
|
|
43
|
+
else return new RegExp(regexBoth, 'i')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const maxLen = ref(0)
|
|
47
|
+
|
|
48
|
+
const input = useTemplateRef('inputRef')
|
|
49
|
+
|
|
50
|
+
provide('otp-context', {
|
|
51
|
+
otpValue,
|
|
52
|
+
cursorIndex,
|
|
53
|
+
redacted: toRef(() => redacted),
|
|
54
|
+
// Called by all OTPItem child components to properly set max length of the input.
|
|
55
|
+
register: () => maxLen.value++,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
watch(otpValue, value => emits('change', value))
|
|
59
|
+
|
|
60
|
+
function setOtpValue(value: string) {
|
|
61
|
+
otpValue.value = value
|
|
62
|
+
if (input.value) {
|
|
63
|
+
input.value.value = value
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function updateValue(e: KeyboardEvent) {
|
|
68
|
+
const key = e.key
|
|
69
|
+
|
|
70
|
+
// Capping at length 0 prevents all non-character keyboard inputs
|
|
71
|
+
if (pattern.value.test(key) && key.length === 1) {
|
|
72
|
+
const newValue = setCharAt(otpValue.value, key, cursorIndex.value)
|
|
73
|
+
|
|
74
|
+
if (newValue.length <= maxLen.value) {
|
|
75
|
+
setOtpValue(newValue)
|
|
76
|
+
|
|
77
|
+
if (cursorIndex.value < maxLen.value - 1)
|
|
78
|
+
cursorIndex.value++
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (key === 'ArrowLeft' && cursorIndex.value > 0) {
|
|
82
|
+
cursorIndex.value--
|
|
83
|
+
}
|
|
84
|
+
else if (key === 'ArrowRight' && cursorIndex.value < otpValue.value.length) {
|
|
85
|
+
cursorIndex.value++
|
|
86
|
+
}
|
|
87
|
+
else if (key === 'Backspace') {
|
|
88
|
+
// If we press backspace multiple times make sure to traverse back by 1
|
|
89
|
+
if (otpValue.value.charAt(cursorIndex.value) === '' && cursorIndex.value > 0) {
|
|
90
|
+
cursorIndex.value--
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const newValue = setCharAt(otpValue.value, '', cursorIndex.value)
|
|
94
|
+
setOtpValue(newValue)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handlePaste(e: any) {
|
|
99
|
+
const clipboard = e.clipboardData?.getData('text/plain')
|
|
100
|
+
if (clipboard) {
|
|
101
|
+
const clipboardTrim = clipboard.trim().slice(0, maxLen.value - cursorIndex.value)
|
|
102
|
+
|
|
103
|
+
if (!pattern.value.test(clipboardTrim)) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const currentTrimStart = otpValue.value.slice(0, cursorIndex.value)
|
|
108
|
+
const currentTrimEnd = otpValue.value.slice(cursorIndex.value + clipboardTrim.length)
|
|
109
|
+
const newValue = (currentTrimStart + clipboardTrim + currentTrimEnd).trim()
|
|
110
|
+
setOtpValue(newValue)
|
|
111
|
+
cursorIndex.value = Math.min(newValue.length, maxLen.value - 1)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="vui-otp">
|
|
118
|
+
<input
|
|
119
|
+
ref="inputRef"
|
|
120
|
+
type="text"
|
|
121
|
+
class="vui-otp-input"
|
|
122
|
+
contenteditable="true"
|
|
123
|
+
@keydown="updateValue"
|
|
124
|
+
@blur="cursorIndex = -1"
|
|
125
|
+
@focus="cursorIndex = Math.min(otpValue.length, maxLen - 1)"
|
|
126
|
+
@paste="handlePaste"
|
|
127
|
+
>
|
|
128
|
+
|
|
129
|
+
<div class="vui-otp-items">
|
|
130
|
+
<slot />
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|