@bagelink/vue 1.15.241 → 1.15.245
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/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import type { BagelInputShellProps } from './bagelInputShell'
|
|
4
4
|
import { useBagelInputShell } from './bagelInputShell'
|
|
5
5
|
|
|
@@ -12,6 +12,8 @@ const props = defineProps<{
|
|
|
12
12
|
color?: string
|
|
13
13
|
} & BagelInputShellProps>()
|
|
14
14
|
|
|
15
|
+
const emit = defineEmits(['update:modelValue', 'complete'])
|
|
16
|
+
|
|
15
17
|
const { shellClass, shellStyle: baseShellStyle } = useBagelInputShell(props)
|
|
16
18
|
|
|
17
19
|
const digitColor = computed(() => {
|
|
@@ -25,88 +27,49 @@ const shellStyle = computed(() => {
|
|
|
25
27
|
if (!color) { return baseShellStyle.value }
|
|
26
28
|
return { ...baseShellStyle.value, '--bgl-otp-digit-color': color }
|
|
27
29
|
})
|
|
28
|
-
const hasValue = computed(() => digits.some(d => d !== undefined && d !== ''))
|
|
29
|
-
|
|
30
|
-
const emit = defineEmits(['update:modelValue', 'complete'])
|
|
31
|
-
const digits = reactive<(number | string | undefined)[]>([])
|
|
32
|
-
const otpCont = ref<HTMLElement>()
|
|
33
|
-
|
|
34
|
-
function handlePaste(event: ClipboardEvent, index: number) {
|
|
35
|
-
event.preventDefault()
|
|
36
|
-
const { clipboardData } = event
|
|
37
|
-
const pastedText = clipboardData?.getData('text').replaceAll(/\D/g, '') // Ensure only numeric digits are pasted
|
|
38
|
-
const pastedDigits = [...(pastedText ?? '')]
|
|
39
|
-
if (!pastedDigits) { return }
|
|
40
|
-
for (let i = 0; i < pastedDigits.length; i++) {
|
|
41
|
-
const digit = pastedDigits[i]
|
|
42
|
-
if (index + i < digits.length) {
|
|
43
|
-
digits[index + i] = digit
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
emitUpdate()
|
|
47
|
-
}
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (event.key === 'Backspace') {
|
|
69
|
-
digits[index] = undefined
|
|
31
|
+
// ── Single-input model ──────────────────────────────────────────────────────
|
|
32
|
+
// One real <input> holds the whole code; the N boxes below are display-only
|
|
33
|
+
// mirrors. A single field is what makes OS SMS OTP autofill work — iOS/web need
|
|
34
|
+
// `autocomplete="one-time-code"` on ONE input (a split field never triggers it),
|
|
35
|
+
// and Android's SMS Retriever pastes the whole code at once.
|
|
36
|
+
const inputEl = ref<HTMLInputElement>()
|
|
37
|
+
|
|
38
|
+
const value = ref<string>(
|
|
39
|
+
(props.default ?? props.modelValue ?? '').replace(/\D/g, '').slice(0, props.digitCount),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// Reflect external modelValue changes (e.g. a parent "reset" back to '').
|
|
43
|
+
watch(() => props.modelValue, (next) => {
|
|
44
|
+
const clean = (next ?? '').replace(/\D/g, '').slice(0, props.digitCount)
|
|
45
|
+
if (clean !== value.value) { value.value = clean }
|
|
46
|
+
})
|
|
70
47
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
emitUpdate()
|
|
76
|
-
return
|
|
77
|
-
}
|
|
48
|
+
const digits = computed(() => {
|
|
49
|
+
const chars = value.value.split('')
|
|
50
|
+
return Array.from({ length: props.digitCount }, (_, i) => chars[i] ?? '')
|
|
51
|
+
})
|
|
78
52
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
53
|
+
const hasValue = computed(() => value.value.length > 0)
|
|
54
|
+
// The "active" box is the next empty slot (or the last one when full).
|
|
55
|
+
const activeIndex = computed(() => Math.min(value.value.length, props.digitCount - 1))
|
|
56
|
+
const focused = ref(false)
|
|
57
|
+
|
|
58
|
+
function setValue(next: string) {
|
|
59
|
+
const clean = next.replace(/\D/g, '').slice(0, props.digitCount)
|
|
60
|
+
value.value = clean
|
|
61
|
+
emit('update:modelValue', clean)
|
|
62
|
+
if (clean.length === props.digitCount) {
|
|
63
|
+
emit('complete', clean)
|
|
90
64
|
}
|
|
91
65
|
}
|
|
92
66
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
digits[i] = props.default.charAt(i)
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
for (let i = 0; i < props.digitCount; i++) {
|
|
99
|
-
digits[i] = undefined
|
|
100
|
-
}
|
|
67
|
+
function onInput(e: Event) {
|
|
68
|
+
setValue((e.target as HTMLInputElement).value)
|
|
101
69
|
}
|
|
102
70
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
if (elem === undefined) {
|
|
106
|
-
return false
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return true
|
|
71
|
+
function focusInput() {
|
|
72
|
+
inputEl.value?.focus()
|
|
110
73
|
}
|
|
111
74
|
</script>
|
|
112
75
|
|
|
@@ -114,14 +77,37 @@ function isDigitsFull() {
|
|
|
114
77
|
<div
|
|
115
78
|
class="otp-input" :class="[shellClass, { 'has-value': hasValue }]" :style="shellStyle"
|
|
116
79
|
tabindex="-1"
|
|
80
|
+
@click="focusInput"
|
|
117
81
|
>
|
|
118
|
-
<div
|
|
82
|
+
<div class="otp_wrap ltr" :class="{ 'has-error': !!error }">
|
|
83
|
+
<!-- The single, real input. Transparent + overlaid on the boxes so OS
|
|
84
|
+
autofill, paste, and typing all target it. -->
|
|
119
85
|
<input
|
|
120
|
-
|
|
121
|
-
class="
|
|
122
|
-
|
|
123
|
-
|
|
86
|
+
ref="inputEl"
|
|
87
|
+
class="otp-hidden-input"
|
|
88
|
+
:value="value"
|
|
89
|
+
type="text"
|
|
90
|
+
inputmode="numeric"
|
|
91
|
+
autocomplete="one-time-code"
|
|
92
|
+
:maxlength="digitCount"
|
|
93
|
+
:autofocus="true"
|
|
94
|
+
name="one-time-code"
|
|
95
|
+
pattern="[0-9]*"
|
|
96
|
+
aria-label="verification code"
|
|
97
|
+
@input="onInput"
|
|
98
|
+
@focus="focused = true"
|
|
99
|
+
@blur="focused = false"
|
|
124
100
|
>
|
|
101
|
+
|
|
102
|
+
<!-- Display-only mirror boxes. -->
|
|
103
|
+
<div
|
|
104
|
+
v-for="(d, ind) in digits" :key="ind"
|
|
105
|
+
class="digit-box"
|
|
106
|
+
:class="{ 'is-active': focused && ind === activeIndex, 'is-filled': d !== '' }"
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
>
|
|
109
|
+
{{ d }}
|
|
110
|
+
</div>
|
|
125
111
|
</div>
|
|
126
112
|
<div v-if="error" class="otp-error-message txt-center txt10 pt-05">
|
|
127
113
|
{{ error }}
|
|
@@ -134,46 +120,61 @@ function isDigitsFull() {
|
|
|
134
120
|
.otp-error-message {
|
|
135
121
|
color: var(--bgl-red);
|
|
136
122
|
}
|
|
137
|
-
.otp_wrap.has-error
|
|
138
|
-
.otp_wrap.has-error textarea {
|
|
123
|
+
.otp_wrap.has-error .digit-box {
|
|
139
124
|
outline: 1px solid var(--bgl-red) !important;
|
|
140
125
|
}
|
|
141
126
|
|
|
142
127
|
.otp_wrap {
|
|
128
|
+
position: relative;
|
|
143
129
|
display: flex;
|
|
144
130
|
flex-direction: row;
|
|
145
131
|
justify-content: center;
|
|
146
132
|
align-items: center;
|
|
133
|
+
cursor: text;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Real input sits on top of the boxes, fully transparent, so taps/focus/autofill
|
|
137
|
+
land on it. The digits render in the boxes below. */
|
|
138
|
+
.otp-hidden-input {
|
|
139
|
+
position: absolute;
|
|
140
|
+
inset: 0;
|
|
141
|
+
width: 100%;
|
|
142
|
+
height: 100%;
|
|
143
|
+
margin: 0;
|
|
144
|
+
padding: 0;
|
|
145
|
+
border: 0;
|
|
146
|
+
background: transparent;
|
|
147
|
+
color: transparent;
|
|
148
|
+
caret-color: transparent;
|
|
149
|
+
text-align: center;
|
|
150
|
+
font-size: 1px;
|
|
151
|
+
letter-spacing: 0;
|
|
152
|
+
z-index: 2;
|
|
153
|
+
outline: none;
|
|
154
|
+
-webkit-tap-highlight-color: transparent;
|
|
147
155
|
}
|
|
156
|
+
.otp-hidden-input::selection { background: transparent; }
|
|
148
157
|
|
|
149
158
|
.digit-box {
|
|
150
159
|
height: 3rem;
|
|
151
|
-
|
|
160
|
+
aspect-ratio: 1;
|
|
161
|
+
flex: 0 0 auto;
|
|
152
162
|
color: var(--bgl-otp-digit-color, inherit);
|
|
153
163
|
border: 1px solid var(--bgl-input-label-active-color, var(--bgl-primary-tint));
|
|
154
|
-
display: inline-
|
|
164
|
+
display: inline-flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
155
167
|
background: var(--bgl-gray-light);
|
|
156
168
|
border-radius: 5px;
|
|
157
169
|
margin: 5px;
|
|
158
170
|
text-align: center;
|
|
159
171
|
font-size: 2.4rem;
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
/* Remove spinner arrows */
|
|
164
|
-
appearance: textfield;
|
|
165
|
-
/* Standardized property */
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/* Remove spinner arrows for Webkit browsers */
|
|
169
|
-
.digit-box::-webkit-outer-spin-button,
|
|
170
|
-
.digit-box::-webkit-inner-spin-button {
|
|
171
|
-
-webkit-appearance: none;
|
|
172
|
-
appearance: none;
|
|
173
|
-
margin: 0;
|
|
172
|
+
font-variant-numeric: tabular-nums;
|
|
173
|
+
user-select: none;
|
|
174
|
+
transition: outline-color 0.2s ease, box-shadow 0.2s ease;
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
.digit-box
|
|
177
|
+
.digit-box.is-active {
|
|
177
178
|
outline: 1px solid var(--bgl-input-label-active-color, var(--bgl-primary));
|
|
178
179
|
filter: drop-shadow(0 0 0.25rem var(--bgl-input-label-active-color, var(--bgl-primary)));
|
|
179
180
|
outline-offset: 1px;
|
|
@@ -181,14 +182,17 @@ outline-offset: 1px;
|
|
|
181
182
|
|
|
182
183
|
/* ── frame variant ──────────────────────────────────────────────────────── */
|
|
183
184
|
|
|
185
|
+
.otp-input {
|
|
186
|
+
width: 100%;
|
|
187
|
+
}
|
|
188
|
+
|
|
184
189
|
.otp-input.frame .digit-box {
|
|
185
190
|
background: transparent;
|
|
186
191
|
outline: 1.5px solid var(--bgl-border-color);
|
|
187
192
|
outline-offset: -1px;
|
|
188
|
-
transition: outline-color 0.2s ease, box-shadow 0.2s ease;
|
|
189
193
|
}
|
|
190
194
|
|
|
191
|
-
.otp-input.frame .digit-box
|
|
195
|
+
.otp-input.frame .digit-box.is-active {
|
|
192
196
|
outline-color: var(--bgl-input-label-active-color, var(--bgl-primary));
|
|
193
197
|
outline-width: 2px;
|
|
194
198
|
box-shadow:
|
|
@@ -204,14 +208,6 @@ outline: 1.5px solid var(--bgl-border-color);
|
|
|
204
208
|
outline-offset: -1px;
|
|
205
209
|
}
|
|
206
210
|
|
|
207
|
-
/* Redundant, already covered above */
|
|
208
|
-
/* .digit-box[type="number"] {
|
|
209
|
-
-moz-appearance: textfield;
|
|
210
|
-
} */
|
|
211
|
-
.relative {
|
|
212
|
-
position: relative !important;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
211
|
@media screen and (max-width: 910px) {
|
|
216
212
|
.digit-box {
|
|
217
213
|
padding: 0.25rem;
|