digitojs 1.0.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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +753 -0
- package/dist/adapters/alpine.d.ts +71 -0
- package/dist/adapters/alpine.d.ts.map +1 -0
- package/dist/adapters/alpine.js +560 -0
- package/dist/adapters/alpine.js.map +1 -0
- package/dist/adapters/react.d.ts +223 -0
- package/dist/adapters/react.d.ts.map +1 -0
- package/dist/adapters/react.js +337 -0
- package/dist/adapters/react.js.map +1 -0
- package/dist/adapters/svelte.d.ts +139 -0
- package/dist/adapters/svelte.d.ts.map +1 -0
- package/dist/adapters/svelte.js +295 -0
- package/dist/adapters/svelte.js.map +1 -0
- package/dist/adapters/vanilla.d.ts +110 -0
- package/dist/adapters/vanilla.d.ts.map +1 -0
- package/dist/adapters/vanilla.js +650 -0
- package/dist/adapters/vanilla.js.map +1 -0
- package/dist/adapters/vue.d.ts +163 -0
- package/dist/adapters/vue.d.ts.map +1 -0
- package/dist/adapters/vue.js +298 -0
- package/dist/adapters/vue.js.map +1 -0
- package/dist/adapters/web-component.d.ts +192 -0
- package/dist/adapters/web-component.d.ts.map +1 -0
- package/dist/adapters/web-component.js +832 -0
- package/dist/adapters/web-component.js.map +1 -0
- package/dist/core/feedback.d.ts +26 -0
- package/dist/core/feedback.d.ts.map +1 -0
- package/dist/core/feedback.js +47 -0
- package/dist/core/feedback.js.map +1 -0
- package/dist/core/filter.d.ts +24 -0
- package/dist/core/filter.d.ts.map +1 -0
- package/dist/core/filter.js +47 -0
- package/dist/core/filter.js.map +1 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +15 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/machine.d.ts +67 -0
- package/dist/core/machine.d.ts.map +1 -0
- package/dist/core/machine.js +328 -0
- package/dist/core/machine.js.map +1 -0
- package/dist/core/timer.d.ts +24 -0
- package/dist/core/timer.d.ts.map +1 -0
- package/dist/core/timer.js +67 -0
- package/dist/core/timer.js.map +1 -0
- package/dist/core/types.d.ts +162 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -0
- package/dist/digito-wc.min.js +254 -0
- package/dist/digito-wc.min.js.map +7 -0
- package/dist/digito.min.js +91 -0
- package/dist/digito.min.js.map +7 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/package.json +109 -0
- package/src/adapters/alpine.ts +666 -0
- package/src/adapters/react.tsx +603 -0
- package/src/adapters/svelte.ts +444 -0
- package/src/adapters/vanilla.ts +810 -0
- package/src/adapters/vue.ts +462 -0
- package/src/adapters/web-component.ts +858 -0
- package/src/core/feedback.ts +44 -0
- package/src/core/filter.ts +48 -0
- package/src/core/index.ts +16 -0
- package/src/core/machine.ts +373 -0
- package/src/core/timer.ts +75 -0
- package/src/core/types.ts +167 -0
- package/src/index.ts +51 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* digito/vue
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* Vue 3 adapter — useOTP composable (single hidden-input architecture)
|
|
5
|
+
*
|
|
6
|
+
* @author Olawale Balo — Product Designer + Design Engineer
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
ref,
|
|
12
|
+
computed,
|
|
13
|
+
watch,
|
|
14
|
+
onMounted,
|
|
15
|
+
onUnmounted,
|
|
16
|
+
isRef,
|
|
17
|
+
type Ref,
|
|
18
|
+
} from 'vue'
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
createDigito,
|
|
22
|
+
createTimer,
|
|
23
|
+
filterString,
|
|
24
|
+
type DigitoOptions,
|
|
25
|
+
type InputType,
|
|
26
|
+
} from '../core/index.js'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// TYPES
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extended options for the Vue useOTP composable.
|
|
35
|
+
* Adds controlled-input, separator, and disabled support on top of DigitoOptions.
|
|
36
|
+
*/
|
|
37
|
+
export type VueOTPOptions = DigitoOptions & {
|
|
38
|
+
/**
|
|
39
|
+
* Controlled value — pre-fills and drives the slot state from outside the composable.
|
|
40
|
+
*
|
|
41
|
+
* Reactive mode (recommended): pass a Ref<string>. The composable watches it
|
|
42
|
+
* via Vue's reactivity system — changes propagate automatically, making it
|
|
43
|
+
* fully equivalent to React's controlled-input pattern.
|
|
44
|
+
* ```ts
|
|
45
|
+
* const code = ref('')
|
|
46
|
+
* const otp = useOTP({ value: code, length: 6 })
|
|
47
|
+
* // Clearing from parent:
|
|
48
|
+
* code.value = ''
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* Static mode: pass a plain string to pre-fill slots once on creation.
|
|
52
|
+
* Subsequent changes to the string will NOT be reactive (composables run
|
|
53
|
+
* once during setup()). Use reset() or the Ref pattern for runtime updates.
|
|
54
|
+
*/
|
|
55
|
+
value?: string | Ref<string>
|
|
56
|
+
/**
|
|
57
|
+
* Fires exactly ONCE per user interaction with the current joined code string.
|
|
58
|
+
* Receives partial values too — not just when the code is complete.
|
|
59
|
+
*/
|
|
60
|
+
onChange?: (code: string) => void
|
|
61
|
+
/**
|
|
62
|
+
* Insert a purely visual separator after this slot index (0-based).
|
|
63
|
+
* Accepts a single position or an array for multiple separators.
|
|
64
|
+
* aria-hidden, never part of the value, no effect on the state machine.
|
|
65
|
+
* Default: 0 (no separator).
|
|
66
|
+
* @example separatorAfter: 3 -> [*][*][*] — [*][*][*]
|
|
67
|
+
* @example separatorAfter: [2, 4] -> [*][*] — [*][*] — [*][*]
|
|
68
|
+
*/
|
|
69
|
+
separatorAfter?: number | number[]
|
|
70
|
+
/**
|
|
71
|
+
* The character or string to render as the separator.
|
|
72
|
+
* Default: '—'
|
|
73
|
+
*/
|
|
74
|
+
separator?: string
|
|
75
|
+
/**
|
|
76
|
+
* When `true`, slot templates should display a mask glyph instead of the real
|
|
77
|
+
* character. The hidden input switches to `type="password"` via `hiddenInputAttrs`.
|
|
78
|
+
*
|
|
79
|
+
* `getCode()` and `onComplete` always return real characters.
|
|
80
|
+
* Use for PIN entry or any sensitive input flow.
|
|
81
|
+
*
|
|
82
|
+
* Default: `false`.
|
|
83
|
+
*/
|
|
84
|
+
masked?: boolean
|
|
85
|
+
/**
|
|
86
|
+
* The glyph displayed in filled slots when `masked` is `true`.
|
|
87
|
+
* Returned as a reactive `Ref<string>` so templates can bind to it directly.
|
|
88
|
+
*
|
|
89
|
+
* Default: `'●'` (U+25CF BLACK CIRCLE).
|
|
90
|
+
* @example maskChar: '*'
|
|
91
|
+
*/
|
|
92
|
+
maskChar?: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type UseOTPResult = {
|
|
96
|
+
/** Current value of each slot. Empty string = unfilled. */
|
|
97
|
+
slotValues: Ref<string[]>
|
|
98
|
+
/** Index of the currently active slot. */
|
|
99
|
+
activeSlot: Ref<number>
|
|
100
|
+
/** Computed joined code string. */
|
|
101
|
+
value: Ref<string>
|
|
102
|
+
/** True when every slot is filled. */
|
|
103
|
+
isComplete: Ref<boolean>
|
|
104
|
+
/** True when error state is active. */
|
|
105
|
+
hasError: Ref<boolean>
|
|
106
|
+
/** True when the field is disabled. Mirrors the disabled option. */
|
|
107
|
+
isDisabled: Ref<boolean>
|
|
108
|
+
/** Remaining timer seconds. */
|
|
109
|
+
timerSeconds: Ref<number>
|
|
110
|
+
/** True while the hidden input has browser focus. */
|
|
111
|
+
isFocused: Ref<boolean>
|
|
112
|
+
/** The separator slot index/indices for template rendering. */
|
|
113
|
+
separatorAfter: Ref<number | number[]>
|
|
114
|
+
/** The separator character/string to render. */
|
|
115
|
+
separator: Ref<string>
|
|
116
|
+
/**
|
|
117
|
+
* Whether masked mode is enabled. When true, templates should display
|
|
118
|
+
* `maskChar.value` instead of the real character. `getCode()` still returns real chars.
|
|
119
|
+
*/
|
|
120
|
+
masked: Ref<boolean>
|
|
121
|
+
/**
|
|
122
|
+
* The configured mask glyph. Use in templates instead of a hard-coded `●`:
|
|
123
|
+
* `{{ masked.value && char ? maskChar.value : char }}`
|
|
124
|
+
*/
|
|
125
|
+
maskChar: Ref<string>
|
|
126
|
+
/** The placeholder character for empty slots. Empty string when not set. */
|
|
127
|
+
placeholder: string
|
|
128
|
+
/** Ref to bind to the hidden input element via :ref. */
|
|
129
|
+
inputRef: Ref<HTMLInputElement | null>
|
|
130
|
+
/** Attribute object to spread onto the hidden input via v-bind. */
|
|
131
|
+
hiddenInputAttrs: Ref<Record<string, unknown>>
|
|
132
|
+
/** Returns the current joined code string. */
|
|
133
|
+
getCode: () => string
|
|
134
|
+
/** Clear all slots, restart timer, return focus to input. */
|
|
135
|
+
reset: () => void
|
|
136
|
+
/** Apply or clear the error state. */
|
|
137
|
+
setError: (isError: boolean) => void
|
|
138
|
+
/** Programmatically move focus to a slot index. */
|
|
139
|
+
focus: (slotIndex: number) => void
|
|
140
|
+
/** Event handlers to bind on the hidden input. */
|
|
141
|
+
onKeydown: (e: KeyboardEvent) => void
|
|
142
|
+
onChange: (e: Event) => void
|
|
143
|
+
onPaste: (e: ClipboardEvent) => void
|
|
144
|
+
onFocus: () => void
|
|
145
|
+
onBlur: () => void
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
// COMPOSABLE
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Vue 3 composable for OTP input — single hidden-input architecture.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```vue
|
|
158
|
+
* <script setup>
|
|
159
|
+
* import { useOTP } from 'digito/vue'
|
|
160
|
+
* const otp = useOTP({ length: 6, onComplete: (code) => verify(code) })
|
|
161
|
+
* </script>
|
|
162
|
+
*
|
|
163
|
+
* <template>
|
|
164
|
+
* <div style="position:relative; display:inline-flex; gap:8px; align-items:center">
|
|
165
|
+
* <input
|
|
166
|
+
* :ref="(el) => otp.inputRef.value = el"
|
|
167
|
+
* v-bind="otp.hiddenInputAttrs.value"
|
|
168
|
+
* style="position:absolute;inset:0;opacity:0;z-index:1;cursor:text"
|
|
169
|
+
* @keydown="otp.onKeydown"
|
|
170
|
+
* @input="otp.onChange"
|
|
171
|
+
* @paste="otp.onPaste"
|
|
172
|
+
* @focus="otp.onFocus"
|
|
173
|
+
* @blur="otp.onBlur"
|
|
174
|
+
* />
|
|
175
|
+
* <template v-for="(char, i) in otp.slotValues.value" :key="i">
|
|
176
|
+
* <span v-if="otp.separatorAfter.value > 0 && i === otp.separatorAfter.value" aria-hidden="true">
|
|
177
|
+
* {{ otp.separator.value }}
|
|
178
|
+
* </span>
|
|
179
|
+
* <div :class="['slot',
|
|
180
|
+
* i === otp.activeSlot.value && otp.isFocused.value ? 'is-active' : '',
|
|
181
|
+
* char ? 'is-filled' : '',
|
|
182
|
+
* otp.hasError.value ? 'is-error' : '',
|
|
183
|
+
* otp.isComplete.value && !otp.hasError.value ? 'is-success' : '',
|
|
184
|
+
* otp.isDisabled.value ? 'is-disabled' : '',
|
|
185
|
+
* ]">{{ char }}</div>
|
|
186
|
+
* </template>
|
|
187
|
+
* </div>
|
|
188
|
+
* </template>
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export function useOTP(options: VueOTPOptions = {}): UseOTPResult {
|
|
192
|
+
const {
|
|
193
|
+
length = 6,
|
|
194
|
+
type = 'numeric' as InputType,
|
|
195
|
+
timer: timerSecs = 0,
|
|
196
|
+
disabled: initialDisabled = false,
|
|
197
|
+
onComplete,
|
|
198
|
+
onExpire,
|
|
199
|
+
onResend,
|
|
200
|
+
haptic = true,
|
|
201
|
+
sound = false,
|
|
202
|
+
pattern,
|
|
203
|
+
pasteTransformer,
|
|
204
|
+
onInvalidChar,
|
|
205
|
+
value: controlledValue,
|
|
206
|
+
onChange: onChangeProp,
|
|
207
|
+
onFocus: onFocusProp,
|
|
208
|
+
onBlur: onBlurProp,
|
|
209
|
+
separatorAfter: separatorAfterOpt = 0,
|
|
210
|
+
separator: separatorOpt = '—',
|
|
211
|
+
masked: maskedOpt = false,
|
|
212
|
+
maskChar: maskCharOpt = '\u25CF',
|
|
213
|
+
autoFocus: autoFocusOpt = true,
|
|
214
|
+
name: nameOpt,
|
|
215
|
+
placeholder: placeholderOpt = '',
|
|
216
|
+
selectOnFocus: selectOnFocusOpt = false,
|
|
217
|
+
blurOnComplete: blurOnCompleteOpt = false,
|
|
218
|
+
} = options
|
|
219
|
+
|
|
220
|
+
// ── Core instance ──────────────────────────────────────────────────────────
|
|
221
|
+
const digito = createDigito({ length, type, pattern, pasteTransformer, onInvalidChar, onComplete, onExpire, onResend, haptic, sound })
|
|
222
|
+
|
|
223
|
+
// ── Reactive state ─────────────────────────────────────────────────────────
|
|
224
|
+
const slotValues = ref<string[]>(Array(length).fill(''))
|
|
225
|
+
const activeSlot = ref(0)
|
|
226
|
+
const isComplete = ref(false)
|
|
227
|
+
const hasError = ref(false)
|
|
228
|
+
const isDisabled = ref(initialDisabled)
|
|
229
|
+
const timerSeconds = ref(timerSecs)
|
|
230
|
+
const isFocused = ref(false)
|
|
231
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
232
|
+
const separatorAfter = ref<number | number[]>(separatorAfterOpt)
|
|
233
|
+
const separator = ref(separatorOpt)
|
|
234
|
+
const masked = ref(maskedOpt)
|
|
235
|
+
const maskChar = ref(maskCharOpt)
|
|
236
|
+
|
|
237
|
+
const value = computed(() => slotValues.value.join(''))
|
|
238
|
+
|
|
239
|
+
const hiddenInputAttrs = computed<Record<string, unknown>>(() => ({
|
|
240
|
+
type: masked.value ? 'password' : 'text',
|
|
241
|
+
inputmode: type === 'numeric' ? 'numeric' : 'text',
|
|
242
|
+
autocomplete: 'one-time-code',
|
|
243
|
+
maxlength: length,
|
|
244
|
+
disabled: isDisabled.value,
|
|
245
|
+
...(nameOpt ? { name: nameOpt } : {}),
|
|
246
|
+
...(autoFocusOpt ? { autofocus: true } : {}),
|
|
247
|
+
'aria-label': `Enter your ${length}-${type === 'numeric' ? 'digit' : 'character'} code`,
|
|
248
|
+
spellcheck: 'false',
|
|
249
|
+
autocorrect: 'off',
|
|
250
|
+
autocapitalize: 'off',
|
|
251
|
+
}))
|
|
252
|
+
|
|
253
|
+
// ── sync() ─────────────────────────────────────────────────────────────────
|
|
254
|
+
function sync(suppressOnChange = false): void {
|
|
255
|
+
const s = digito.state
|
|
256
|
+
slotValues.value = [...s.slotValues]
|
|
257
|
+
activeSlot.value = s.activeSlot
|
|
258
|
+
isComplete.value = s.isComplete
|
|
259
|
+
hasError.value = s.hasError
|
|
260
|
+
if (!suppressOnChange) {
|
|
261
|
+
onChangeProp?.(s.slotValues.join(''))
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── Controlled value sync ──────────────────────────────────────────────────
|
|
266
|
+
// When value is a Ref<string>, watch it reactively so parent changes
|
|
267
|
+
// propagate automatically. When it's a plain string, the arrow-function
|
|
268
|
+
// source returns a constant — watch fires once via { immediate: true }
|
|
269
|
+
// and never again (documented static-pre-fill behaviour).
|
|
270
|
+
if (controlledValue !== undefined) {
|
|
271
|
+
const watchSource = isRef(controlledValue)
|
|
272
|
+
? controlledValue
|
|
273
|
+
: () => controlledValue as string
|
|
274
|
+
|
|
275
|
+
watch(
|
|
276
|
+
watchSource,
|
|
277
|
+
(incoming: string) => {
|
|
278
|
+
const filtered = filterString(incoming.slice(0, length), type, pattern)
|
|
279
|
+
const current = digito.state.slotValues.join('')
|
|
280
|
+
if (filtered === current) return
|
|
281
|
+
|
|
282
|
+
digito.resetState()
|
|
283
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
284
|
+
digito.inputChar(i, filtered[i])
|
|
285
|
+
}
|
|
286
|
+
sync(true)
|
|
287
|
+
if (inputRef.value) {
|
|
288
|
+
inputRef.value.value = filtered
|
|
289
|
+
inputRef.value.setSelectionRange(filtered.length, filtered.length)
|
|
290
|
+
}
|
|
291
|
+
onChangeProp?.(filtered)
|
|
292
|
+
},
|
|
293
|
+
{ immediate: true }
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Timer ──────────────────────────────────────────────────────────────────
|
|
298
|
+
let timerControls: ReturnType<typeof createTimer> | null = null
|
|
299
|
+
|
|
300
|
+
onMounted(() => {
|
|
301
|
+
if (autoFocusOpt && !initialDisabled && inputRef.value) {
|
|
302
|
+
inputRef.value.focus()
|
|
303
|
+
inputRef.value.setSelectionRange(0, 0)
|
|
304
|
+
}
|
|
305
|
+
if (!timerSecs) return
|
|
306
|
+
timerControls = createTimer({
|
|
307
|
+
totalSeconds: timerSecs,
|
|
308
|
+
onTick: (r) => { timerSeconds.value = r },
|
|
309
|
+
onExpire: () => { timerSeconds.value = 0; onExpire?.() },
|
|
310
|
+
})
|
|
311
|
+
timerControls.start()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
onUnmounted(() => timerControls?.stop())
|
|
315
|
+
|
|
316
|
+
// ── Event handlers ─────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
function onKeydown(e: KeyboardEvent): void {
|
|
319
|
+
if (isDisabled.value) return
|
|
320
|
+
const pos = inputRef.value?.selectionStart ?? 0
|
|
321
|
+
if (e.key === 'Backspace') {
|
|
322
|
+
e.preventDefault()
|
|
323
|
+
digito.deleteChar(pos)
|
|
324
|
+
sync()
|
|
325
|
+
const next = digito.state.activeSlot
|
|
326
|
+
requestAnimationFrame(() => inputRef.value?.setSelectionRange(next, next))
|
|
327
|
+
} else if (e.key === 'ArrowLeft') {
|
|
328
|
+
e.preventDefault()
|
|
329
|
+
digito.moveFocusLeft(pos)
|
|
330
|
+
sync()
|
|
331
|
+
const next = digito.state.activeSlot
|
|
332
|
+
requestAnimationFrame(() => inputRef.value?.setSelectionRange(next, next))
|
|
333
|
+
} else if (e.key === 'ArrowRight') {
|
|
334
|
+
e.preventDefault()
|
|
335
|
+
digito.moveFocusRight(pos)
|
|
336
|
+
sync()
|
|
337
|
+
const next = digito.state.activeSlot
|
|
338
|
+
requestAnimationFrame(() => inputRef.value?.setSelectionRange(next, next))
|
|
339
|
+
} else if (e.key === 'Tab') {
|
|
340
|
+
if (e.shiftKey) {
|
|
341
|
+
if (pos === 0) return
|
|
342
|
+
e.preventDefault()
|
|
343
|
+
digito.moveFocusLeft(pos)
|
|
344
|
+
} else {
|
|
345
|
+
if (!digito.state.slotValues[pos]) return
|
|
346
|
+
if (pos >= length - 1) return
|
|
347
|
+
e.preventDefault()
|
|
348
|
+
digito.moveFocusRight(pos)
|
|
349
|
+
}
|
|
350
|
+
sync()
|
|
351
|
+
const next = digito.state.activeSlot
|
|
352
|
+
requestAnimationFrame(() => inputRef.value?.setSelectionRange(next, next))
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function onChange(e: Event): void {
|
|
357
|
+
if (isDisabled.value) return
|
|
358
|
+
const raw = (e.target as HTMLInputElement).value
|
|
359
|
+
if (!raw) {
|
|
360
|
+
digito.resetState()
|
|
361
|
+
if (inputRef.value) { inputRef.value.value = ''; inputRef.value.setSelectionRange(0, 0) }
|
|
362
|
+
sync()
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
const valid = filterString(raw, type, pattern).slice(0, length)
|
|
366
|
+
digito.resetState()
|
|
367
|
+
for (let i = 0; i < valid.length; i++) digito.inputChar(i, valid[i])
|
|
368
|
+
const next = Math.min(valid.length, length - 1)
|
|
369
|
+
if (inputRef.value) { inputRef.value.value = valid; inputRef.value.setSelectionRange(next, next) }
|
|
370
|
+
digito.moveFocusTo(next)
|
|
371
|
+
sync()
|
|
372
|
+
if (blurOnCompleteOpt && digito.state.isComplete) {
|
|
373
|
+
requestAnimationFrame(() => inputRef.value?.blur())
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function onPaste(e: ClipboardEvent): void {
|
|
378
|
+
if (isDisabled.value) return
|
|
379
|
+
e.preventDefault()
|
|
380
|
+
const text = e.clipboardData?.getData('text') ?? ''
|
|
381
|
+
const pos = inputRef.value?.selectionStart ?? 0
|
|
382
|
+
digito.pasteString(pos, text)
|
|
383
|
+
const { slotValues: sv, activeSlot: as } = digito.state
|
|
384
|
+
if (inputRef.value) { inputRef.value.value = sv.join(''); inputRef.value.setSelectionRange(as, as) }
|
|
385
|
+
sync()
|
|
386
|
+
if (blurOnCompleteOpt && digito.state.isComplete) {
|
|
387
|
+
requestAnimationFrame(() => inputRef.value?.blur())
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function onFocus(): void {
|
|
392
|
+
isFocused.value = true
|
|
393
|
+
onFocusProp?.()
|
|
394
|
+
const pos = digito.state.activeSlot
|
|
395
|
+
requestAnimationFrame(() => {
|
|
396
|
+
const char = digito.state.slotValues[pos]
|
|
397
|
+
if (selectOnFocusOpt && char) {
|
|
398
|
+
inputRef.value?.setSelectionRange(pos, pos + 1)
|
|
399
|
+
} else {
|
|
400
|
+
inputRef.value?.setSelectionRange(pos, pos)
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function onBlur(): void {
|
|
406
|
+
isFocused.value = false
|
|
407
|
+
onBlurProp?.()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
function reset(): void {
|
|
413
|
+
digito.resetState()
|
|
414
|
+
if (inputRef.value) { inputRef.value.value = ''; inputRef.value.focus(); inputRef.value.setSelectionRange(0, 0) }
|
|
415
|
+
timerSeconds.value = timerSecs
|
|
416
|
+
timerControls?.restart()
|
|
417
|
+
sync()
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function setError(isError: boolean): void {
|
|
421
|
+
digito.setError(isError)
|
|
422
|
+
sync(true)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function focus(slotIndex: number): void {
|
|
426
|
+
digito.moveFocusTo(slotIndex)
|
|
427
|
+
inputRef.value?.focus()
|
|
428
|
+
requestAnimationFrame(() => inputRef.value?.setSelectionRange(slotIndex, slotIndex))
|
|
429
|
+
sync(true)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function getCode(): string {
|
|
433
|
+
return digito.getCode()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
slotValues,
|
|
438
|
+
activeSlot,
|
|
439
|
+
value,
|
|
440
|
+
isComplete,
|
|
441
|
+
hasError,
|
|
442
|
+
isDisabled,
|
|
443
|
+
timerSeconds,
|
|
444
|
+
isFocused,
|
|
445
|
+
separatorAfter,
|
|
446
|
+
separator,
|
|
447
|
+
masked,
|
|
448
|
+
maskChar,
|
|
449
|
+
placeholder: placeholderOpt,
|
|
450
|
+
inputRef,
|
|
451
|
+
hiddenInputAttrs,
|
|
452
|
+
getCode,
|
|
453
|
+
reset,
|
|
454
|
+
setError,
|
|
455
|
+
focus,
|
|
456
|
+
onKeydown,
|
|
457
|
+
onChange,
|
|
458
|
+
onPaste,
|
|
459
|
+
onFocus,
|
|
460
|
+
onBlur,
|
|
461
|
+
}
|
|
462
|
+
}
|