@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.
Files changed (114) hide show
  1. package/LICENSE +673 -673
  2. package/README.md +41 -40
  3. package/dist/components/Dropdown/DropdownItem.vue.d.ts +1 -0
  4. package/dist/components/Flex/Flex.vue.d.ts +3 -1
  5. package/dist/components/Grid/Grid.vue.d.ts +3 -1
  6. package/dist/components/Tabs/Tabs.vue.d.ts +4 -0
  7. package/dist/style.css +1 -1
  8. package/dist/vui.js +1547 -1534
  9. package/package.json +68 -68
  10. package/src/App.vue +176 -175
  11. package/src/components/Accordion/Accordion.vue +91 -91
  12. package/src/components/Accordion/AccordionGroup.vue +43 -43
  13. package/src/components/Accordion/accordion.scss +81 -80
  14. package/src/components/Alert/Alert.vue +53 -53
  15. package/src/components/Alert/alert.scss +80 -80
  16. package/src/components/Avatar/Avatar.vue +50 -50
  17. package/src/components/Avatar/avatar.scss +52 -52
  18. package/src/components/Badge/Badge.vue +21 -21
  19. package/src/components/Badge/badge.scss +89 -89
  20. package/src/components/Breadcrumbs/BreadcrumbItem.vue +26 -26
  21. package/src/components/Breadcrumbs/Breadcrumbs.vue +33 -33
  22. package/src/components/Breadcrumbs/breadcrumbs.scss +30 -30
  23. package/src/components/Button/Button.vue +90 -90
  24. package/src/components/Button/button.scss +178 -176
  25. package/src/components/ButtonGroup/ButtonGroup.vue +25 -25
  26. package/src/components/ButtonGroup/button-group.scss +51 -51
  27. package/src/components/Calendar/Calendar.vue +63 -60
  28. package/src/components/Calendar/calendar.scss +60 -56
  29. package/src/components/Card/Card.vue +48 -48
  30. package/src/components/Card/card.scss +53 -53
  31. package/src/components/Checkbox/Checkbox.vue +51 -52
  32. package/src/components/Checkbox/checkbox.scss +75 -66
  33. package/src/components/CopyClipboard/CopyClipboard.vue +82 -82
  34. package/src/components/CopyClipboard/copy-clipboard.scss +17 -17
  35. package/src/components/Divider/Divider.vue +44 -44
  36. package/src/components/Divider/divider.scss +35 -35
  37. package/src/components/Drawer/Drawer.vue +97 -97
  38. package/src/components/Drawer/drawer.scss +36 -36
  39. package/src/components/Dropdown/Dropdown.vue +111 -111
  40. package/src/components/Dropdown/DropdownItem.vue +33 -29
  41. package/src/components/Dropdown/DropdownTitle.vue +8 -8
  42. package/src/components/Dropdown/dropdown-item.scss +77 -0
  43. package/src/components/Dropdown/dropdown.scss +39 -117
  44. package/src/components/Flex/Flex.vue +113 -106
  45. package/src/components/Grid/Grid.vue +60 -54
  46. package/src/components/Input/Counter.vue +70 -70
  47. package/src/components/Input/Dropzone.vue +65 -65
  48. package/src/components/Input/File.vue +15 -15
  49. package/src/components/Input/Input.vue +121 -121
  50. package/src/components/Input/Password.vue +47 -47
  51. package/src/components/Input/Textarea.vue +76 -76
  52. package/src/components/Input/input.scss +208 -208
  53. package/src/components/Kbd/Kbd.vue +48 -48
  54. package/src/components/Kbd/KbdGroup.vue +31 -31
  55. package/src/components/Kbd/kbd.scss +18 -18
  56. package/src/components/Modal/Confirm.vue +56 -56
  57. package/src/components/Modal/Modal.vue +91 -91
  58. package/src/components/Modal/modal.scss +49 -49
  59. package/src/components/OTP/OTP.vue +133 -133
  60. package/src/components/OTP/OTPItem.vue +37 -37
  61. package/src/components/OTP/otp.scss +83 -83
  62. package/src/components/Pagination/Pagination.vue +74 -74
  63. package/src/components/Pagination/pagination.ts +78 -78
  64. package/src/components/Popout/Popout.vue +42 -42
  65. package/src/components/Popout/popout.scss +8 -8
  66. package/src/components/Progress/Progress.vue +90 -90
  67. package/src/components/Progress/progress.scss +41 -41
  68. package/src/components/Radio/Radio.vue +36 -36
  69. package/src/components/Radio/RadioGroup.vue +40 -40
  70. package/src/components/Radio/radio.scss +68 -59
  71. package/src/components/Select/Select.vue +180 -180
  72. package/src/components/Select/select.scss +44 -44
  73. package/src/components/Sheet/Sheet.vue +92 -92
  74. package/src/components/Sheet/sheet.scss +60 -60
  75. package/src/components/Sidebar/Sidebar.vue +102 -0
  76. package/src/components/Sidebar/sidebar.scss +123 -0
  77. package/src/components/Skeleton/Skeleton.vue +43 -43
  78. package/src/components/Skeleton/skeleton.scss +14 -14
  79. package/src/components/Spinner/Spinner.vue +42 -42
  80. package/src/components/Spinner/spinner.scss +46 -46
  81. package/src/components/Switch/Switch.vue +30 -30
  82. package/src/components/Switch/switch.scss +60 -52
  83. package/src/components/Table/Cell.vue +23 -23
  84. package/src/components/Table/Header.vue +59 -59
  85. package/src/components/Table/Row.vue +9 -9
  86. package/src/components/Table/SelectAll.vue +23 -23
  87. package/src/components/Table/SelectRow.vue +29 -29
  88. package/src/components/Table/Table.vue +66 -66
  89. package/src/components/Table/table.scss +134 -134
  90. package/src/components/Table/table.ts +244 -244
  91. package/src/components/Tabs/Tab.vue +27 -27
  92. package/src/components/Tabs/Tabs.vue +89 -82
  93. package/src/components/Tabs/tabs.scss +80 -79
  94. package/src/components/Toast/Toasts.vue +47 -47
  95. package/src/components/Toast/toast.scss +41 -41
  96. package/src/components/Toast/toast.ts +68 -68
  97. package/src/components/Tooltip/Tooltip.vue +86 -86
  98. package/src/components/Tooltip/tooltip.scss +4 -4
  99. package/src/index.scss +1 -1
  100. package/src/index.ts +119 -119
  101. package/src/internal/Backdrop/Backdrop.vue +22 -22
  102. package/src/internal/Backdrop/backdrop.scss +28 -28
  103. package/src/main.ts +5 -5
  104. package/src/shared/helpers.ts +74 -74
  105. package/src/shared/types.ts +29 -29
  106. package/src/style/animation.scss +21 -21
  107. package/src/style/core.scss +150 -148
  108. package/src/style/layout.scss +168 -136
  109. package/src/style/media-query.scss +29 -29
  110. package/src/style/reset.scss +135 -135
  111. package/src/style/{fonts.scss → text.scss} +74 -53
  112. package/src/style/tooltip.scss +128 -128
  113. package/src/style/typography.scss +338 -338
  114. 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>