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,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* digito/web-component
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* Framework-agnostic Web Component — <digito-input>
|
|
5
|
+
* Uses single hidden-input architecture for correct SMS autofill + a11y.
|
|
6
|
+
*
|
|
7
|
+
* Attributes:
|
|
8
|
+
* length Number of slots (default: 6)
|
|
9
|
+
* type Character set: numeric | alphabet | alphanumeric | any (default: numeric)
|
|
10
|
+
* timer Countdown seconds (default: 0 = no timer)
|
|
11
|
+
* resend-after Cooldown seconds before the built-in Resend re-enables (default: 30)
|
|
12
|
+
* disabled Boolean attribute — disables all input when present
|
|
13
|
+
* separator-after Slot index (1-based) or comma-separated list, e.g. "3" or "2,4" (default: none)
|
|
14
|
+
* separator Separator character to render (default: —)
|
|
15
|
+
* name Sets the hidden input's name attr for native form submission
|
|
16
|
+
* placeholder Character shown in empty slots (e.g. "○" or "_")
|
|
17
|
+
* auto-focus Boolean attribute — focus input on mount (default: true when absent)
|
|
18
|
+
* select-on-focus Boolean attribute — selects the current slot char on focus
|
|
19
|
+
* blur-on-complete Boolean attribute — blurs the input when all slots are filled
|
|
20
|
+
*
|
|
21
|
+
* Events:
|
|
22
|
+
* complete CustomEvent<{ code: string }> — fired when all slots filled
|
|
23
|
+
* expire CustomEvent — fired when timer reaches zero
|
|
24
|
+
* change CustomEvent<{ code: string }> — fired on every input change
|
|
25
|
+
*
|
|
26
|
+
* DOM API:
|
|
27
|
+
* el.reset()
|
|
28
|
+
* el.setError(boolean)
|
|
29
|
+
* el.setSuccess(boolean)
|
|
30
|
+
* el.setDisabled(boolean)
|
|
31
|
+
* el.getCode() -> string
|
|
32
|
+
* el.pattern = /^[0-9A-F]$/ (JS property, not attribute)
|
|
33
|
+
* el.pasteTransformer = fn (JS property)
|
|
34
|
+
* el.onComplete = code => {} (JS property)
|
|
35
|
+
* el.onResend = () => {} (JS property)
|
|
36
|
+
* el.onFocus = () => {} (JS property)
|
|
37
|
+
* el.onBlur = () => {} (JS property)
|
|
38
|
+
* el.onInvalidChar = (char, i) => {} (JS property)
|
|
39
|
+
*
|
|
40
|
+
* @author Olawale Balo — Product Designer + Design Engineer
|
|
41
|
+
* @license MIT
|
|
42
|
+
*/
|
|
43
|
+
import { createDigito, createTimer, filterString, } from '../core/index.js';
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// SHADOW DOM STYLES
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
const STYLES = `
|
|
48
|
+
:host {
|
|
49
|
+
display: inline-block;
|
|
50
|
+
position: relative;
|
|
51
|
+
line-height: 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.digito-wc-root {
|
|
55
|
+
position: relative;
|
|
56
|
+
display: inline-block;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.digito-wc-slots {
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
gap: var(--digito-gap, 12px);
|
|
62
|
+
align-items: center;
|
|
63
|
+
position: relative;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.digito-wc-hidden {
|
|
67
|
+
position: absolute;
|
|
68
|
+
inset: 0;
|
|
69
|
+
width: 100%;
|
|
70
|
+
height: 100%;
|
|
71
|
+
opacity: 0;
|
|
72
|
+
border: none;
|
|
73
|
+
outline: none;
|
|
74
|
+
background: transparent;
|
|
75
|
+
color: transparent;
|
|
76
|
+
caret-color: transparent;
|
|
77
|
+
z-index: 1;
|
|
78
|
+
cursor: text;
|
|
79
|
+
font-size: 1px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.digito-wc-slot {
|
|
83
|
+
position: relative;
|
|
84
|
+
width: var(--digito-size, 56px);
|
|
85
|
+
height: var(--digito-size, 56px);
|
|
86
|
+
border: 1px solid var(--digito-border-color, #E5E5E5);
|
|
87
|
+
border-radius: var(--digito-radius, 10px);
|
|
88
|
+
font-size: var(--digito-font-size, 24px);
|
|
89
|
+
font-weight: 600;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
background: var(--digito-bg, #FAFAFA);
|
|
94
|
+
color: var(--digito-color, #0A0A0A);
|
|
95
|
+
font-family: inherit;
|
|
96
|
+
cursor: text;
|
|
97
|
+
user-select: none;
|
|
98
|
+
transition: border-color 150ms ease, box-shadow 150ms ease, background 150ms ease, opacity 150ms ease;
|
|
99
|
+
}
|
|
100
|
+
.digito-wc-slot.is-active {
|
|
101
|
+
border-color: var(--digito-active-color, #3D3D3D);
|
|
102
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--digito-active-color, #3D3D3D) 10%, transparent);
|
|
103
|
+
background: var(--digito-bg-filled, #FFFFFF);
|
|
104
|
+
}
|
|
105
|
+
.digito-wc-slot.is-filled { background: var(--digito-bg-filled, #FFFFFF); }
|
|
106
|
+
.digito-wc-slot.is-error {
|
|
107
|
+
border-color: var(--digito-error-color, #FB2C36);
|
|
108
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--digito-error-color, #FB2C36) 12%, transparent);
|
|
109
|
+
}
|
|
110
|
+
.digito-wc-slot.is-success {
|
|
111
|
+
border-color: var(--digito-success-color, #00C950);
|
|
112
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--digito-success-color, #00C950) 12%, transparent);
|
|
113
|
+
}
|
|
114
|
+
.digito-wc-slot.is-disabled {
|
|
115
|
+
opacity: 0.45;
|
|
116
|
+
cursor: not-allowed;
|
|
117
|
+
pointer-events: none;
|
|
118
|
+
}
|
|
119
|
+
.digito-wc-slot:not(.is-filled) {
|
|
120
|
+
font-size: var(--digito-placeholder-size, 16px);
|
|
121
|
+
color: var(--digito-placeholder-color, #D3D3D3);
|
|
122
|
+
}
|
|
123
|
+
.digito-wc-slot.is-masked.is-filled {
|
|
124
|
+
font-size: var(--digito-masked-size, 16px);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.digito-wc-separator {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
color: var(--digito-separator-color, #A1A1A1);
|
|
132
|
+
font-size: var(--digito-separator-size, 18px);
|
|
133
|
+
font-weight: 400;
|
|
134
|
+
user-select: none;
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
padding: 0 2px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.digito-wc-caret {
|
|
140
|
+
position: absolute;
|
|
141
|
+
width: 2px;
|
|
142
|
+
height: 52%;
|
|
143
|
+
background: var(--digito-caret-color, #3D3D3D);
|
|
144
|
+
border-radius: 1px;
|
|
145
|
+
animation: wc-blink 1s step-start infinite;
|
|
146
|
+
pointer-events: none;
|
|
147
|
+
display: none;
|
|
148
|
+
}
|
|
149
|
+
@keyframes wc-blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
|
150
|
+
|
|
151
|
+
.digito-wc-timer {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
font-size: 14px;
|
|
156
|
+
padding: 12px 0 0;
|
|
157
|
+
}
|
|
158
|
+
.digito-wc-timer-label {
|
|
159
|
+
color: var(--digito-timer-color, #5C5C5C);
|
|
160
|
+
font-size: 14px;
|
|
161
|
+
}
|
|
162
|
+
.digito-wc-timer-badge {
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
background: color-mix(in srgb, var(--digito-error-color, #FB2C36) 10%, transparent);
|
|
166
|
+
color: var(--digito-error-color, #FB2C36);
|
|
167
|
+
font-weight: 500;
|
|
168
|
+
font-size: 14px;
|
|
169
|
+
padding: 2px 10px;
|
|
170
|
+
border-radius: 99px;
|
|
171
|
+
height: 24px;
|
|
172
|
+
font-variant-numeric: tabular-nums;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.digito-wc-resend {
|
|
176
|
+
display: none;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 8px;
|
|
179
|
+
font-size: 14px;
|
|
180
|
+
color: var(--digito-timer-color, #5C5C5C);
|
|
181
|
+
padding: 12px 0 0;
|
|
182
|
+
}
|
|
183
|
+
.digito-wc-resend.is-visible { display: flex; }
|
|
184
|
+
.digito-wc-resend-btn {
|
|
185
|
+
display: inline-flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
background: #E8E8E8;
|
|
188
|
+
border: none;
|
|
189
|
+
padding: 2px 10px;
|
|
190
|
+
border-radius: 99px;
|
|
191
|
+
color: #0A0A0A;
|
|
192
|
+
font-weight: 500;
|
|
193
|
+
font-size: 14px;
|
|
194
|
+
transition: background 150ms ease;
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
height: 24px;
|
|
197
|
+
font-family: inherit;
|
|
198
|
+
}
|
|
199
|
+
.digito-wc-resend-btn:hover { background: #E5E5E5; }
|
|
200
|
+
.digito-wc-resend-btn:disabled { color: #A1A1A1; cursor: not-allowed; background: #F5F5F5; }
|
|
201
|
+
`;
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
// HELPERS
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
function formatCountdown(totalSeconds) {
|
|
206
|
+
const m = Math.floor(totalSeconds / 60);
|
|
207
|
+
const s = totalSeconds % 60;
|
|
208
|
+
return m > 0 ? `${m}:${String(s).padStart(2, '0')}` : `0:${String(s).padStart(2, '0')}`;
|
|
209
|
+
}
|
|
210
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
211
|
+
// WEB COMPONENT
|
|
212
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
class DigitoInput extends HTMLElement {
|
|
214
|
+
/** Called when all slots are filled. Also dispatches the `complete` CustomEvent. */
|
|
215
|
+
set onComplete(fn) {
|
|
216
|
+
if (fn !== undefined && typeof fn !== 'function') {
|
|
217
|
+
console.warn('[digito] onComplete must be a function, got:', typeof fn);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this._onComplete = fn;
|
|
221
|
+
}
|
|
222
|
+
/** Called when the built-in Resend button is clicked. */
|
|
223
|
+
set onResend(fn) {
|
|
224
|
+
if (fn !== undefined && typeof fn !== 'function') {
|
|
225
|
+
console.warn('[digito] onResend must be a function, got:', typeof fn);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this._onResend = fn;
|
|
229
|
+
}
|
|
230
|
+
/** Fires when the hidden input receives focus. Set as JS property. */
|
|
231
|
+
set onFocus(fn) {
|
|
232
|
+
if (fn !== undefined && typeof fn !== 'function') {
|
|
233
|
+
console.warn('[digito] onFocus must be a function, got:', typeof fn);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
this._onFocus = fn;
|
|
237
|
+
}
|
|
238
|
+
/** Fires when the hidden input loses focus. Set as JS property. */
|
|
239
|
+
set onBlur(fn) {
|
|
240
|
+
if (fn !== undefined && typeof fn !== 'function') {
|
|
241
|
+
console.warn('[digito] onBlur must be a function, got:', typeof fn);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this._onBlur = fn;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Fires when a typed character is rejected by type/pattern validation.
|
|
248
|
+
* Receives the character and the slot index it was attempted on.
|
|
249
|
+
* Set as JS property.
|
|
250
|
+
*/
|
|
251
|
+
set onInvalidChar(fn) {
|
|
252
|
+
if (fn !== undefined && typeof fn !== 'function') {
|
|
253
|
+
console.warn('[digito] onInvalidChar must be a function, got:', typeof fn);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this._onInvalidChar = fn;
|
|
257
|
+
if (this.shadow.children.length > 0)
|
|
258
|
+
this.build();
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Arbitrary per-character regex. When set, each typed/pasted character must
|
|
262
|
+
* match to be accepted. Takes precedence over the type attribute for
|
|
263
|
+
* character validation. Cannot be expressed as an HTML attribute — set as a
|
|
264
|
+
* JS property instead.
|
|
265
|
+
* @example el.pattern = /^[0-9A-F]$/
|
|
266
|
+
*/
|
|
267
|
+
set pattern(re) {
|
|
268
|
+
if (re !== undefined && !(re instanceof RegExp)) {
|
|
269
|
+
console.warn('[digito] pattern must be a RegExp, got:', typeof re);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this._pattern = re;
|
|
273
|
+
if (this.shadow.children.length > 0)
|
|
274
|
+
this.build();
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Optional paste transformer function. Applied to raw clipboard text before
|
|
278
|
+
* filtering. Use to strip formatting (e.g. `"G-123456"` → `"123456"`).
|
|
279
|
+
* Cannot be expressed as an HTML attribute — set as a JS property.
|
|
280
|
+
* @example el.pasteTransformer = (raw) => raw.replace(/\s+|-/g, '')
|
|
281
|
+
*/
|
|
282
|
+
set pasteTransformer(fn) {
|
|
283
|
+
if (fn !== undefined && typeof fn !== 'function') {
|
|
284
|
+
console.warn('[digito] pasteTransformer must be a function, got:', typeof fn);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this._pasteTransformer = fn;
|
|
288
|
+
if (this.shadow.children.length > 0)
|
|
289
|
+
this.build();
|
|
290
|
+
}
|
|
291
|
+
constructor() {
|
|
292
|
+
super();
|
|
293
|
+
// Shadow DOM references — rebuilt in full on every attributeChangedCallback.
|
|
294
|
+
this.slotEls = [];
|
|
295
|
+
this.caretEls = [];
|
|
296
|
+
this.hiddenInput = null;
|
|
297
|
+
this.timerEl = null;
|
|
298
|
+
this.timerBadgeEl = null;
|
|
299
|
+
this.resendEl = null;
|
|
300
|
+
this.timerCtrl = null;
|
|
301
|
+
this.resendCountdown = null;
|
|
302
|
+
this.digito = null;
|
|
303
|
+
// Runtime mutable state — toggled by setDisabled() without a full rebuild.
|
|
304
|
+
this._isDisabled = false;
|
|
305
|
+
this._isSuccess = false;
|
|
306
|
+
// JS-property-only options. These cannot be expressed as HTML attributes
|
|
307
|
+
// (RegExp and functions are not serialisable to strings), so they are stored
|
|
308
|
+
// here and applied on every build().
|
|
309
|
+
this._pattern = undefined;
|
|
310
|
+
this._pasteTransformer = undefined;
|
|
311
|
+
this._onComplete = undefined;
|
|
312
|
+
this._onResend = undefined;
|
|
313
|
+
this._onFocus = undefined;
|
|
314
|
+
this._onBlur = undefined;
|
|
315
|
+
this._onInvalidChar = undefined;
|
|
316
|
+
// Open shadow root so external CSS custom properties (--digito-*) cascade in.
|
|
317
|
+
this.shadow = this.attachShadow({ mode: 'open' });
|
|
318
|
+
}
|
|
319
|
+
/** Called when the element is inserted into the DOM. Triggers the initial build. */
|
|
320
|
+
connectedCallback() { this.build(); }
|
|
321
|
+
/**
|
|
322
|
+
* Called when the element is removed from the DOM.
|
|
323
|
+
* Stops both timers and cancels any pending `onComplete` timeout to avoid
|
|
324
|
+
* callbacks firing after the element is detached.
|
|
325
|
+
*/
|
|
326
|
+
disconnectedCallback() {
|
|
327
|
+
this.timerCtrl?.stop();
|
|
328
|
+
this.resendCountdown?.stop();
|
|
329
|
+
this.digito?.resetState();
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Called when any observed attribute changes after the initial connection.
|
|
333
|
+
* Guards on `shadow.children.length > 0` so it does not fire before
|
|
334
|
+
* `connectedCallback` has completed the first build.
|
|
335
|
+
*/
|
|
336
|
+
attributeChangedCallback() {
|
|
337
|
+
if (this.shadow.children.length > 0)
|
|
338
|
+
this.build();
|
|
339
|
+
}
|
|
340
|
+
// ── Attribute accessors ─────────────────────────────────────────────────────
|
|
341
|
+
// Each getter reads directly from the live attribute to stay in sync with
|
|
342
|
+
// external attribute mutations. All values are snapshotted at the top of
|
|
343
|
+
// build() so a single rebuild is always internally consistent.
|
|
344
|
+
get _length() {
|
|
345
|
+
const v = parseInt(this.getAttribute('length') ?? '6', 10);
|
|
346
|
+
return isNaN(v) || v < 1 ? 6 : Math.floor(v);
|
|
347
|
+
}
|
|
348
|
+
get _type() { return (this.getAttribute('type') ?? 'numeric'); }
|
|
349
|
+
get _timer() {
|
|
350
|
+
const v = parseInt(this.getAttribute('timer') ?? '0', 10);
|
|
351
|
+
return isNaN(v) || v < 0 ? 0 : Math.floor(v);
|
|
352
|
+
}
|
|
353
|
+
get _resendAfter() {
|
|
354
|
+
const v = parseInt(this.getAttribute('resend-after') ?? '30', 10);
|
|
355
|
+
return isNaN(v) || v < 1 ? 30 : Math.floor(v);
|
|
356
|
+
}
|
|
357
|
+
get _disabledAttr() { return this.hasAttribute('disabled'); }
|
|
358
|
+
/** Parses `separator-after="2,4"` into `[2, 4]`. Filters NaN and zero values. */
|
|
359
|
+
get _separatorAfter() {
|
|
360
|
+
const v = this.getAttribute('separator-after');
|
|
361
|
+
if (!v)
|
|
362
|
+
return [];
|
|
363
|
+
return v.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n) && n > 0);
|
|
364
|
+
}
|
|
365
|
+
get _separator() { return this.getAttribute('separator') ?? '—'; }
|
|
366
|
+
/** `masked` is a boolean attribute — present means true, absent means false. */
|
|
367
|
+
get _masked() { return this.hasAttribute('masked'); }
|
|
368
|
+
get _maskChar() { return this.getAttribute('mask-char') ?? '\u25CF'; }
|
|
369
|
+
get _name() { return this.getAttribute('name') ?? ''; }
|
|
370
|
+
get _placeholder() { return this.getAttribute('placeholder') ?? ''; }
|
|
371
|
+
/**
|
|
372
|
+
* `auto-focus` defaults to `true` when the attribute is absent.
|
|
373
|
+
* Setting `auto-focus="false"` explicitly suppresses focus on mount.
|
|
374
|
+
*/
|
|
375
|
+
get _autoFocus() { return !this.hasAttribute('auto-focus') || this.getAttribute('auto-focus') !== 'false'; }
|
|
376
|
+
get _selectOnFocus() { return this.hasAttribute('select-on-focus'); }
|
|
377
|
+
get _blurOnComplete() { return this.hasAttribute('blur-on-complete'); }
|
|
378
|
+
// ── Build ───────────────────────────────────────────────────────────────────
|
|
379
|
+
/**
|
|
380
|
+
* Constructs the entire shadow DOM from scratch.
|
|
381
|
+
*
|
|
382
|
+
* Called on first connect, on every observed attribute change, and when
|
|
383
|
+
* certain JS-property setters (`pattern`, `pasteTransformer`, `onInvalidChar`)
|
|
384
|
+
* are assigned after mount. Tears down any running timer and resets the
|
|
385
|
+
* state machine before rebuilding to prevent duplicate intervals or stale
|
|
386
|
+
* closure references from the previous build.
|
|
387
|
+
*/
|
|
388
|
+
build() {
|
|
389
|
+
const length = this._length;
|
|
390
|
+
const type = this._type;
|
|
391
|
+
const timerSecs = this._timer;
|
|
392
|
+
const resendCooldown = this._resendAfter;
|
|
393
|
+
const separatorPositions = this._separatorAfter;
|
|
394
|
+
const separator = this._separator;
|
|
395
|
+
const masked = this._masked;
|
|
396
|
+
const inputName = this._name;
|
|
397
|
+
const autoFocus = this._autoFocus;
|
|
398
|
+
const selectOnFocus = this._selectOnFocus;
|
|
399
|
+
const blurOnComplete = this._blurOnComplete;
|
|
400
|
+
this._isDisabled = this._disabledAttr;
|
|
401
|
+
this.timerCtrl?.stop();
|
|
402
|
+
this.resendCountdown?.stop();
|
|
403
|
+
this.digito?.resetState();
|
|
404
|
+
// Clear shadow DOM using safe child removal
|
|
405
|
+
while (this.shadow.firstChild)
|
|
406
|
+
this.shadow.removeChild(this.shadow.firstChild);
|
|
407
|
+
this.slotEls = [];
|
|
408
|
+
this.caretEls = [];
|
|
409
|
+
this.timerEl = null;
|
|
410
|
+
this.timerBadgeEl = null;
|
|
411
|
+
this.resendEl = null;
|
|
412
|
+
this.timerCtrl = null;
|
|
413
|
+
this.resendCountdown = null;
|
|
414
|
+
// Styles
|
|
415
|
+
const styleEl = document.createElement('style');
|
|
416
|
+
styleEl.textContent = STYLES;
|
|
417
|
+
this.shadow.appendChild(styleEl);
|
|
418
|
+
// Root
|
|
419
|
+
const rootEl = document.createElement('div');
|
|
420
|
+
rootEl.className = 'digito-wc-root';
|
|
421
|
+
// Slot row
|
|
422
|
+
const slotRowEl = document.createElement('div');
|
|
423
|
+
slotRowEl.className = 'digito-wc-slots';
|
|
424
|
+
// Visual slots + optional separator
|
|
425
|
+
for (let i = 0; i < length; i++) {
|
|
426
|
+
const slotEl = document.createElement('div');
|
|
427
|
+
slotEl.className = 'digito-wc-slot';
|
|
428
|
+
slotEl.setAttribute('aria-hidden', 'true');
|
|
429
|
+
const caretEl = document.createElement('div');
|
|
430
|
+
caretEl.className = 'digito-wc-caret';
|
|
431
|
+
slotEl.appendChild(caretEl);
|
|
432
|
+
this.caretEls.push(caretEl);
|
|
433
|
+
this.slotEls.push(slotEl);
|
|
434
|
+
slotRowEl.appendChild(slotEl);
|
|
435
|
+
if (separatorPositions.some(pos => i === pos - 1)) {
|
|
436
|
+
const sepEl = document.createElement('div');
|
|
437
|
+
sepEl.className = 'digito-wc-separator';
|
|
438
|
+
sepEl.textContent = separator;
|
|
439
|
+
sepEl.setAttribute('aria-hidden', 'true');
|
|
440
|
+
slotRowEl.appendChild(sepEl);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Hidden input
|
|
444
|
+
const hiddenInput = document.createElement('input');
|
|
445
|
+
hiddenInput.type = masked ? 'password' : 'text';
|
|
446
|
+
hiddenInput.inputMode = type === 'numeric' ? 'numeric' : 'text';
|
|
447
|
+
hiddenInput.autocomplete = 'one-time-code';
|
|
448
|
+
hiddenInput.maxLength = length;
|
|
449
|
+
hiddenInput.disabled = this._isDisabled;
|
|
450
|
+
hiddenInput.className = 'digito-wc-hidden';
|
|
451
|
+
hiddenInput.setAttribute('aria-label', `Enter your ${length}-${type === 'numeric' ? 'digit' : 'character'} code`);
|
|
452
|
+
hiddenInput.setAttribute('spellcheck', 'false');
|
|
453
|
+
hiddenInput.setAttribute('autocorrect', 'off');
|
|
454
|
+
hiddenInput.setAttribute('autocapitalize', 'off');
|
|
455
|
+
if (inputName)
|
|
456
|
+
hiddenInput.name = inputName;
|
|
457
|
+
this.hiddenInput = hiddenInput;
|
|
458
|
+
rootEl.appendChild(slotRowEl);
|
|
459
|
+
rootEl.appendChild(hiddenInput);
|
|
460
|
+
this.shadow.appendChild(rootEl);
|
|
461
|
+
// Core
|
|
462
|
+
this.digito = createDigito({
|
|
463
|
+
length,
|
|
464
|
+
type,
|
|
465
|
+
pattern: this._pattern,
|
|
466
|
+
pasteTransformer: this._pasteTransformer,
|
|
467
|
+
onInvalidChar: this._onInvalidChar,
|
|
468
|
+
onComplete: (code) => {
|
|
469
|
+
// Call JS property setter AND dispatch CustomEvent
|
|
470
|
+
this._onComplete?.(code);
|
|
471
|
+
this.dispatchEvent(new CustomEvent('complete', { detail: { code }, bubbles: true, composed: true }));
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
// ── Built-in timer + resend (mirrors vanilla/alpine adapters) ──────────────
|
|
475
|
+
if (timerSecs > 0) {
|
|
476
|
+
// Timer footer — "Code expires in [0:45]"
|
|
477
|
+
const timerFooterEl = document.createElement('div');
|
|
478
|
+
timerFooterEl.className = 'digito-wc-timer';
|
|
479
|
+
this.timerEl = timerFooterEl;
|
|
480
|
+
const timerLabel = document.createElement('span');
|
|
481
|
+
timerLabel.className = 'digito-wc-timer-label';
|
|
482
|
+
timerLabel.textContent = 'Code expires in';
|
|
483
|
+
const timerBadge = document.createElement('span');
|
|
484
|
+
timerBadge.className = 'digito-wc-timer-badge';
|
|
485
|
+
timerBadge.textContent = formatCountdown(timerSecs);
|
|
486
|
+
this.timerBadgeEl = timerBadge;
|
|
487
|
+
timerFooterEl.appendChild(timerLabel);
|
|
488
|
+
timerFooterEl.appendChild(timerBadge);
|
|
489
|
+
rootEl.appendChild(timerFooterEl);
|
|
490
|
+
// Resend row — "Didn't receive the code? [Resend]"
|
|
491
|
+
const resendRowEl = document.createElement('div');
|
|
492
|
+
resendRowEl.className = 'digito-wc-resend';
|
|
493
|
+
this.resendEl = resendRowEl;
|
|
494
|
+
const resendLabel = document.createElement('span');
|
|
495
|
+
resendLabel.textContent = 'Didn\u2019t receive the code?';
|
|
496
|
+
const resendBtn = document.createElement('button');
|
|
497
|
+
resendBtn.className = 'digito-wc-resend-btn';
|
|
498
|
+
resendBtn.textContent = 'Resend';
|
|
499
|
+
resendBtn.type = 'button';
|
|
500
|
+
resendRowEl.appendChild(resendLabel);
|
|
501
|
+
resendRowEl.appendChild(resendBtn);
|
|
502
|
+
rootEl.appendChild(resendRowEl);
|
|
503
|
+
// Main countdown
|
|
504
|
+
this.timerCtrl = createTimer({
|
|
505
|
+
totalSeconds: timerSecs,
|
|
506
|
+
onTick: (r) => { if (this.timerBadgeEl)
|
|
507
|
+
this.timerBadgeEl.textContent = formatCountdown(r); },
|
|
508
|
+
onExpire: () => {
|
|
509
|
+
if (this.timerEl)
|
|
510
|
+
this.timerEl.style.display = 'none';
|
|
511
|
+
if (this.resendEl)
|
|
512
|
+
this.resendEl.classList.add('is-visible');
|
|
513
|
+
this.dispatchEvent(new CustomEvent('expire', { bubbles: true, composed: true }));
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
this.timerCtrl.start();
|
|
517
|
+
// Resend button click — restart with resend cooldown
|
|
518
|
+
resendBtn.addEventListener('click', () => {
|
|
519
|
+
if (!this.timerEl || !this.timerBadgeEl || !this.resendEl)
|
|
520
|
+
return;
|
|
521
|
+
this.resendEl.classList.remove('is-visible');
|
|
522
|
+
this.timerEl.style.display = 'flex';
|
|
523
|
+
this.timerBadgeEl.textContent = formatCountdown(resendCooldown);
|
|
524
|
+
this.resendCountdown?.stop();
|
|
525
|
+
this.resendCountdown = createTimer({
|
|
526
|
+
totalSeconds: resendCooldown,
|
|
527
|
+
onTick: (r) => { if (this.timerBadgeEl)
|
|
528
|
+
this.timerBadgeEl.textContent = formatCountdown(r); },
|
|
529
|
+
onExpire: () => {
|
|
530
|
+
if (this.timerEl)
|
|
531
|
+
this.timerEl.style.display = 'none';
|
|
532
|
+
if (this.resendEl)
|
|
533
|
+
this.resendEl.classList.add('is-visible');
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
this.resendCountdown.start();
|
|
537
|
+
this._onResend?.();
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
this.attachEvents(selectOnFocus, blurOnComplete);
|
|
541
|
+
if (this._isDisabled)
|
|
542
|
+
this.applyDisabledDOM(true);
|
|
543
|
+
hiddenInput.addEventListener('click', (e) => {
|
|
544
|
+
if (this._isDisabled)
|
|
545
|
+
return;
|
|
546
|
+
// click fires after the browser places cursor (always 0 due to font-size:1px).
|
|
547
|
+
// Coordinate hit-test determines which slot was visually clicked, then
|
|
548
|
+
// setSelectionRange overrides the browser's placement.
|
|
549
|
+
let rawSlot = this.slotEls.length - 1;
|
|
550
|
+
for (let i = 0; i < this.slotEls.length; i++) {
|
|
551
|
+
if (e.clientX <= this.slotEls[i].getBoundingClientRect().right) {
|
|
552
|
+
rawSlot = i;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Clamp to filled count so the visual active slot matches the actual cursor position.
|
|
557
|
+
const clickedSlot = Math.min(rawSlot, hiddenInput.value.length);
|
|
558
|
+
this.digito?.moveFocusTo(clickedSlot);
|
|
559
|
+
const char = this.digito?.state.slotValues[clickedSlot] ?? '';
|
|
560
|
+
if (selectOnFocus && char) {
|
|
561
|
+
hiddenInput.setSelectionRange(clickedSlot, clickedSlot + 1);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
hiddenInput.setSelectionRange(clickedSlot, clickedSlot);
|
|
565
|
+
}
|
|
566
|
+
this.syncSlotsToDOM();
|
|
567
|
+
});
|
|
568
|
+
requestAnimationFrame(() => {
|
|
569
|
+
if (!this._isDisabled && autoFocus)
|
|
570
|
+
hiddenInput.focus();
|
|
571
|
+
hiddenInput.setSelectionRange(0, 0);
|
|
572
|
+
this.syncSlotsToDOM();
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
// ── DOM sync ────────────────────────────────────────────────────────────────
|
|
576
|
+
/**
|
|
577
|
+
* Reconcile the shadow slot divs with the current core state using CSS class
|
|
578
|
+
* toggles. Called after every user action (input, keydown, paste, focus, click).
|
|
579
|
+
*
|
|
580
|
+
* Uses `this.shadow.activeElement` instead of `document.activeElement` to
|
|
581
|
+
* correctly detect focus within the shadow root across all browsers — the
|
|
582
|
+
* document active element is the host `<digito-input>` element, not the
|
|
583
|
+
* internal hidden input.
|
|
584
|
+
*/
|
|
585
|
+
syncSlotsToDOM() {
|
|
586
|
+
if (!this.digito || !this.hiddenInput)
|
|
587
|
+
return;
|
|
588
|
+
const { slotValues, activeSlot, hasError } = this.digito.state;
|
|
589
|
+
const focused = this.shadow.activeElement === this.hiddenInput;
|
|
590
|
+
this.slotEls.forEach((slotEl, i) => {
|
|
591
|
+
const char = slotValues[i] ?? '';
|
|
592
|
+
const isActive = i === activeSlot && focused;
|
|
593
|
+
let textNode = slotEl.childNodes[1];
|
|
594
|
+
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
|
|
595
|
+
textNode = document.createTextNode('');
|
|
596
|
+
slotEl.appendChild(textNode);
|
|
597
|
+
}
|
|
598
|
+
textNode.nodeValue = this._masked && char ? this._maskChar : char || this._placeholder;
|
|
599
|
+
slotEl.classList.toggle('is-active', isActive && !this._isDisabled);
|
|
600
|
+
slotEl.classList.toggle('is-filled', !!char);
|
|
601
|
+
slotEl.classList.toggle('is-masked', this._masked);
|
|
602
|
+
slotEl.classList.toggle('is-error', hasError);
|
|
603
|
+
slotEl.classList.toggle('is-success', this._isSuccess);
|
|
604
|
+
slotEl.classList.toggle('is-disabled', this._isDisabled);
|
|
605
|
+
this.caretEls[i].style.display = isActive && !char && !this._isDisabled ? 'block' : 'none';
|
|
606
|
+
});
|
|
607
|
+
// Only update value when it actually differs — assigning the same string
|
|
608
|
+
// resets selectionStart/End in some browsers, clobbering the cursor.
|
|
609
|
+
const newValue = slotValues.join('');
|
|
610
|
+
if (this.hiddenInput.value !== newValue)
|
|
611
|
+
this.hiddenInput.value = newValue;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Apply or remove the disabled state directly on existing DOM nodes without
|
|
615
|
+
* triggering a full rebuild. Used by both `build()` (initial disabled attr)
|
|
616
|
+
* and `setDisabled()` (runtime toggle).
|
|
617
|
+
*/
|
|
618
|
+
applyDisabledDOM(value) {
|
|
619
|
+
if (this.hiddenInput)
|
|
620
|
+
this.hiddenInput.disabled = value;
|
|
621
|
+
this.slotEls.forEach(s => s.classList.toggle('is-disabled', value));
|
|
622
|
+
}
|
|
623
|
+
// ── Events ──────────────────────────────────────────────────────────────────
|
|
624
|
+
/**
|
|
625
|
+
* Wire all event listeners to the hidden input element.
|
|
626
|
+
* Called once at the end of each `build()`. Because `build()` creates a fresh
|
|
627
|
+
* `hiddenInput` element, there is no need to `removeEventListener` — the old
|
|
628
|
+
* element is discarded and its listeners are garbage-collected with it.
|
|
629
|
+
*
|
|
630
|
+
* @param selectOnFocus When `true`, focusing a filled slot selects its character.
|
|
631
|
+
* @param blurOnComplete When `true`, blurs the input after the last slot is filled.
|
|
632
|
+
*/
|
|
633
|
+
attachEvents(selectOnFocus, blurOnComplete) {
|
|
634
|
+
const input = this.hiddenInput;
|
|
635
|
+
const digito = this.digito;
|
|
636
|
+
const length = this._length;
|
|
637
|
+
const type = this._type;
|
|
638
|
+
const pattern = this._pattern;
|
|
639
|
+
input.addEventListener('keydown', (e) => {
|
|
640
|
+
if (this._isDisabled)
|
|
641
|
+
return;
|
|
642
|
+
const pos = input.selectionStart ?? 0;
|
|
643
|
+
if (e.key === 'Backspace') {
|
|
644
|
+
e.preventDefault();
|
|
645
|
+
digito.deleteChar(pos);
|
|
646
|
+
this.syncSlotsToDOM();
|
|
647
|
+
this.dispatchChange();
|
|
648
|
+
const next = digito.state.activeSlot;
|
|
649
|
+
requestAnimationFrame(() => input.setSelectionRange(next, next));
|
|
650
|
+
}
|
|
651
|
+
else if (e.key === 'ArrowLeft') {
|
|
652
|
+
e.preventDefault();
|
|
653
|
+
digito.moveFocusLeft(pos);
|
|
654
|
+
this.syncSlotsToDOM();
|
|
655
|
+
requestAnimationFrame(() => input.setSelectionRange(digito.state.activeSlot, digito.state.activeSlot));
|
|
656
|
+
}
|
|
657
|
+
else if (e.key === 'ArrowRight') {
|
|
658
|
+
e.preventDefault();
|
|
659
|
+
digito.moveFocusRight(pos);
|
|
660
|
+
this.syncSlotsToDOM();
|
|
661
|
+
requestAnimationFrame(() => input.setSelectionRange(digito.state.activeSlot, digito.state.activeSlot));
|
|
662
|
+
}
|
|
663
|
+
else if (e.key === 'Tab') {
|
|
664
|
+
if (e.shiftKey) {
|
|
665
|
+
if (pos === 0)
|
|
666
|
+
return;
|
|
667
|
+
e.preventDefault();
|
|
668
|
+
digito.moveFocusLeft(pos);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
if (!digito.state.slotValues[pos])
|
|
672
|
+
return;
|
|
673
|
+
if (pos >= length - 1)
|
|
674
|
+
return;
|
|
675
|
+
e.preventDefault();
|
|
676
|
+
digito.moveFocusRight(pos);
|
|
677
|
+
}
|
|
678
|
+
this.syncSlotsToDOM();
|
|
679
|
+
const next = digito.state.activeSlot;
|
|
680
|
+
requestAnimationFrame(() => input.setSelectionRange(next, next));
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
input.addEventListener('input', () => {
|
|
684
|
+
if (this._isDisabled)
|
|
685
|
+
return;
|
|
686
|
+
const raw = input.value;
|
|
687
|
+
if (!raw) {
|
|
688
|
+
digito.resetState();
|
|
689
|
+
input.value = '';
|
|
690
|
+
input.setSelectionRange(0, 0);
|
|
691
|
+
this.syncSlotsToDOM();
|
|
692
|
+
this.dispatchChange();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const valid = filterString(raw, type, pattern).slice(0, length);
|
|
696
|
+
digito.resetState();
|
|
697
|
+
for (let i = 0; i < valid.length; i++)
|
|
698
|
+
digito.inputChar(i, valid[i]);
|
|
699
|
+
const next = Math.min(valid.length, length - 1);
|
|
700
|
+
input.value = valid;
|
|
701
|
+
input.setSelectionRange(next, next);
|
|
702
|
+
digito.moveFocusTo(next);
|
|
703
|
+
this.syncSlotsToDOM();
|
|
704
|
+
this.dispatchChange();
|
|
705
|
+
if (blurOnComplete && digito.state.isComplete) {
|
|
706
|
+
requestAnimationFrame(() => input.blur());
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
input.addEventListener('paste', (e) => {
|
|
710
|
+
if (this._isDisabled)
|
|
711
|
+
return;
|
|
712
|
+
e.preventDefault();
|
|
713
|
+
const text = e.clipboardData?.getData('text') ?? '';
|
|
714
|
+
const pos = input.selectionStart ?? 0;
|
|
715
|
+
digito.pasteString(pos, text);
|
|
716
|
+
const { slotValues, activeSlot } = digito.state;
|
|
717
|
+
input.value = slotValues.join('');
|
|
718
|
+
input.setSelectionRange(activeSlot, activeSlot);
|
|
719
|
+
this.syncSlotsToDOM();
|
|
720
|
+
this.dispatchChange();
|
|
721
|
+
if (blurOnComplete && digito.state.isComplete) {
|
|
722
|
+
requestAnimationFrame(() => input.blur());
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
input.addEventListener('focus', () => {
|
|
726
|
+
this._onFocus?.();
|
|
727
|
+
requestAnimationFrame(() => {
|
|
728
|
+
const pos = digito.state.activeSlot;
|
|
729
|
+
const char = digito.state.slotValues[pos];
|
|
730
|
+
if (selectOnFocus && char) {
|
|
731
|
+
input.setSelectionRange(pos, pos + 1);
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
input.setSelectionRange(pos, pos);
|
|
735
|
+
}
|
|
736
|
+
this.syncSlotsToDOM();
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
input.addEventListener('blur', () => {
|
|
740
|
+
this._onBlur?.();
|
|
741
|
+
this.slotEls.forEach(s => { s.classList.remove('is-active'); });
|
|
742
|
+
this.caretEls.forEach(c => { c.style.display = 'none'; });
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Dispatch a `change` CustomEvent carrying the current code string.
|
|
747
|
+
* Fired after every input, paste, and backspace action.
|
|
748
|
+
* `composed: true` lets the event cross the shadow root boundary so host-page
|
|
749
|
+
* listeners registered with `el.addEventListener('change', ...)` receive it.
|
|
750
|
+
*/
|
|
751
|
+
dispatchChange() {
|
|
752
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
753
|
+
detail: { code: this.digito?.getCode() ?? '' },
|
|
754
|
+
bubbles: true,
|
|
755
|
+
composed: true,
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
// ── Public DOM API ──────────────────────────────────────────────────────────
|
|
759
|
+
/** Clear all slots, reset the timer display, and re-focus the hidden input. */
|
|
760
|
+
reset() {
|
|
761
|
+
this._isSuccess = false;
|
|
762
|
+
this.digito?.resetState();
|
|
763
|
+
if (this.hiddenInput) {
|
|
764
|
+
this.hiddenInput.value = '';
|
|
765
|
+
if (!this._isDisabled)
|
|
766
|
+
this.hiddenInput.focus();
|
|
767
|
+
this.hiddenInput.setSelectionRange(0, 0);
|
|
768
|
+
}
|
|
769
|
+
if (this.timerBadgeEl)
|
|
770
|
+
this.timerBadgeEl.textContent = formatCountdown(this._timer);
|
|
771
|
+
if (this.timerEl)
|
|
772
|
+
this.timerEl.style.display = 'flex';
|
|
773
|
+
if (this.resendEl)
|
|
774
|
+
this.resendEl.classList.remove('is-visible');
|
|
775
|
+
this.resendCountdown?.stop();
|
|
776
|
+
this.timerCtrl?.restart();
|
|
777
|
+
this.syncSlotsToDOM();
|
|
778
|
+
}
|
|
779
|
+
/** Apply or clear the error state on all visual slots. */
|
|
780
|
+
setError(isError) {
|
|
781
|
+
if (isError)
|
|
782
|
+
this._isSuccess = false;
|
|
783
|
+
this.digito?.setError(isError);
|
|
784
|
+
this.syncSlotsToDOM();
|
|
785
|
+
}
|
|
786
|
+
/** Apply or clear the success state on all visual slots. Stops the timer on success. */
|
|
787
|
+
setSuccess(isSuccess) {
|
|
788
|
+
this._isSuccess = isSuccess;
|
|
789
|
+
if (isSuccess) {
|
|
790
|
+
this.digito?.setError(false);
|
|
791
|
+
this.timerCtrl?.stop();
|
|
792
|
+
this.resendCountdown?.stop();
|
|
793
|
+
if (this.timerEl)
|
|
794
|
+
this.timerEl.style.display = 'none';
|
|
795
|
+
if (this.resendEl)
|
|
796
|
+
this.resendEl.style.display = 'none';
|
|
797
|
+
}
|
|
798
|
+
this.syncSlotsToDOM();
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Enable or disable the input at runtime.
|
|
802
|
+
* Equivalent to toggling the `disabled` HTML attribute but without triggering
|
|
803
|
+
* a full rebuild. Re-enabling automatically restores focus to the active slot.
|
|
804
|
+
*/
|
|
805
|
+
setDisabled(value) {
|
|
806
|
+
this._isDisabled = value;
|
|
807
|
+
this.digito?.setDisabled(value);
|
|
808
|
+
this.applyDisabledDOM(value);
|
|
809
|
+
this.syncSlotsToDOM();
|
|
810
|
+
if (!value && this.hiddenInput) {
|
|
811
|
+
requestAnimationFrame(() => {
|
|
812
|
+
this.hiddenInput?.focus();
|
|
813
|
+
this.hiddenInput?.setSelectionRange(this.digito?.state.activeSlot ?? 0, this.digito?.state.activeSlot ?? 0);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/** Returns the current code as a joined string (e.g. `"123456"`). */
|
|
818
|
+
getCode() {
|
|
819
|
+
return this.digito?.getCode() ?? '';
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* HTML attribute names whose changes trigger `attributeChangedCallback`.
|
|
824
|
+
* Any change to these attributes causes a full shadow DOM rebuild so the
|
|
825
|
+
* component always reflects its attribute state without manual reconciliation.
|
|
826
|
+
*/
|
|
827
|
+
DigitoInput.observedAttributes = ['length', 'type', 'timer', 'resend-after', 'disabled', 'separator-after', 'separator', 'masked', 'mask-char', 'name', 'placeholder', 'auto-focus', 'select-on-focus', 'blur-on-complete'];
|
|
828
|
+
if (typeof customElements !== 'undefined' && !customElements.get('digito-input')) {
|
|
829
|
+
customElements.define('digito-input', DigitoInput);
|
|
830
|
+
}
|
|
831
|
+
export { DigitoInput };
|
|
832
|
+
//# sourceMappingURL=web-component.js.map
|