digitojs 1.0.1 → 1.2.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 +15 -0
- package/README.md +112 -39
- package/dist/adapters/alpine.d.ts.map +1 -1
- package/dist/adapters/alpine.js +65 -58
- package/dist/adapters/alpine.js.map +1 -1
- package/dist/adapters/react.d.ts +9 -0
- package/dist/adapters/react.d.ts.map +1 -1
- package/dist/adapters/react.js +45 -4
- package/dist/adapters/react.js.map +1 -1
- package/dist/adapters/svelte.d.ts +16 -14
- package/dist/adapters/svelte.d.ts.map +1 -1
- package/dist/adapters/svelte.js +34 -4
- package/dist/adapters/svelte.js.map +1 -1
- package/dist/adapters/vanilla.d.ts.map +1 -1
- package/dist/adapters/vanilla.js +36 -12
- package/dist/adapters/vanilla.js.map +1 -1
- package/dist/adapters/vue.d.ts +2 -0
- package/dist/adapters/vue.d.ts.map +1 -1
- package/dist/adapters/vue.js +35 -4
- package/dist/adapters/vue.js.map +1 -1
- package/dist/adapters/web-component.d.ts +3 -0
- package/dist/adapters/web-component.d.ts.map +1 -1
- package/dist/adapters/web-component.js +37 -12
- package/dist/adapters/web-component.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/machine.d.ts +3 -0
- package/dist/core/machine.d.ts.map +1 -1
- package/dist/core/machine.js +31 -3
- package/dist/core/machine.js.map +1 -1
- package/dist/core/timer.d.ts +8 -0
- package/dist/core/timer.d.ts.map +1 -1
- package/dist/core/timer.js +14 -0
- package/dist/core/timer.js.map +1 -1
- package/dist/core/types.d.ts +30 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/digito-wc.min.js +2 -2
- package/dist/digito-wc.min.js.map +3 -3
- package/dist/digito.min.js +1 -1
- package/dist/digito.min.js.map +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/src/adapters/alpine.ts +58 -50
- package/src/adapters/react.tsx +50 -5
- package/src/adapters/svelte.ts +48 -16
- package/src/adapters/vanilla.ts +32 -14
- package/src/adapters/vue.ts +32 -3
- package/src/adapters/web-component.ts +35 -13
- package/src/core/index.ts +1 -1
- package/src/core/machine.ts +31 -3
- package/src/core/timer.ts +15 -0
- package/src/core/types.ts +30 -2
- package/src/index.ts +0 -1
package/src/adapters/vue.ts
CHANGED
|
@@ -129,6 +129,8 @@ export type UseOTPResult = {
|
|
|
129
129
|
inputRef: Ref<HTMLInputElement | null>
|
|
130
130
|
/** Attribute object to spread onto the hidden input via v-bind. */
|
|
131
131
|
hiddenInputAttrs: Ref<Record<string, unknown>>
|
|
132
|
+
/** Spread onto the wrapper element to expose state as data attributes for CSS/Tailwind targeting. */
|
|
133
|
+
wrapperAttrs: Ref<Record<string, string | undefined>>
|
|
132
134
|
/** Returns the current joined code string. */
|
|
133
135
|
getCode: () => string
|
|
134
136
|
/** Clear all slots, restart timer, return focus to input. */
|
|
@@ -203,6 +205,8 @@ export function useOTP(options: VueOTPOptions = {}): UseOTPResult {
|
|
|
203
205
|
pasteTransformer,
|
|
204
206
|
onInvalidChar,
|
|
205
207
|
value: controlledValue,
|
|
208
|
+
defaultValue,
|
|
209
|
+
readOnly: readOnlyOpt = false,
|
|
206
210
|
onChange: onChangeProp,
|
|
207
211
|
onFocus: onFocusProp,
|
|
208
212
|
onBlur: onBlurProp,
|
|
@@ -218,7 +222,7 @@ export function useOTP(options: VueOTPOptions = {}): UseOTPResult {
|
|
|
218
222
|
} = options
|
|
219
223
|
|
|
220
224
|
// ── Core instance ──────────────────────────────────────────────────────────
|
|
221
|
-
const digito = createDigito({ length, type, pattern, pasteTransformer, onInvalidChar, onComplete, onExpire, onResend, haptic, sound })
|
|
225
|
+
const digito = createDigito({ length, type, pattern, pasteTransformer, onInvalidChar, onComplete, onExpire, onResend, haptic, sound, readOnly: readOnlyOpt })
|
|
222
226
|
|
|
223
227
|
// ── Reactive state ─────────────────────────────────────────────────────────
|
|
224
228
|
const slotValues = ref<string[]>(Array(length).fill(''))
|
|
@@ -248,6 +252,14 @@ export function useOTP(options: VueOTPOptions = {}): UseOTPResult {
|
|
|
248
252
|
spellcheck: 'false',
|
|
249
253
|
autocorrect: 'off',
|
|
250
254
|
autocapitalize: 'off',
|
|
255
|
+
...(readOnlyOpt ? { 'aria-readonly': 'true' } : {}),
|
|
256
|
+
}))
|
|
257
|
+
|
|
258
|
+
const wrapperAttrs = computed<Record<string, string | undefined>>(() => ({
|
|
259
|
+
...(isComplete.value ? { 'data-complete': '' } : {}),
|
|
260
|
+
...(hasError.value ? { 'data-invalid': '' } : {}),
|
|
261
|
+
...(isDisabled.value ? { 'data-disabled': '' } : {}),
|
|
262
|
+
...(readOnlyOpt ? { 'data-readonly': '' } : {}),
|
|
251
263
|
}))
|
|
252
264
|
|
|
253
265
|
// ── sync() ─────────────────────────────────────────────────────────────────
|
|
@@ -298,6 +310,15 @@ export function useOTP(options: VueOTPOptions = {}): UseOTPResult {
|
|
|
298
310
|
let timerControls: ReturnType<typeof createTimer> | null = null
|
|
299
311
|
|
|
300
312
|
onMounted(() => {
|
|
313
|
+
if (controlledValue === undefined && defaultValue) {
|
|
314
|
+
const filtered = filterString(defaultValue.slice(0, length), type, pattern)
|
|
315
|
+
if (filtered) {
|
|
316
|
+
for (let i = 0; i < filtered.length; i++) digito.inputChar(i, filtered[i])
|
|
317
|
+
digito.cancelPendingComplete()
|
|
318
|
+
sync(true)
|
|
319
|
+
if (inputRef.value) { inputRef.value.value = filtered; inputRef.value.setSelectionRange(filtered.length, filtered.length) }
|
|
320
|
+
}
|
|
321
|
+
}
|
|
301
322
|
if (autoFocusOpt && !initialDisabled && inputRef.value) {
|
|
302
323
|
inputRef.value.focus()
|
|
303
324
|
inputRef.value.setSelectionRange(0, 0)
|
|
@@ -320,10 +341,17 @@ export function useOTP(options: VueOTPOptions = {}): UseOTPResult {
|
|
|
320
341
|
const pos = inputRef.value?.selectionStart ?? 0
|
|
321
342
|
if (e.key === 'Backspace') {
|
|
322
343
|
e.preventDefault()
|
|
344
|
+
if (readOnlyOpt) return
|
|
323
345
|
digito.deleteChar(pos)
|
|
324
346
|
sync()
|
|
325
347
|
const next = digito.state.activeSlot
|
|
326
348
|
requestAnimationFrame(() => inputRef.value?.setSelectionRange(next, next))
|
|
349
|
+
} else if (e.key === 'Delete') {
|
|
350
|
+
e.preventDefault()
|
|
351
|
+
if (readOnlyOpt) return
|
|
352
|
+
digito.clearSlot(pos)
|
|
353
|
+
sync()
|
|
354
|
+
requestAnimationFrame(() => inputRef.value?.setSelectionRange(pos, pos))
|
|
327
355
|
} else if (e.key === 'ArrowLeft') {
|
|
328
356
|
e.preventDefault()
|
|
329
357
|
digito.moveFocusLeft(pos)
|
|
@@ -354,7 +382,7 @@ export function useOTP(options: VueOTPOptions = {}): UseOTPResult {
|
|
|
354
382
|
}
|
|
355
383
|
|
|
356
384
|
function onChange(e: Event): void {
|
|
357
|
-
if (isDisabled.value) return
|
|
385
|
+
if (isDisabled.value || readOnlyOpt) return
|
|
358
386
|
const raw = (e.target as HTMLInputElement).value
|
|
359
387
|
if (!raw) {
|
|
360
388
|
digito.resetState()
|
|
@@ -375,7 +403,7 @@ export function useOTP(options: VueOTPOptions = {}): UseOTPResult {
|
|
|
375
403
|
}
|
|
376
404
|
|
|
377
405
|
function onPaste(e: ClipboardEvent): void {
|
|
378
|
-
if (isDisabled.value) return
|
|
406
|
+
if (isDisabled.value || readOnlyOpt) return
|
|
379
407
|
e.preventDefault()
|
|
380
408
|
const text = e.clipboardData?.getData('text') ?? ''
|
|
381
409
|
const pos = inputRef.value?.selectionStart ?? 0
|
|
@@ -449,6 +477,7 @@ export function useOTP(options: VueOTPOptions = {}): UseOTPResult {
|
|
|
449
477
|
placeholder: placeholderOpt,
|
|
450
478
|
inputRef,
|
|
451
479
|
hiddenInputAttrs,
|
|
480
|
+
wrapperAttrs,
|
|
452
481
|
getCode,
|
|
453
482
|
reset,
|
|
454
483
|
setError,
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
createDigito,
|
|
46
46
|
createTimer,
|
|
47
47
|
filterString,
|
|
48
|
+
formatCountdown,
|
|
48
49
|
type InputType,
|
|
49
50
|
} from '../core/index.js'
|
|
50
51
|
|
|
@@ -208,16 +209,6 @@ const STYLES = `
|
|
|
208
209
|
.digito-wc-resend-btn:disabled { color: #A1A1A1; cursor: not-allowed; background: #F5F5F5; }
|
|
209
210
|
`
|
|
210
211
|
|
|
211
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
212
|
-
// HELPERS
|
|
213
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
214
|
-
|
|
215
|
-
function formatCountdown(totalSeconds: number): string {
|
|
216
|
-
const m = Math.floor(totalSeconds / 60)
|
|
217
|
-
const s = totalSeconds % 60
|
|
218
|
-
return m > 0 ? `${m}:${String(s).padStart(2, '0')}` : `0:${String(s).padStart(2, '0')}`
|
|
219
|
-
}
|
|
220
|
-
|
|
221
212
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
222
213
|
// WEB COMPONENT
|
|
223
214
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -228,7 +219,7 @@ class DigitoInput extends HTMLElement {
|
|
|
228
219
|
* Any change to these attributes causes a full shadow DOM rebuild so the
|
|
229
220
|
* component always reflects its attribute state without manual reconciliation.
|
|
230
221
|
*/
|
|
231
|
-
static observedAttributes = ['length', 'type', 'timer', 'resend-after', 'disabled', 'separator-after', 'separator', 'masked', 'mask-char', 'name', 'placeholder', 'auto-focus', 'select-on-focus', 'blur-on-complete']
|
|
222
|
+
static observedAttributes = ['length', 'type', 'timer', 'resend-after', 'disabled', 'readonly', 'separator-after', 'separator', 'masked', 'mask-char', 'name', 'placeholder', 'auto-focus', 'select-on-focus', 'blur-on-complete', 'default-value']
|
|
232
223
|
|
|
233
224
|
// Shadow DOM references — rebuilt in full on every attributeChangedCallback.
|
|
234
225
|
private slotEls: HTMLDivElement[] = []
|
|
@@ -245,6 +236,7 @@ class DigitoInput extends HTMLElement {
|
|
|
245
236
|
// Runtime mutable state — toggled by setDisabled() without a full rebuild.
|
|
246
237
|
private _isDisabled = false
|
|
247
238
|
private _isSuccess = false
|
|
239
|
+
private _isReadOnly = false
|
|
248
240
|
|
|
249
241
|
// JS-property-only options. These cannot be expressed as HTML attributes
|
|
250
242
|
// (RegExp and functions are not serialisable to strings), so they are stored
|
|
@@ -375,6 +367,8 @@ class DigitoInput extends HTMLElement {
|
|
|
375
367
|
return isNaN(v) || v < 1 ? 30 : Math.floor(v)
|
|
376
368
|
}
|
|
377
369
|
private get _disabledAttr(): boolean { return this.hasAttribute('disabled') }
|
|
370
|
+
private get _readOnlyAttr(): boolean { return this.hasAttribute('readonly') }
|
|
371
|
+
private get _defaultValue(): string { return this.getAttribute('default-value') ?? '' }
|
|
378
372
|
/** Parses `separator-after="2,4"` into `[2, 4]`. Filters NaN and zero values. */
|
|
379
373
|
private get _separatorAfter(): number[] {
|
|
380
374
|
const v = this.getAttribute('separator-after')
|
|
@@ -418,6 +412,7 @@ class DigitoInput extends HTMLElement {
|
|
|
418
412
|
const selectOnFocus = this._selectOnFocus
|
|
419
413
|
const blurOnComplete = this._blurOnComplete
|
|
420
414
|
this._isDisabled = this._disabledAttr
|
|
415
|
+
this._isReadOnly = this._readOnlyAttr
|
|
421
416
|
|
|
422
417
|
this.timerCtrl?.stop()
|
|
423
418
|
this.resendCountdown?.stop()
|
|
@@ -495,6 +490,7 @@ class DigitoInput extends HTMLElement {
|
|
|
495
490
|
pattern: this._pattern,
|
|
496
491
|
pasteTransformer: this._pasteTransformer,
|
|
497
492
|
onInvalidChar: this._onInvalidChar,
|
|
493
|
+
readOnly: this._isReadOnly,
|
|
498
494
|
onComplete: (code) => {
|
|
499
495
|
// Call JS property setter AND dispatch CustomEvent
|
|
500
496
|
this._onComplete?.(code)
|
|
@@ -573,6 +569,19 @@ class DigitoInput extends HTMLElement {
|
|
|
573
569
|
})
|
|
574
570
|
}
|
|
575
571
|
|
|
572
|
+
if (this._isReadOnly) hiddenInput.setAttribute('aria-readonly', 'true')
|
|
573
|
+
|
|
574
|
+
// Apply defaultValue once on build — no onComplete, no change event
|
|
575
|
+
const dv = this._defaultValue
|
|
576
|
+
if (dv) {
|
|
577
|
+
const filtered = filterString(dv.slice(0, length), type, this._pattern)
|
|
578
|
+
if (filtered) {
|
|
579
|
+
for (let i = 0; i < filtered.length; i++) this.digito!.inputChar(i, filtered[i])
|
|
580
|
+
this.digito!.cancelPendingComplete()
|
|
581
|
+
hiddenInput.value = filtered
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
576
585
|
this.attachEvents(selectOnFocus, blurOnComplete)
|
|
577
586
|
|
|
578
587
|
if (this._isDisabled) this.applyDisabledDOM(true)
|
|
@@ -645,6 +654,11 @@ class DigitoInput extends HTMLElement {
|
|
|
645
654
|
// resets selectionStart/End in some browsers, clobbering the cursor.
|
|
646
655
|
const newValue = slotValues.join('')
|
|
647
656
|
if (this.hiddenInput.value !== newValue) this.hiddenInput.value = newValue
|
|
657
|
+
|
|
658
|
+
this.toggleAttribute('data-complete', this.digito.state.isComplete)
|
|
659
|
+
this.toggleAttribute('data-invalid', this.digito.state.hasError)
|
|
660
|
+
this.toggleAttribute('data-disabled', this._isDisabled)
|
|
661
|
+
this.toggleAttribute('data-readonly', this._isReadOnly)
|
|
648
662
|
}
|
|
649
663
|
|
|
650
664
|
/**
|
|
@@ -679,11 +693,19 @@ class DigitoInput extends HTMLElement {
|
|
|
679
693
|
const pos = input.selectionStart ?? 0
|
|
680
694
|
if (e.key === 'Backspace') {
|
|
681
695
|
e.preventDefault()
|
|
696
|
+
if (this._isReadOnly) return
|
|
682
697
|
digito.deleteChar(pos)
|
|
683
698
|
this.syncSlotsToDOM()
|
|
684
699
|
this.dispatchChange()
|
|
685
700
|
const next = digito.state.activeSlot
|
|
686
701
|
requestAnimationFrame(() => input.setSelectionRange(next, next))
|
|
702
|
+
} else if (e.key === 'Delete') {
|
|
703
|
+
e.preventDefault()
|
|
704
|
+
if (this._isReadOnly) return
|
|
705
|
+
digito.clearSlot(pos)
|
|
706
|
+
this.syncSlotsToDOM()
|
|
707
|
+
this.dispatchChange()
|
|
708
|
+
requestAnimationFrame(() => input.setSelectionRange(pos, pos))
|
|
687
709
|
} else if (e.key === 'ArrowLeft') {
|
|
688
710
|
e.preventDefault()
|
|
689
711
|
digito.moveFocusLeft(pos)
|
|
@@ -712,7 +734,7 @@ class DigitoInput extends HTMLElement {
|
|
|
712
734
|
})
|
|
713
735
|
|
|
714
736
|
input.addEventListener('input', () => {
|
|
715
|
-
if (this._isDisabled) return
|
|
737
|
+
if (this._isDisabled || this._isReadOnly) return
|
|
716
738
|
const raw = input.value
|
|
717
739
|
if (!raw) {
|
|
718
740
|
digito.resetState()
|
|
@@ -737,7 +759,7 @@ class DigitoInput extends HTMLElement {
|
|
|
737
759
|
})
|
|
738
760
|
|
|
739
761
|
input.addEventListener('paste', (e) => {
|
|
740
|
-
if (this._isDisabled) return
|
|
762
|
+
if (this._isDisabled || this._isReadOnly) return
|
|
741
763
|
e.preventDefault()
|
|
742
764
|
const text = e.clipboardData?.getData('text') ?? ''
|
|
743
765
|
const pos = input.selectionStart ?? 0
|
package/src/core/index.ts
CHANGED
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
export type { InputType, DigitoState, DigitoOptions, TimerOptions, TimerControls, StateListener } from './types.js'
|
|
13
13
|
export { filterChar, filterString } from './filter.js'
|
|
14
|
-
export { createTimer }
|
|
14
|
+
export { createTimer, formatCountdown } from './timer.js'
|
|
15
15
|
export { triggerHapticFeedback, triggerSoundFeedback } from './feedback.js'
|
|
16
16
|
export { createDigito } from './machine.js'
|
package/src/core/machine.ts
CHANGED
|
@@ -73,6 +73,8 @@ export function createDigito(options: DigitoOptions = {}) {
|
|
|
73
73
|
// requiring the instance to be recreated. Adapters that pass disabled at
|
|
74
74
|
// construction time still work — the initial value is honoured.
|
|
75
75
|
let disabled = options.disabled ?? false
|
|
76
|
+
// `readOnly` allows focus/navigation while blocking all slot mutations.
|
|
77
|
+
let readOnly = options.readOnly ?? false
|
|
76
78
|
|
|
77
79
|
let state: DigitoState = {
|
|
78
80
|
slotValues: Array(length).fill('') as string[],
|
|
@@ -140,7 +142,7 @@ export function createDigito(options: DigitoOptions = {}) {
|
|
|
140
142
|
* Out-of-bounds indices are silently ignored to prevent sparse-array corruption.
|
|
141
143
|
*/
|
|
142
144
|
function inputChar(slotIndex: number, char: string): DigitoState {
|
|
143
|
-
if (disabled) return state
|
|
145
|
+
if (disabled || readOnly) return state
|
|
144
146
|
if (slotIndex < 0 || slotIndex >= length) return state
|
|
145
147
|
const validChar = filterChar(char, type, pattern)
|
|
146
148
|
if (!validChar) {
|
|
@@ -176,7 +178,7 @@ export function createDigito(options: DigitoOptions = {}) {
|
|
|
176
178
|
* Clears the current slot if filled, otherwise clears the previous slot and moves back.
|
|
177
179
|
*/
|
|
178
180
|
function deleteChar(slotIndex: number): DigitoState {
|
|
179
|
-
if (disabled) return state
|
|
181
|
+
if (disabled || readOnly) return state
|
|
180
182
|
if (slotIndex < 0 || slotIndex >= length) return state
|
|
181
183
|
const slotValues = [...state.slotValues]
|
|
182
184
|
|
|
@@ -210,7 +212,7 @@ export function createDigito(options: DigitoOptions = {}) {
|
|
|
210
212
|
* paste(0, '84AB91') → filtered='8491', fills slots 0–3, slots 4–5 unchanged
|
|
211
213
|
*/
|
|
212
214
|
function pasteString(cursorSlot: number, rawText: string): DigitoState {
|
|
213
|
-
if (disabled) return state
|
|
215
|
+
if (disabled || readOnly) return state
|
|
214
216
|
|
|
215
217
|
let transformed: string
|
|
216
218
|
try {
|
|
@@ -261,6 +263,20 @@ export function createDigito(options: DigitoOptions = {}) {
|
|
|
261
263
|
return newState
|
|
262
264
|
}
|
|
263
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Clear the slot at slotIndex without moving focus.
|
|
268
|
+
* Used by the Delete key — differs from deleteChar (backspace) which steps the cursor back.
|
|
269
|
+
* Returns early when the slot is already empty, the field is disabled, or readOnly.
|
|
270
|
+
*/
|
|
271
|
+
function clearSlot(slotIndex: number): DigitoState {
|
|
272
|
+
if (disabled || readOnly) return state
|
|
273
|
+
if (slotIndex < 0 || slotIndex >= length) return state
|
|
274
|
+
if (!state.slotValues[slotIndex]) return state
|
|
275
|
+
const slotValues = [...state.slotValues]
|
|
276
|
+
slotValues[slotIndex] = ''
|
|
277
|
+
return applyState({ slotValues, activeSlot: slotIndex, isComplete: false })
|
|
278
|
+
}
|
|
279
|
+
|
|
264
280
|
/** Set or clear the error state. Triggers haptic feedback when setting. */
|
|
265
281
|
function setError(isError: boolean): DigitoState {
|
|
266
282
|
if (isError && haptic) triggerHapticFeedback()
|
|
@@ -299,6 +315,14 @@ export function createDigito(options: DigitoOptions = {}) {
|
|
|
299
315
|
disabled = value
|
|
300
316
|
}
|
|
301
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Toggle readOnly at runtime. When `true`, all slot mutations are blocked
|
|
320
|
+
* but navigation and focus remain fully functional.
|
|
321
|
+
*/
|
|
322
|
+
function setReadOnly(value: boolean): void {
|
|
323
|
+
readOnly = value
|
|
324
|
+
}
|
|
325
|
+
|
|
302
326
|
/**
|
|
303
327
|
* Subscribe to state changes. The listener is called after every mutation
|
|
304
328
|
* with a shallow copy of the new state.
|
|
@@ -325,6 +349,7 @@ export function createDigito(options: DigitoOptions = {}) {
|
|
|
325
349
|
// Input actions
|
|
326
350
|
inputChar,
|
|
327
351
|
deleteChar,
|
|
352
|
+
clearSlot,
|
|
328
353
|
moveFocusLeft,
|
|
329
354
|
moveFocusRight,
|
|
330
355
|
pasteString,
|
|
@@ -348,6 +373,9 @@ export function createDigito(options: DigitoOptions = {}) {
|
|
|
348
373
|
*/
|
|
349
374
|
setDisabled,
|
|
350
375
|
|
|
376
|
+
/** Toggle readOnly at runtime. Blocks mutations, preserves navigation. */
|
|
377
|
+
setReadOnly,
|
|
378
|
+
|
|
351
379
|
/** Returns the current joined code string. */
|
|
352
380
|
getCode: () => joinSlots(state.slotValues),
|
|
353
381
|
/**
|
package/src/core/timer.ts
CHANGED
|
@@ -73,3 +73,18 @@ export function createTimer(options: TimerOptions): TimerControls {
|
|
|
73
73
|
|
|
74
74
|
return { start, stop, reset, restart }
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format a second count as a `m:ss` countdown string (e.g. `"1:05"`, `"0:30"`).
|
|
79
|
+
* Used by the vanilla, alpine, and web-component adapters for their built-in timer UI.
|
|
80
|
+
*
|
|
81
|
+
* @example formatCountdown(65) → "1:05"
|
|
82
|
+
* @example formatCountdown(9) → "0:09"
|
|
83
|
+
*/
|
|
84
|
+
export function formatCountdown(totalSeconds: number): string {
|
|
85
|
+
const minutes = Math.floor(totalSeconds / 60)
|
|
86
|
+
const seconds = totalSeconds % 60
|
|
87
|
+
return minutes > 0
|
|
88
|
+
? `${minutes}:${String(seconds).padStart(2, '0')}`
|
|
89
|
+
: `0:${String(seconds).padStart(2, '0')}`
|
|
90
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -40,11 +40,25 @@ export type DigitoOptions = {
|
|
|
40
40
|
resendAfter?: number
|
|
41
41
|
/** Called with the joined code string when all slots are filled. */
|
|
42
42
|
onComplete?: (code: string) => void
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Called every second with the remaining seconds. Use to drive a custom timer UI.
|
|
45
|
+
*
|
|
46
|
+
* **Adapter note:** Only fires in adapters that include a built-in countdown timer
|
|
47
|
+
* (vanilla, alpine, web component). In React, Vue, and Svelte the timer is managed
|
|
48
|
+
* separately inside each adapter — pass `onTick` as part of those adapters' options.
|
|
49
|
+
* Has no effect when passed directly to `createDigito`.
|
|
50
|
+
*/
|
|
44
51
|
onTick?: (remainingSeconds: number) => void
|
|
45
52
|
/** Called when the countdown reaches zero. */
|
|
46
53
|
onExpire?: () => void
|
|
47
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Called when the resend action is triggered.
|
|
56
|
+
*
|
|
57
|
+
* **Adapter note:** Only fires automatically in adapters with a built-in Resend button
|
|
58
|
+
* (vanilla, alpine, web component). In React, Vue, and Svelte there is no built-in
|
|
59
|
+
* Resend button — call `onResend` manually in your own UI handler.
|
|
60
|
+
* Has no effect when passed directly to `createDigito`.
|
|
61
|
+
*/
|
|
48
62
|
onResend?: () => void
|
|
49
63
|
/** Vibrate on completion and error via `navigator.vibrate`. Default: `true`. */
|
|
50
64
|
haptic?: boolean
|
|
@@ -127,6 +141,20 @@ export type DigitoOptions = {
|
|
|
127
141
|
* Default: `false`.
|
|
128
142
|
*/
|
|
129
143
|
blurOnComplete?: boolean
|
|
144
|
+
/**
|
|
145
|
+
* Uncontrolled initial value applied once on mount.
|
|
146
|
+
* Distributed across slots exactly like user input but does NOT trigger
|
|
147
|
+
* `onComplete` or fire change events. Ignored when a `value` prop is present.
|
|
148
|
+
* Default: `undefined` (no pre-fill).
|
|
149
|
+
*/
|
|
150
|
+
defaultValue?: string
|
|
151
|
+
/**
|
|
152
|
+
* When `true`, all slot mutations (typing, backspace, delete, paste) are
|
|
153
|
+
* blocked while focus, selection, arrow navigation, and copy remain allowed.
|
|
154
|
+
* Semantically distinct from `disabled` — the field is readable and focusable.
|
|
155
|
+
* Default: `false`.
|
|
156
|
+
*/
|
|
157
|
+
readOnly?: boolean
|
|
130
158
|
/**
|
|
131
159
|
* Called when the user types or pastes a character that is rejected by the
|
|
132
160
|
* current `type` or `pattern` filter.
|