mother-mask 2.0.4 → 3.0.1
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/README.md +181 -53
- package/dist/mother-mask.cjs +1 -1
- package/dist/mother-mask.cjs.map +1 -1
- package/dist/mother-mask.d.cts +82 -17
- package/dist/mother-mask.d.mts +82 -17
- package/dist/mother-mask.mjs +1 -1
- package/dist/mother-mask.mjs.map +1 -1
- package/dist/mother-mask.umd.js +1 -1
- package/dist/mother-mask.umd.js.map +1 -1
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# mother-mask
|
|
2
2
|
|
|
3
|
-
Lightweight input mask library for browsers. Zero dependencies, TypeScript-first, ships ESM
|
|
3
|
+
Lightweight input mask library for browsers. Zero runtime dependencies, TypeScript-first, ships **ESM**, **CJS**, and **UMD**.
|
|
4
|
+
|
|
5
|
+
Published as [`mother-mask` on npm](https://www.npmjs.com/package/mother-mask).
|
|
6
|
+
|
|
7
|
+
## Live demo
|
|
8
|
+
|
|
9
|
+
**[Try it on StackBlitz →](https://stackblitz.com/edit/mother-mask-simple-demo?file=src%2Fmain.ts)**
|
|
4
10
|
|
|
5
11
|
## Install
|
|
6
12
|
|
|
@@ -12,9 +18,13 @@ pnpm add mother-mask
|
|
|
12
18
|
|
|
13
19
|
## Usage
|
|
14
20
|
|
|
15
|
-
### `bind(input, mask,
|
|
21
|
+
### `bind(input, mask, options?)`
|
|
16
22
|
|
|
17
|
-
Attach a mask to any input element
|
|
23
|
+
Attach a mask to any input element — this is the main API.
|
|
24
|
+
|
|
25
|
+
- **Idempotent** — calling `bind()` again on the same element does nothing (the element is marked with `data-masked`).
|
|
26
|
+
- **Returns a dispose function** — call it to remove listeners and attributes so you can bind again later.
|
|
27
|
+
- Sets sensible defaults when missing: `autocomplete`, `autocorrect`, `autocapitalize`, `spellcheck`, and `maxlength` from the mask.
|
|
18
28
|
|
|
19
29
|
```ts
|
|
20
30
|
import { bind } from 'mother-mask'
|
|
@@ -22,48 +32,129 @@ import { bind } from 'mother-mask'
|
|
|
22
32
|
const input = document.getElementById('phone') as HTMLInputElement
|
|
23
33
|
|
|
24
34
|
// Fixed mask
|
|
25
|
-
bind(input, '(99) 99999-9999')
|
|
35
|
+
const dispose = bind(input, '(99) 99999-9999')
|
|
26
36
|
|
|
27
|
-
// Dynamic mask —
|
|
37
|
+
// Dynamic mask — picks the pattern from an ordered list (shortest → longest)
|
|
28
38
|
bind(input, ['(99) 9999-9999', '(99) 99999-9999'])
|
|
29
39
|
|
|
30
|
-
//
|
|
40
|
+
// Callback after paste or keyboard-driven changes
|
|
31
41
|
bind(input, '999.999.999-99', (value) => {
|
|
32
42
|
console.log(value) // e.g. "123.456.789-01"
|
|
33
43
|
})
|
|
44
|
+
|
|
45
|
+
// Or options object (same as callback for a single `onChange`)
|
|
46
|
+
bind(input, '999.999.999-99', { onChange: (value) => console.log(value) })
|
|
47
|
+
|
|
48
|
+
// Later: allow rebinding
|
|
49
|
+
dispose()
|
|
34
50
|
```
|
|
35
51
|
|
|
36
|
-
###
|
|
52
|
+
### Segmented masks (default) vs. flat/reflow masks
|
|
37
53
|
|
|
38
|
-
|
|
54
|
+
By default, every mask treats its literal separators as hard boundaries
|
|
55
|
+
between independent fields. Selecting "12" in `25/12/2025` and typing a
|
|
56
|
+
shorter or longer replacement stays scoped to the month — it never bleeds
|
|
57
|
+
digits into the year, and deleting a whole field just leaves it empty
|
|
58
|
+
instead of pulling the next field left across the separator:
|
|
39
59
|
|
|
40
60
|
```ts
|
|
41
|
-
|
|
61
|
+
bind(input, '99/99/9999') // segmented by default
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is the right behavior for masks made of independent fields — dates,
|
|
65
|
+
times — and it's a safe default everywhere else too, since it never
|
|
66
|
+
*discards* data (typing past a field's capacity still flows forward into
|
|
67
|
+
the next one; only backward bleed is blocked). A fully raw, unformatted
|
|
68
|
+
paste (e.g. `25122025`) still fills every field in one pass, and array
|
|
69
|
+
masks that switch pattern width (see below) still reflow correctly.
|
|
42
70
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
Pass `segmented: false` to opt into the classic flat/reflow behavior
|
|
72
|
+
instead, where deleting or replacing characters anywhere shifts everything
|
|
73
|
+
after it to close the gap — useful when a mask really is one continuous
|
|
74
|
+
number with cosmetic separators (e.g. formatting a running total) rather
|
|
75
|
+
than independent fields:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
bind(input, '999.999.999-99', { segmented: false })
|
|
47
79
|
```
|
|
48
80
|
|
|
81
|
+
`segmented` is also accepted by `applyMask`, `buildMask`, and `process` as
|
|
82
|
+
part of an options object.
|
|
83
|
+
|
|
84
|
+
### `bindDecimal(input, options?)`
|
|
85
|
+
|
|
86
|
+
A second binder for decimal/currency-style inputs — numbers, not
|
|
87
|
+
pattern-slot strings. There's no fixed template: the integer part grows and
|
|
88
|
+
shrinks freely as the user types, and formatting (grouping, decimal places,
|
|
89
|
+
prefix/suffix) is driven entirely by `options`. Same contract as `bind()`:
|
|
90
|
+
idempotent, marked with `data-masked`, returns a dispose function.
|
|
91
|
+
|
|
92
|
+
Typing behaves like a normal number field, not a cents-first calculator:
|
|
93
|
+
digits fill the integer part until you type the decimal separator, and the
|
|
94
|
+
fixed-width fraction is always shown zero-padded. A lone placeholder "0" in
|
|
95
|
+
the integer part (an untouched field showing e.g. "$0.00") is overwritten,
|
|
96
|
+
not extended, by the next digit — typing "2" anywhere against that "0" gives
|
|
97
|
+
"$2.00", never "$20.00" or "$02.00".
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { bindDecimal } from 'mother-mask'
|
|
101
|
+
|
|
102
|
+
const input = document.querySelector<HTMLInputElement>('#decimal')!
|
|
103
|
+
|
|
104
|
+
bindDecimal(input, {
|
|
105
|
+
decimalPlaces: 2, // fixed fractional digits — default 2
|
|
106
|
+
segmented: true, // group the integer part into thousands — default true
|
|
107
|
+
separator: ',', // thousands grouping separator — default ','
|
|
108
|
+
decimalSeparator: '.', // integer/fraction separator, and its typing trigger — default '.'
|
|
109
|
+
prefix: '$',
|
|
110
|
+
suffix: '',
|
|
111
|
+
allowNegative: false, // allow a leading "-" — default false
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Typing "42" → "$42.00"
|
|
115
|
+
// Typing "423" → "$423.00"
|
|
116
|
+
// Typing "423." → "$423.00" (the separator opens the fraction segment)
|
|
117
|
+
// Typing "423.4" → "$423.40"
|
|
118
|
+
// Typing "423.42" → "$423.42"
|
|
119
|
+
|
|
120
|
+
// Editing is segmented, same as bind(): shortening "423.42" to "423.4"
|
|
121
|
+
// (deleting the trailing "2") re-pads the fraction instead of reflowing
|
|
122
|
+
// digits from the integer part → "$423.40"
|
|
123
|
+
|
|
124
|
+
// Either "." or "," opens the fraction, regardless of decimalSeparator —
|
|
125
|
+
// handy since numeric keypads often only have ".".
|
|
126
|
+
|
|
127
|
+
// Callback receives both the masked string and its parsed numeric value
|
|
128
|
+
bindDecimal(input, {
|
|
129
|
+
prefix: '$',
|
|
130
|
+
onChange: (value, numericValue) => {
|
|
131
|
+
console.log(value, numericValue) // "$423.42", 423.42
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Or a bare callback (legacy style, same as bind())
|
|
136
|
+
bindDecimal(input, (value, numericValue) => console.log(value, numericValue))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Locale note: `separator` and `decimalSeparator` are independent, so
|
|
140
|
+
`{ separator: '.', decimalSeparator: ',' }` gives the `1.234,56` format
|
|
141
|
+
common outside the US.
|
|
142
|
+
|
|
49
143
|
## Pattern syntax
|
|
50
144
|
|
|
51
|
-
| Character | Matches
|
|
52
|
-
|
|
53
|
-
| `9`
|
|
54
|
-
| `Z`
|
|
55
|
-
| `A`
|
|
56
|
-
|
|
|
145
|
+
| Character | Matches |
|
|
146
|
+
|-----------|---------|
|
|
147
|
+
| `9` | Digit (`0`–`9`) |
|
|
148
|
+
| `Z` | Letter (`a`–`z`, `A`–`Z`) |
|
|
149
|
+
| `A` | Alphanumeric (digit or letter) |
|
|
150
|
+
| Anything else | Literal — inserted as the user fills slots |
|
|
57
151
|
|
|
58
152
|
## Array masks
|
|
59
153
|
|
|
60
|
-
Pass an ordered array
|
|
154
|
+
Pass an ordered array **shortest → longest** for variable-length inputs. The active mask is chosen from the **count of alphanumeric “data” characters** in the current value, so it works for both progressively masked input and fast typing.
|
|
61
155
|
|
|
62
156
|
```ts
|
|
63
|
-
// Brazilian phone: 8-digit → 9-digit landline / mobile
|
|
64
157
|
bind(input, ['(99) 9999-9999', '(99) 99999-9999'])
|
|
65
|
-
|
|
66
|
-
// CPF / CNPJ alfanumérico
|
|
67
158
|
bind(input, ['999.999.999-99', 'AA.AAA.AAA/AAAA-99'])
|
|
68
159
|
```
|
|
69
160
|
|
|
@@ -72,51 +163,88 @@ bind(input, ['999.999.999-99', 'AA.AAA.AAA/AAAA-99'])
|
|
|
72
163
|
```html
|
|
73
164
|
<script src="https://unpkg.com/mother-mask/dist/mother-mask.umd.js"></script>
|
|
74
165
|
<script>
|
|
75
|
-
MotherMask.bind(document.getElementById('cpf'), '999.999.999-99')
|
|
166
|
+
const dispose = MotherMask.bind(document.getElementById('cpf'), '999.999.999-99')
|
|
76
167
|
</script>
|
|
77
168
|
```
|
|
78
169
|
|
|
170
|
+
The global name is **`MotherMask`**.
|
|
171
|
+
|
|
79
172
|
## API reference
|
|
80
173
|
|
|
81
|
-
|
|
82
|
-
// Apply mask to a string — no DOM required
|
|
83
|
-
process(value: string, mask: MaskPattern): string
|
|
174
|
+
### `bind` (primary)
|
|
84
175
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
): void
|
|
176
|
+
| | |
|
|
177
|
+
|--|--|
|
|
178
|
+
| **Signature** | `bind(input, mask, options?)` |
|
|
179
|
+
| **Returns** | `() => void` — call to remove listeners and attributes so the input can be bound again |
|
|
180
|
+
| **Third argument** | `{ onChange?: (value: string) => void }`, or a legacy `(value) => void` callback |
|
|
91
181
|
|
|
92
|
-
|
|
93
|
-
buildMask(value: string, mask: MaskPattern, caret?: number): Mask
|
|
182
|
+
### Other exports
|
|
94
183
|
|
|
95
|
-
|
|
96
|
-
|
|
184
|
+
| Export | Description |
|
|
185
|
+
|--------|-------------|
|
|
186
|
+
| `buildMask(value, mask, caret?, options?)` | Build a `Mask` instance (array `mask` is resolved to one string first). |
|
|
187
|
+
| `getMaxLength(mask)` | Maximum string length for the mask (for array masks, the longest pattern). |
|
|
188
|
+
| `applyMask(value, mask, inputCaret?, options?)` | Low-level: apply a **single** mask string; returns `{ value, caret }`. |
|
|
189
|
+
| `process(value, mask, options?)` | Apply a mask pattern to a raw value and return just the masked string. |
|
|
97
190
|
|
|
98
|
-
|
|
99
|
-
type MaskPattern = string | string[]
|
|
100
|
-
```
|
|
191
|
+
### `Mask` class
|
|
101
192
|
|
|
102
|
-
|
|
193
|
+
`buildMask` returns a `Mask` for advanced use. The instance applies the pattern and keeps a `caret` position aligned with the masked output (see TypeScript definitions in the package).
|
|
103
194
|
|
|
104
|
-
|
|
105
|
-
make install # install dependencies
|
|
106
|
-
make test # run tests + coverage
|
|
107
|
-
make build # build ESM + CJS + UMD
|
|
108
|
-
make dev # watch mode
|
|
109
|
-
make lint # lint source files
|
|
110
|
-
```
|
|
195
|
+
### `bindDecimal`
|
|
111
196
|
|
|
112
|
-
|
|
197
|
+
| | |
|
|
198
|
+
|--|--|
|
|
199
|
+
| **Signature** | `bindDecimal(input, options?)` |
|
|
200
|
+
| **Returns** | `() => void` — call to remove listeners and attributes so the input can be bound again |
|
|
201
|
+
| **Second argument** | `DecimalMaskOptions & { onChange?: (value: string, numericValue: number) => void }`, or a legacy `(value, numericValue) => void` callback |
|
|
113
202
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
203
|
+
### Other decimal exports
|
|
204
|
+
|
|
205
|
+
| Export | Description |
|
|
206
|
+
|--------|-------------|
|
|
207
|
+
| `applyDecimalMask(value, inputCaret?, options?)` | Low-level: format a raw/already-masked value; returns `{ value, caret }`. |
|
|
208
|
+
| `processDecimal(value, options?)` | Apply a decimal mask to a raw value and return just the masked string. |
|
|
209
|
+
| `unmaskDecimal(value, options?)` | Parse a raw or masked decimal string back into a JS `number` (`0` if it has no digits). |
|
|
210
|
+
| `formatDecimalValue(value, options?)` | Format a plain JS `number` into its masked display string — useful to pre-populate an input. |
|
|
211
|
+
|
|
212
|
+
### Types
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
type MaskPattern = string | string[]
|
|
216
|
+
|
|
217
|
+
interface MaskResult {
|
|
218
|
+
readonly value: string
|
|
219
|
+
readonly caret: number
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface ApplyMaskOptions {
|
|
223
|
+
/** Hard boundaries between fields — on by default. Pass `false` for flat/reflow. */
|
|
224
|
+
segmented?: boolean
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface BindOptions extends ApplyMaskOptions {
|
|
228
|
+
onChange?: (value: string) => void
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface DecimalMaskOptions {
|
|
232
|
+
decimalPlaces?: number // default 2
|
|
233
|
+
segmented?: boolean // group into thousands — default true
|
|
234
|
+
separator?: string // thousands separator — default ','
|
|
235
|
+
decimalSeparator?: string // default '.'
|
|
236
|
+
prefix?: string // default ''
|
|
237
|
+
suffix?: string // default ''
|
|
238
|
+
allowNegative?: boolean // default false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
interface BindDecimalOptions extends DecimalMaskOptions {
|
|
242
|
+
onChange?: (value: string, numericValue: number) => void
|
|
243
|
+
}
|
|
118
244
|
```
|
|
119
245
|
|
|
246
|
+
`MaskPattern`, `MaskResult`, `ApplyMaskOptions`, `BindOptions`, `DecimalMaskOptions`, and `BindDecimalOptions` are exported as types.
|
|
247
|
+
|
|
120
248
|
## License
|
|
121
249
|
|
|
122
250
|
MIT — [Danilo Celestino de Castro](https://github.com/dan2dev)
|
package/dist/mother-mask.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(e){return e===`9`||e===`Z`||e===`A`}function r(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function i(e,t,n){let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(r(t,u)){i+=a+t,a=``,d=!0,c||(o<=n?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}function a(e){let t=[],r=0;for(;r<e.length;){let i=r,a=n(e[r]);for(;r<e.length&&n(e[r])===a;)r++;let o=e.slice(i,r);t.push(a?{kind:`slots`,chars:o}:{kind:`literal`,text:o})}return t}function o(e,t){let n=0;for(let r=t;r<e.length;r++){let t=e[r];t.kind===`slots`&&(n+=t.chars.length)}return n}function s(n,r){let i=0;for(let a=r;a<n.length;a++){let r=n[a];(e(r)||t(r))&&i++}return i}function c(e,t,n){let i=a(t),c=``,l=``,u=0,d=0,f=!1,p=!1;for(let t=0;t<i.length&&!p;t++){let a=i[t];if(a.kind===`literal`){l+=a.text,e.startsWith(a.text,u)&&(u+=a.text.length);continue}let m=i[t+1],h=o(i,t+1);for(let t=0;t<a.chars.length;t++){let i=a.chars[t],o=!1;for(;u<e.length;){let t=e[u];if(r(t,i)){u++,c+=l+t,l=``,o=!0,f||(u<=n?d=c.length:f=!0);break}if(m?.kind===`literal`&&e.startsWith(m.text,u)&&s(e,u)<=h)break;u++}if(!o){u>=e.length&&(p=!0);break}}}return f||(d=c.length),{value:c,caret:d}}function l(e,t,n=0,r){return e?r?.segmented===!1?i(e,t,n):c(e,t,n):{value:``,caret:0}}function u(e){let t=0;for(let n of e)(n>=`0`&&n<=`9`||n>=`a`&&n<=`z`||n>=`A`&&n<=`Z`)&&t++;return t}function d(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function f(e,t){if(!Array.isArray(t))return t;let n=u(e),r=0;for(;r<t.length-1&&n>d(t[r]);)r++;return t[r]}function p(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var m=class{caret;_value;_mask;_options;constructor(e,t,n=0,r){this._value=e,this._mask=t,this.caret=n,this._options=r}process(){let e=l(this._value,this._mask,this.caret,this._options);return this.caret=e.caret,e.value}};function h(e,t,n=0,r){return new m(e,f(e,t),n,r)}function g(e,t,n){return h(e,t,0,n).process()}let _;function v(){return _===void 0&&(_=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),_}const y=`data-masked`;function b(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function x(e,t,n){if(e.getAttribute(y)!==null)return()=>{};let{onChange:r,segmented:i}=b(n),a=[],o=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),a.push(t))};e.setAttribute(y,Array.isArray(t)?t.join(`|`):t),o(`autocomplete`,`off`),o(`autocorrect`,`off`),o(`autocapitalize`,`off`),o(`spellcheck`,`false`),o(`maxlength`,String(p(t)));let s=!1,c=v()?`keyup`:`keydown`,l=e=>{let n=e.target;requestAnimationFrame(()=>{n.value=h(n.value,t,0,{segmented:i}).process(),r?.(n.value)})},u=e=>{let n=e,a=n.target,o=a.value;if(!n.key){s=!0,requestAnimationFrame(()=>{let e=a.selectionStart??999,n=h(a.value,t,e,{segmented:i});a.value=n.process(),a.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{s=!1})});return}if(n.key===`Meta`)return;let c=n.key===`Backspace`,l=n.key===`Delete`,u=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,d=n.key===`Unidentified`;if(u&&a.selectionStart===a.selectionEnd&&o.length>=p(t)&&!v()){n.preventDefault();return}if(s){n.preventDefault();return}requestAnimationFrame(()=>{let e=a.selectionStart??999,n=h(a.value,t,e,{segmented:i});if(a.value=n.process(),d){let t=a.value.length>o.length?n.caret:e;a.setSelectionRange(t,t)}else if(l){let t=o.length===a.value.length?e+1:e;a.setSelectionRange(t,t)}else c?a.setSelectionRange(e,e):u&&a.setSelectionRange(n.caret,n.caret);r?.(a.value)})};return e.addEventListener(`paste`,l),e.addEventListener(c,u),()=>{e.removeEventListener(`paste`,l),e.removeEventListener(c,u),e.removeAttribute(y);for(let t of a)e.removeAttribute(t)}}function S(e){return e>=`0`&&e<=`9`}function C(e){let t=e?.decimalPlaces??2;return{decimalPlaces:Number.isFinite(t)?Math.max(0,Math.floor(t)):2,segmented:e?.segmented??!0,separator:e?.separator??`,`,decimalSeparator:e?.decimalSeparator??`.`,prefix:e?.prefix??``,suffix:e?.suffix??``,allowNegative:e?.allowNegative??!1}}function w(e){let t=0;for(;t<e.length-1&&e[t]===`0`;)t++;return e.slice(t)}function T(e,t){if(!t||e.length<=3)return e;let n=[],r=e.length;for(;r>3;)n.unshift(e.slice(r-3,r)),r-=3;return n.unshift(e.slice(0,r)),n.join(t)}function E(e,t){if(t<=0)return 0;let n=0;for(let r=0;r<e.length;r++)if(S(e[r])&&(n++,n===t))return r+1;return e.length}function D(e,t){let n=``,r=``,i=!1,a=!1,o=t.decimalPlaces>0;for(let s of e){if(S(s)){a?r.length<t.decimalPlaces&&(r+=s):n+=s;continue}if(o&&!a&&s===t.decimalSeparator){a=!0;continue}s===`-`&&t.allowNegative&&(i=!0)}return{isNegative:i,intDigits:n,fracDigits:r,hasSeparator:a}}function O(e,t,n){let r=n.decimalPlaces>0,i=!1,a=0;for(let o=0;o<t;o++){let t=e[o];if(S(t)){(!i||a<n.decimalPlaces)&&a++;continue}r&&!i&&t===n.decimalSeparator&&(i=!0,a=0)}return{inFraction:i,digitsBefore:a}}function k(e,t=0,n){let r=C(n);if(!e)return{value:``,caret:0};let{isNegative:i,intDigits:a,fracDigits:o,hasSeparator:s}=D(e,r);if(a===``&&o===``&&!s)return{value:``,caret:0};let c=w(a||`0`),l=r.segmented?T(c,r.separator):c,u=r.decimalPlaces>0?o.padEnd(r.decimalPlaces,`0`):``,d=l+(r.decimalPlaces>0?r.decimalSeparator+u:``),f=i?`-`:``,p=f+r.prefix+d+r.suffix,{inFraction:m,digitsBefore:h}=O(e,Math.max(0,Math.min(t,e.length)),r),g=f.length+r.prefix.length;return{value:p,caret:m?g+l.length+r.decimalSeparator.length+h:g+E(l,h)}}function A(e,t){return k(e,e.length,t).value}function j(e,t){let{isNegative:n,intDigits:r,fracDigits:i}=D(e,C(t)),a=Number(i?`${r||`0`}.${i}`:r||`0`);return n?-a:a}function M(e,t){let n=C(t);if(n.decimalPlaces<=0)return null;let{isNegative:r,intDigits:i,hasSeparator:a}=D(e,n);if(a||i.length<n.decimalPlaces+1)return null;let o=i.length-n.decimalPlaces,s=i.slice(o),c=i.slice(0,o-1),l=r?`-`:``;return k(l+c+n.decimalSeparator+s,l.length+c.length,n)}function N(e,t,n,r){let i=C(r),a=t-1;if(a<0||e[a]!==n)return null;let{isNegative:o,intDigits:s,fracDigits:c,hasSeparator:l}=D(e.slice(0,a)+e.slice(t),i);if(s!==`0`)return null;let u=+!!o+i.prefix.length;if(a<u||a>u+1)return null;let d=o?`-`:``;return k(d+n+(l?i.decimalSeparator+c:``),d.length+1,i)}function P(e,t){let n=C(t);if(!Number.isFinite(e))return``;let r=n.allowNegative&&e<0,i=Math.abs(e).toFixed(n.decimalPlaces),a=i.indexOf(`.`),o=a===-1?i:i.slice(0,a),s=a===-1?``:i.slice(a+1),c=w(o||`0`),l=(n.segmented?T(c,n.separator):c)+(n.decimalPlaces>0?n.decimalSeparator+s:``);return(r?`-`:``)+n.prefix+l+n.suffix}const F=`data-masked`;function I(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function L(e){return e.length===1&&e>=`0`&&e<=`9`}function R(e,t){if(e.getAttribute(F)!==null)return()=>{};let{onChange:n,...r}=I(t),i=r,{decimalSeparator:a,decimalPlaces:o}=C(i),s=[],c=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),s.push(t))};e.setAttribute(F,`decimal`),c(`autocomplete`,`off`),c(`autocorrect`,`off`),c(`autocapitalize`,`off`),c(`spellcheck`,`false`);let l=!1,u=v()?`keyup`:`keydown`,d=(e,t)=>{e.value=t.value,e.setSelectionRange(t.caret,t.caret),n?.(t.value,j(t.value,i))},f=e=>{let t=e.target;requestAnimationFrame(()=>{d(t,k(t.value,t.value.length,i))})},p=e=>{let t=e,n=t.target;if(!t.key){l=!0,requestAnimationFrame(()=>{let e=n.selectionStart??n.value.length;d(n,k(n.value,e,i)),requestAnimationFrame(()=>{l=!1})});return}if(t.key===`Meta`)return;if(l){t.preventDefault();return}let r=t.key===`Backspace`,s=t.key===`Delete`,c=t.key.length===1&&!t.ctrlKey&&!t.altKey&&!t.metaKey;!r&&!s&&!c||requestAnimationFrame(()=>{let e=n.selectionStart??n.value.length;if(r){let e=M(n.value,i);if(e){d(n,e);return}}o>0&&(t.key===`.`||t.key===`,`)&&t.key!==a&&e>0&&n.value[e-1]===t.key&&(n.value=n.value.slice(0,e-1)+a+n.value.slice(e)),d(n,(L(t.key)?N(n.value,e,t.key,i):null)??k(n.value,e,i))})};return e.addEventListener(`paste`,f),e.addEventListener(u,p),()=>{e.removeEventListener(`paste`,f),e.removeEventListener(u,p),e.removeAttribute(F);for(let t of s)e.removeAttribute(t)}}exports.Mask=m,exports.applyDecimalMask=k,exports.applyMask=l,exports.bind=x,exports.bindDecimal=R,exports.buildMask=h,exports.formatDecimalValue=P,exports.getMaxLength=p,exports.process=g,exports.processDecimal=A,exports.unmaskDecimal=j;
|
|
2
2
|
//# sourceMappingURL=mother-mask.cjs.map
|
package/dist/mother-mask.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mother-mask.cjs","names":[],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts"],"sourcesContent":["import type { MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Core masking — pure function\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nexport function applyMask(value: string, mask: string, inputCaret = 0): MaskResult {\n if (!value) return { value: '', caret: 0 }\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n\n constructor(value: string, mask: string, caret = 0) {\n this._value = value\n this._mask = mask\n this.caret = caret\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(value: string, mask: MaskPattern, caret = 0): Mask {\n return new Mask(value, resolveMask(value, mask), caret)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern): string {\n return buildMask(value, mask).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask)\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mother-mask.cjs","names":["isDigitChar","MASKED_ATTR"],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts","../src/decimal-mask.ts","../src/bind-decimal.ts"],"sourcesContent":["import type { ApplyMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction isSlotChar(ch: string): boolean {\n return ch === '9' || ch === 'Z' || ch === 'A'\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Flat masking (default) — treats the mask as one continuous character\n// stream. Best for continuous identifiers (phone numbers, CPF/CNPJ, credit\n// cards) where deleting/inserting a digit anywhere is expected to reflow\n// every digit after it — this is the classic mother-mask behavior and is\n// relied on by the majority of the test suite (paste, backspace, mid-string\n// insert, etc).\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nfunction applyFlatMask(value: string, mask: string, inputCaret: number): MaskResult {\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (opt-in) — treats literal separators as hard boundaries\n// between independent fields (e.g. day/month/year in \"99/99/9999\"). Editing\n// one segment never bleeds characters into a neighboring one, so replacing\n// the \"12\" in \"25/12/2025\" with a shorter or longer value keeps the year\n// exactly where it is instead of shifting digits across the \"/\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n/**\n * Split a mask into alternating literal and slot-run tokens (e.g. \"99/99/9999\"\n * → slots\"99\", literal\"/\", slots\"99\", literal\"/\", slots\"9999\"), so the masking\n * pass can reason about segment boundaries instead of a flat character stream.\n */\nfunction tokenizeMask(mask: string): MaskToken[] {\n const tokens: MaskToken[] = []\n let i = 0\n while (i < mask.length) {\n const start = i\n const wantSlots = isSlotChar(mask[i])\n while (i < mask.length && isSlotChar(mask[i]) === wantSlots) i++\n const text = mask.slice(start, i)\n tokens.push(wantSlots ? { kind: 'slots', chars: text } : { kind: 'literal', text })\n }\n return tokens\n}\n\n/** Total slot capacity from token index `from` (inclusive) to the end of `tokens`. */\nfunction slotCapacityFrom(tokens: MaskToken[], from: number): number {\n let capacity = 0\n for (let i = from; i < tokens.length; i++) {\n const token = tokens[i]\n if (token.kind === 'slots') capacity += token.chars.length\n }\n return capacity\n}\n\n/** Count of remaining slot-matchable (digit/letter) characters in `value` from `fromIdx` onward. */\nfunction remainingDataChars(value: string, fromIdx: number): number {\n let count = 0\n for (let i = fromIdx; i < value.length; i++) {\n const ch = value[i]\n if (isDigitChar(ch) || isLetterChar(ch)) count++\n }\n return count\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but walks the mask one *segment* at\n * a time (a run of slots, or a literal) rather than one character at a time.\n * The rule that keeps an edit inside a segment from crossing into its\n * neighbor is an **early stop**: if the value hits the literal that ends the\n * current slot run before all of that run's slots are filled (e.g. only one\n * digit typed into a two-digit month slot), the run is left partially filled\n * instead of skipping past the separator to steal a digit from the next\n * segment.\n *\n * That stop is only taken when it's actually safe — i.e. every character\n * still to come in `value` fits in the slot capacity that remains *after*\n * this segment. If stopping here would strand more data than the rest of\n * the mask can hold (e.g. pasting into a later segment while an earlier one\n * still sits under-filled from before), the \"separator\" is treated as stray\n * noise instead and skipped, letting this segment take the extra slot it\n * needs so nothing at the end gets silently dropped. This is also what lets\n * an array mask grow or shrink its pattern (moving every literal after the\n * change point) reflow correctly instead of losing a digit at the boundary.\n */\nfunction applySegmentedMask(value: string, mask: string, inputCaret: number): MaskResult {\n const tokens = tokenizeMask(mask)\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n let exhausted = false\n\n for (let t = 0; t < tokens.length && !exhausted; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n pending += token.text\n if (value.startsWith(token.text, valueIdx)) valueIdx += token.text.length\n continue\n }\n\n const nextToken = tokens[t + 1]\n const capacityAfter = slotCapacityFrom(tokens, t + 1)\n\n for (let s = 0; s < token.chars.length; s++) {\n const slotCh = token.chars[s]\n let found = false\n\n while (valueIdx < value.length) {\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, slotCh)) {\n valueIdx++\n output += pending + ch\n pending = ''\n found = true\n\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n\n // This segment's ending separator showed up before the run filled.\n // Stopping here is only safe if the rest of the value still fits in\n // whatever slot capacity remains after this segment — otherwise\n // stopping would strand data, so fall through and skip this char as\n // noise instead, letting the segment take the slot it needs.\n if (\n nextToken?.kind === 'literal' &&\n value.startsWith(nextToken.text, valueIdx) &&\n remainingDataChars(value, valueIdx) <= capacityAfter\n ) {\n break\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n if (!found) {\n if (valueIdx >= value.length) exhausted = true\n break\n }\n }\n }\n\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\nexport function applyMask(\n value: string,\n mask: string,\n inputCaret = 0,\n options?: ApplyMaskOptions,\n): MaskResult {\n if (!value) return { value: '', caret: 0 }\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret)\n : applySegmentedMask(value, mask, inputCaret)\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { ApplyMaskOptions, MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n private readonly _options: ApplyMaskOptions | undefined\n\n constructor(value: string, mask: string, caret = 0, options?: ApplyMaskOptions) {\n this._value = value\n this._mask = mask\n this.caret = caret\n this._options = options\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret, this._options)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(\n value: string,\n mask: MaskPattern,\n caret = 0,\n options?: ApplyMaskOptions,\n): Mask {\n return new Mask(value, resolveMask(value, mask), caret, options)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string {\n return buildMask(value, mask, 0, options).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, segmented } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask, 0, { segmented })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n","import type { DecimalMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — mirrors apply-mask.ts)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\n// ---------------------------------------------------------------------------\n// Option resolution\n// ---------------------------------------------------------------------------\n\ninterface ResolvedDecimalOptions {\n decimalPlaces: number\n segmented: boolean\n separator: string\n decimalSeparator: string\n prefix: string\n suffix: string\n allowNegative: boolean\n}\n\n/** @internal exported for {@link bindDecimal}'s \".\" / \",\" key normalization */\nexport function resolveDecimalOptions(options?: DecimalMaskOptions): ResolvedDecimalOptions {\n const rawPlaces = options?.decimalPlaces ?? 2\n const decimalPlaces = Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : 2\n return {\n decimalPlaces,\n segmented: options?.segmented ?? true,\n separator: options?.separator ?? ',',\n decimalSeparator: options?.decimalSeparator ?? '.',\n prefix: options?.prefix ?? '',\n suffix: options?.suffix ?? '',\n allowNegative: options?.allowNegative ?? false,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Digit-stream helpers\n// ---------------------------------------------------------------------------\n\n/** Strip leading zeros from a digit string, always keeping at least one digit. */\nfunction stripLeadingZeros(s: string): string {\n let i = 0\n while (i < s.length - 1 && s[i] === '0') i++\n return s.slice(i)\n}\n\n/** Insert `sep` every 3 digits from the right (e.g. \"1234567\" → \"1,234,567\"). */\nfunction groupThousands(s: string, sep: string): string {\n if (!sep || s.length <= 3) return s\n const parts: string[] = []\n let i = s.length\n while (i > 3) {\n parts.unshift(s.slice(i - 3, i))\n i -= 3\n }\n parts.unshift(s.slice(0, i))\n return parts.join(sep)\n}\n\n/**\n * Find the position in `s` that leaves exactly `digitsBefore` digit\n * characters preceding it — the position immediately after that digit and\n * before any subsequent literal (grouping separator, ...), so the caret\n * stays glued to the last digit the user placed there.\n */\nfunction caretForDigitsBefore(s: string, digitsBefore: number): number {\n if (digitsBefore <= 0) return 0\n let count = 0\n for (let i = 0; i < s.length; i++) {\n if (isDigitChar(s[i])) {\n count++\n if (count === digitsBefore) return i + 1\n }\n }\n return s.length\n}\n\n// ---------------------------------------------------------------------------\n// Segmented parsing — integer digits before the decimal separator, fraction\n// digits after. Unlike a slot-pattern mask, the integer segment has no fixed\n// length; only the fraction is fixed-width (`decimalPlaces`), zero-padded on\n// the right so a shorter fraction reads as its low-order (trailing) digits\n// being zero rather than reflowing/shifting — e.g. editing \"423,42\" down to\n// \"423,4\" produces \"423,40\", not \"42,34\".\n//\n// The decimal separator only has meaning as the *first* occurrence of\n// `opts.decimalSeparator`; every other non-digit character (thousands\n// separator, prefix/suffix text, a second stray separator, ...) is noise and\n// is dropped. This keeps re-parsing an already-masked value idempotent.\n// ---------------------------------------------------------------------------\n\ninterface DecimalParts {\n isNegative: boolean\n intDigits: string\n fracDigits: string\n hasSeparator: boolean\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n let intDigits = ''\n let fracDigits = ''\n let isNegative = false\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces > 0\n\n for (const ch of raw) {\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else {\n intDigits += ch\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n continue\n }\n if (ch === '-' && opts.allowNegative) isNegative = true\n // Anything else — thousands separator, prefix/suffix text, a repeated\n // separator, stray letters — is noise and is dropped.\n }\n\n return { isNegative, intDigits, fracDigits, hasSeparator: inFraction }\n}\n\n/**\n * Walk `raw[0:caret]` to find which segment the caret sits in (integer or\n * fraction) and how many digits of that segment precede it, so the same\n * position can be re-derived in the freshly formatted output.\n */\nfunction locateCaretSegment(\n raw: string,\n caret: number,\n opts: ResolvedDecimalOptions,\n): { inFraction: boolean; digitsBefore: number } {\n const canHaveFraction = opts.decimalPlaces > 0\n let inFraction = false\n let digitsBefore = 0\n\n for (let i = 0; i < caret; i++) {\n const ch = raw[i]\n if (isDigitChar(ch)) {\n if (!inFraction || digitsBefore < opts.decimalPlaces) digitsBefore++\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n digitsBefore = 0\n }\n }\n\n return { inFraction, digitsBefore }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry points\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a decimal/currency mask to a value, producing the masked output and\n * a computed caret position. Digits typed before the decimal separator\n * extend the integer part; the fraction only starts once the separator is\n * typed, and is always displayed zero-padded to `decimalPlaces` width.\n */\nexport function applyDecimalMask(\n value: string,\n inputCaret = 0,\n options?: DecimalMaskOptions,\n): MaskResult {\n const opts = resolveDecimalOptions(options)\n if (!value) return { value: '', caret: 0 }\n\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (intDigits === '' && fracDigits === '' && !hasSeparator) return { value: '', caret: 0 }\n\n const intPart = stripLeadingZeros(intDigits || '0')\n const groupedInt = opts.segmented ? groupThousands(intPart, opts.separator) : intPart\n const fracPadded = opts.decimalPlaces > 0 ? fracDigits.padEnd(opts.decimalPlaces, '0') : ''\n const numberStr = groupedInt + (opts.decimalPlaces > 0 ? opts.decimalSeparator + fracPadded : '')\n const signStr = isNegative ? '-' : ''\n const output = signStr + opts.prefix + numberStr + opts.suffix\n\n const clampedCaret = Math.max(0, Math.min(inputCaret, value.length))\n const { inFraction, digitsBefore } = locateCaretSegment(value, clampedCaret, opts)\n const prefixLen = signStr.length + opts.prefix.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore)\n\n return { value: output, caret }\n}\n\n/** Apply a decimal mask to a raw value and return just the masked string. */\nexport function processDecimal(value: string, options?: DecimalMaskOptions): string {\n return applyDecimalMask(value, value.length, options).value\n}\n\n/**\n * Parse a raw or already-masked decimal value back into a JS number.\n * Ignores prefix/suffix/thousands separator; returns `0` for an empty or\n * digit-less value.\n */\nexport function unmaskDecimal(value: string, options?: DecimalMaskOptions): number {\n const opts = resolveDecimalOptions(options)\n const { isNegative, intDigits, fracDigits } = computeDecimalParts(value, opts)\n const n = Number(fracDigits ? `${intDigits || '0'}.${fracDigits}` : intDigits || '0')\n return isNegative ? -n : n\n}\n\n/**\n * After a Backspace removes the decimal separator itself, the integer and\n * fraction digit runs collapse into one continuous stream (e.g. \"25.00\"\n * with the caret right after \".\" → Backspace deletes the \".\" → \"2500\").\n * Left alone, that reads as one big integer (\"$2,500.00\"). This restores\n * the segment boundary instead: the trailing `decimalPlaces` digits are\n * still the fraction, and the digit right before them — the one that used\n * to sit at the end of the integer part — is the one Backspace actually\n * removed, so it's dropped (not kept) — \"$25.00\" → \"$2.00\".\n *\n * Every reformat re-appends `decimalSeparator` whenever `decimalPlaces > 0`,\n * so its absence from `value` is an unambiguous signal that this exact\n * keystroke just deleted it — no \"value before this keystroke\" snapshot is\n * needed. Returns `null` when there's nothing to restore (the separator is\n * still present, `decimalPlaces` is `0`, or too few digits remain), so the\n * caller falls through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskUnmergingSeparator(\n value: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n if (opts.decimalPlaces <= 0) return null\n\n const { isNegative, intDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (hasSeparator || intDigits.length < opts.decimalPlaces + 1) return null\n\n const preMergeIntLength = intDigits.length - opts.decimalPlaces\n const fracDigits = intDigits.slice(preMergeIntLength)\n const remainingInt = intDigits.slice(0, preMergeIntLength - 1)\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + remainingInt + opts.decimalSeparator + fracDigits\n return applyDecimalMask(raw, signPart.length + remainingInt.length, opts)\n}\n\n/**\n * Special-cases typing a single digit into a field whose integer part is\n * exactly the auto-inserted \"0\" placeholder: the new digit replaces that\n * zero instead of combining with it — e.g. \"$0.00\" with the caret anywhere\n * against that lone \"0\" and typing \"2\" gives \"$2.00\", not \"$20.00\"/\"$02.00\".\n * Any already-typed fraction is preserved.\n *\n * `value`/`caret` must be the state *after* the browser has already\n * inserted `digit` at `caret - 1` (the same post-insertion snapshot\n * `applyDecimalMask` itself expects from `bindDecimal`). Returns `null`\n * when the pattern doesn't apply — either the integer part has real digits\n * already, or the caret wasn't against that lone zero — so the caller falls\n * through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskReplacingLoneZero(\n value: string,\n caret: number,\n digit: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n const insertIdx = caret - 1\n if (insertIdx < 0 || value[insertIdx] !== digit) return null\n\n const withoutDigit = value.slice(0, insertIdx) + value.slice(caret)\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(withoutDigit, opts)\n if (intDigits !== '0') return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + 1) return null\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + digit + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + 1, opts)\n}\n\n/** Format a plain JS number into its masked display string. */\nexport function formatDecimalValue(value: number, options?: DecimalMaskOptions): string {\n const opts = resolveDecimalOptions(options)\n if (!Number.isFinite(value)) return ''\n\n const isNegative = opts.allowNegative && value < 0\n const fixed = Math.abs(value).toFixed(opts.decimalPlaces)\n const dotIdx = fixed.indexOf('.')\n const intRaw = dotIdx === -1 ? fixed : fixed.slice(0, dotIdx)\n const fracPart = dotIdx === -1 ? '' : fixed.slice(dotIdx + 1)\n const intPart = stripLeadingZeros(intRaw || '0')\n\n const groupedInt = opts.segmented ? groupThousands(intPart, opts.separator) : intPart\n const numberStr = groupedInt + (opts.decimalPlaces > 0 ? opts.decimalSeparator + fracPart : '')\n\n return (isNegative ? '-' : '') + opts.prefix + numberStr + opts.suffix\n}\n","import {\n applyDecimalMask,\n applyDecimalMaskReplacingLoneZero,\n applyDecimalMaskUnmergingSeparator,\n resolveDecimalOptions,\n unmaskDecimal,\n} from './decimal-mask'\nimport { isIos } from './platform'\nimport type { BindDecimalOptions, DecimalMaskOptions, MaskResult } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindDecimalOptions(\n second:\n | BindDecimalOptions\n | ((value: string, numericValue: number) => void)\n | null\n | undefined,\n): BindDecimalOptions {\n if (second == null) return {}\n if (typeof second === 'function') return { onChange: second }\n return second\n}\n\nfunction isDigitKey(key: string): boolean {\n return key.length === 1 && key >= '0' && key <= '9'\n}\n\n/**\n * Bind a decimal/currency mask to an input element.\n *\n * Same contract as {@link bind}: idempotent (marked with `data-masked`),\n * returns a dispose function, and reformats on paste and keyboard-driven\n * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no\n * fixed pattern — the integer part grows and shrinks freely; formatting is\n * driven entirely by `options`.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.\n */\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n options?: BindDecimalOptions | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n onChange: ((value: string, numericValue: number) => void) | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n second?: BindDecimalOptions | ((value: string, numericValue: number) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, ...maskOptions } = toBindDecimalOptions(second)\n const decimalOptions: DecimalMaskOptions = maskOptions\n const { decimalSeparator, decimalPlaces } = resolveDecimalOptions(decimalOptions)\n\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, 'decimal')\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n\n let lockInput = false\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n target.setSelectionRange(m.caret, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n applyResult(target, applyDecimalMask(target.value, target.value.length, decimalOptions))\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n\n // Navigation (arrows, Home/End, Tab, ...), selection, and shortcut keys\n // (Ctrl/Cmd+A, Ctrl/Cmd+C, ...) don't change the text — leave the\n // browser's native caret/selection handling alone instead of recomputing\n // and overwriting it on every keystroke.\n if (!isBackspace && !isDelete && !isCharInsert) return\n\n // Everything below reads `target.value`/`selectionStart` inside the rAF\n // callback rather than synchronously here, since the browser's native\n // character insertion for this keystroke isn't guaranteed to have landed\n // yet at the point a keydown listener runs — only by the next frame.\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n\n // Backspace that just deleted the decimal separator merges the\n // integer and fraction digit runs into one continuous stream —\n // restore the boundary instead of treating that as one big integer.\n if (isBackspace) {\n const unmerged = applyDecimalMaskUnmergingSeparator(target.value, decimalOptions)\n if (unmerged) {\n applyResult(target, unmerged)\n return\n }\n }\n\n // A numeric keypad (or a locale mismatch) may only offer \".\" or \",\".\n // Normalize whichever one the user just typed to the configured\n // `decimalSeparator` so it reliably opens the fraction segment.\n if (\n decimalPlaces > 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator &&\n pos > 0 &&\n target.value[pos - 1] === ke.key\n ) {\n target.value = target.value.slice(0, pos - 1) + decimalSeparator + target.value.slice(pos)\n }\n\n // Typing a digit into a field whose integer part is still the\n // auto-inserted \"0\" placeholder replaces that zero instead of\n // combining with it (e.g. \"$0.00\" + \"2\" → \"$2.00\", not \"$20.00\").\n const replaced = isDigitKey(ke.key)\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, ke.key, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n"],"mappings":"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"}
|
package/dist/mother-mask.d.cts
CHANGED
|
@@ -15,25 +15,54 @@ interface MaskResult {
|
|
|
15
15
|
readonly value: string;
|
|
16
16
|
readonly caret: number;
|
|
17
17
|
}
|
|
18
|
+
/** Options for {@link applyMask} and {@link buildMask}. */
|
|
19
|
+
interface ApplyMaskOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Treat literal separators as hard boundaries between independent fields
|
|
22
|
+
* instead of one continuous digit/character stream. **On by default**: a
|
|
23
|
+
* mask made of independent fields — dates, times, phone area codes — never
|
|
24
|
+
* bleeds digits from one field into a neighboring one when you edit a
|
|
25
|
+
* single field (e.g. the month in "99/99/9999" won't steal a digit from
|
|
26
|
+
* the year).
|
|
27
|
+
*
|
|
28
|
+
* Pass `segmented: false` to opt into the classic flat/reflow behavior
|
|
29
|
+
* instead, where deleting or replacing characters anywhere shifts
|
|
30
|
+
* everything after it to close the gap — useful when a mask really is one
|
|
31
|
+
* continuous number with cosmetic separators (e.g. formatting a running
|
|
32
|
+
* total) rather than independent fields.
|
|
33
|
+
*/
|
|
34
|
+
segmented?: boolean;
|
|
35
|
+
}
|
|
18
36
|
/** Options for {@link bind}. */
|
|
19
|
-
interface BindOptions {
|
|
37
|
+
interface BindOptions extends ApplyMaskOptions {
|
|
20
38
|
/** Fires with the masked value after paste or keyboard-driven changes. */
|
|
21
39
|
onChange?: (value: string) => void;
|
|
22
40
|
}
|
|
41
|
+
/** Options for {@link applyDecimalMask}, {@link processDecimal}, {@link unmaskDecimal}, {@link formatDecimalValue}, and {@link bindDecimal}. */
|
|
42
|
+
interface DecimalMaskOptions {
|
|
43
|
+
/** Number of fixed fractional digits. Negative/fractional values are floored to `0`. @default 2 */
|
|
44
|
+
decimalPlaces?: number;
|
|
45
|
+
/** Group the integer part into thousands using `separator`. @default true */
|
|
46
|
+
segmented?: boolean;
|
|
47
|
+
/** Thousands grouping separator, used when `segmented` is `true`. @default ',' */
|
|
48
|
+
separator?: string;
|
|
49
|
+
/** Separator between the integer and fractional parts. @default '.' */
|
|
50
|
+
decimalSeparator?: string;
|
|
51
|
+
/** Fixed text prepended to the formatted number (after the sign, if negative). @default '' */
|
|
52
|
+
prefix?: string;
|
|
53
|
+
/** Fixed text appended to the formatted number. @default '' */
|
|
54
|
+
suffix?: string;
|
|
55
|
+
/** Allow a leading `-` to produce a negative value. @default false */
|
|
56
|
+
allowNegative?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** Options for {@link bindDecimal}. */
|
|
59
|
+
interface BindDecimalOptions extends DecimalMaskOptions {
|
|
60
|
+
/** Fires with the masked string and its parsed numeric value after paste or keyboard-driven changes. */
|
|
61
|
+
onChange?: (value: string, numericValue: number) => void;
|
|
62
|
+
}
|
|
23
63
|
//#endregion
|
|
24
64
|
//#region src/apply-mask.d.ts
|
|
25
|
-
|
|
26
|
-
* Apply a single mask string to a value, producing the masked output and
|
|
27
|
-
* a computed caret position.
|
|
28
|
-
*
|
|
29
|
-
* **Caret algorithm**: as the mask consumes characters from `value`, every
|
|
30
|
-
* time a *matching* input character at a position *before* `inputCaret` is
|
|
31
|
-
* written to the output (including any preceding pending literals that were
|
|
32
|
-
* just flushed), the output caret is updated to the current output length.
|
|
33
|
-
* This correctly handles literal insertion, middle-of-string edits, and
|
|
34
|
-
* characters that are skipped because they don't match the current slot.
|
|
35
|
-
*/
|
|
36
|
-
declare function applyMask(value: string, mask: string, inputCaret?: number): MaskResult;
|
|
65
|
+
declare function applyMask(value: string, mask: string, inputCaret?: number, options?: ApplyMaskOptions): MaskResult;
|
|
37
66
|
//#endregion
|
|
38
67
|
//#region src/bind.d.ts
|
|
39
68
|
/**
|
|
@@ -52,6 +81,41 @@ declare function applyMask(value: string, mask: string, inputCaret?: number): Ma
|
|
|
52
81
|
declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, options?: BindOptions | null): () => void;
|
|
53
82
|
declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, onChange: ((value: string) => void) | null): () => void;
|
|
54
83
|
//#endregion
|
|
84
|
+
//#region src/bind-decimal.d.ts
|
|
85
|
+
/**
|
|
86
|
+
* Bind a decimal/currency mask to an input element.
|
|
87
|
+
*
|
|
88
|
+
* Same contract as {@link bind}: idempotent (marked with `data-masked`),
|
|
89
|
+
* returns a dispose function, and reformats on paste and keyboard-driven
|
|
90
|
+
* changes via `requestAnimationFrame`. Unlike the pattern masks, there is no
|
|
91
|
+
* fixed pattern — the integer part grows and shrinks freely; formatting is
|
|
92
|
+
* driven entirely by `options`.
|
|
93
|
+
*
|
|
94
|
+
* @param input - Any `HTMLInputElement` or `Element` that behaves like one.
|
|
95
|
+
* @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.
|
|
96
|
+
*/
|
|
97
|
+
declare function bindDecimal(input: HTMLInputElement | Element, options?: BindDecimalOptions | null): () => void;
|
|
98
|
+
declare function bindDecimal(input: HTMLInputElement | Element, onChange: ((value: string, numericValue: number) => void) | null): () => void;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/decimal-mask.d.ts
|
|
101
|
+
/**
|
|
102
|
+
* Apply a decimal/currency mask to a value, producing the masked output and
|
|
103
|
+
* a computed caret position. Digits typed before the decimal separator
|
|
104
|
+
* extend the integer part; the fraction only starts once the separator is
|
|
105
|
+
* typed, and is always displayed zero-padded to `decimalPlaces` width.
|
|
106
|
+
*/
|
|
107
|
+
declare function applyDecimalMask(value: string, inputCaret?: number, options?: DecimalMaskOptions): MaskResult;
|
|
108
|
+
/** Apply a decimal mask to a raw value and return just the masked string. */
|
|
109
|
+
declare function processDecimal(value: string, options?: DecimalMaskOptions): string;
|
|
110
|
+
/**
|
|
111
|
+
* Parse a raw or already-masked decimal value back into a JS number.
|
|
112
|
+
* Ignores prefix/suffix/thousands separator; returns `0` for an empty or
|
|
113
|
+
* digit-less value.
|
|
114
|
+
*/
|
|
115
|
+
declare function unmaskDecimal(value: string, options?: DecimalMaskOptions): number;
|
|
116
|
+
/** Format a plain JS number into its masked display string. */
|
|
117
|
+
declare function formatDecimalValue(value: number, options?: DecimalMaskOptions): string;
|
|
118
|
+
//#endregion
|
|
55
119
|
//#region src/pattern.d.ts
|
|
56
120
|
/** Maximum allowed input length for the given mask. */
|
|
57
121
|
declare function getMaxLength(mask: MaskPattern): number;
|
|
@@ -63,14 +127,15 @@ declare class Mask {
|
|
|
63
127
|
caret: number;
|
|
64
128
|
private readonly _value;
|
|
65
129
|
private readonly _mask;
|
|
66
|
-
|
|
130
|
+
private readonly _options;
|
|
131
|
+
constructor(value: string, mask: string, caret?: number, options?: ApplyMaskOptions);
|
|
67
132
|
/** Apply the mask to the value and return the masked string. */
|
|
68
133
|
process(): string;
|
|
69
134
|
}
|
|
70
135
|
/** Build a `Mask` instance, resolving array patterns by value length. */
|
|
71
|
-
declare function buildMask(value: string, mask: MaskPattern, caret?: number): Mask;
|
|
136
|
+
declare function buildMask(value: string, mask: MaskPattern, caret?: number, options?: ApplyMaskOptions): Mask;
|
|
72
137
|
/** Apply a mask pattern to a raw value string and return the masked result. */
|
|
73
|
-
declare function process(value: string, mask: MaskPattern): string;
|
|
138
|
+
declare function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string;
|
|
74
139
|
//#endregion
|
|
75
|
-
export { type BindOptions, Mask, type MaskPattern, type MaskResult, applyMask, bind, buildMask, getMaxLength, process };
|
|
140
|
+
export { type ApplyMaskOptions, type BindDecimalOptions, type BindOptions, type DecimalMaskOptions, Mask, type MaskPattern, type MaskResult, applyDecimalMask, applyMask, bind, bindDecimal, buildMask, formatDecimalValue, getMaxLength, process, processDecimal, unmaskDecimal };
|
|
76
141
|
//# sourceMappingURL=mother-mask.d.cts.map
|
package/dist/mother-mask.d.mts
CHANGED
|
@@ -15,25 +15,54 @@ interface MaskResult {
|
|
|
15
15
|
readonly value: string;
|
|
16
16
|
readonly caret: number;
|
|
17
17
|
}
|
|
18
|
+
/** Options for {@link applyMask} and {@link buildMask}. */
|
|
19
|
+
interface ApplyMaskOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Treat literal separators as hard boundaries between independent fields
|
|
22
|
+
* instead of one continuous digit/character stream. **On by default**: a
|
|
23
|
+
* mask made of independent fields — dates, times, phone area codes — never
|
|
24
|
+
* bleeds digits from one field into a neighboring one when you edit a
|
|
25
|
+
* single field (e.g. the month in "99/99/9999" won't steal a digit from
|
|
26
|
+
* the year).
|
|
27
|
+
*
|
|
28
|
+
* Pass `segmented: false` to opt into the classic flat/reflow behavior
|
|
29
|
+
* instead, where deleting or replacing characters anywhere shifts
|
|
30
|
+
* everything after it to close the gap — useful when a mask really is one
|
|
31
|
+
* continuous number with cosmetic separators (e.g. formatting a running
|
|
32
|
+
* total) rather than independent fields.
|
|
33
|
+
*/
|
|
34
|
+
segmented?: boolean;
|
|
35
|
+
}
|
|
18
36
|
/** Options for {@link bind}. */
|
|
19
|
-
interface BindOptions {
|
|
37
|
+
interface BindOptions extends ApplyMaskOptions {
|
|
20
38
|
/** Fires with the masked value after paste or keyboard-driven changes. */
|
|
21
39
|
onChange?: (value: string) => void;
|
|
22
40
|
}
|
|
41
|
+
/** Options for {@link applyDecimalMask}, {@link processDecimal}, {@link unmaskDecimal}, {@link formatDecimalValue}, and {@link bindDecimal}. */
|
|
42
|
+
interface DecimalMaskOptions {
|
|
43
|
+
/** Number of fixed fractional digits. Negative/fractional values are floored to `0`. @default 2 */
|
|
44
|
+
decimalPlaces?: number;
|
|
45
|
+
/** Group the integer part into thousands using `separator`. @default true */
|
|
46
|
+
segmented?: boolean;
|
|
47
|
+
/** Thousands grouping separator, used when `segmented` is `true`. @default ',' */
|
|
48
|
+
separator?: string;
|
|
49
|
+
/** Separator between the integer and fractional parts. @default '.' */
|
|
50
|
+
decimalSeparator?: string;
|
|
51
|
+
/** Fixed text prepended to the formatted number (after the sign, if negative). @default '' */
|
|
52
|
+
prefix?: string;
|
|
53
|
+
/** Fixed text appended to the formatted number. @default '' */
|
|
54
|
+
suffix?: string;
|
|
55
|
+
/** Allow a leading `-` to produce a negative value. @default false */
|
|
56
|
+
allowNegative?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** Options for {@link bindDecimal}. */
|
|
59
|
+
interface BindDecimalOptions extends DecimalMaskOptions {
|
|
60
|
+
/** Fires with the masked string and its parsed numeric value after paste or keyboard-driven changes. */
|
|
61
|
+
onChange?: (value: string, numericValue: number) => void;
|
|
62
|
+
}
|
|
23
63
|
//#endregion
|
|
24
64
|
//#region src/apply-mask.d.ts
|
|
25
|
-
|
|
26
|
-
* Apply a single mask string to a value, producing the masked output and
|
|
27
|
-
* a computed caret position.
|
|
28
|
-
*
|
|
29
|
-
* **Caret algorithm**: as the mask consumes characters from `value`, every
|
|
30
|
-
* time a *matching* input character at a position *before* `inputCaret` is
|
|
31
|
-
* written to the output (including any preceding pending literals that were
|
|
32
|
-
* just flushed), the output caret is updated to the current output length.
|
|
33
|
-
* This correctly handles literal insertion, middle-of-string edits, and
|
|
34
|
-
* characters that are skipped because they don't match the current slot.
|
|
35
|
-
*/
|
|
36
|
-
declare function applyMask(value: string, mask: string, inputCaret?: number): MaskResult;
|
|
65
|
+
declare function applyMask(value: string, mask: string, inputCaret?: number, options?: ApplyMaskOptions): MaskResult;
|
|
37
66
|
//#endregion
|
|
38
67
|
//#region src/bind.d.ts
|
|
39
68
|
/**
|
|
@@ -52,6 +81,41 @@ declare function applyMask(value: string, mask: string, inputCaret?: number): Ma
|
|
|
52
81
|
declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, options?: BindOptions | null): () => void;
|
|
53
82
|
declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, onChange: ((value: string) => void) | null): () => void;
|
|
54
83
|
//#endregion
|
|
84
|
+
//#region src/bind-decimal.d.ts
|
|
85
|
+
/**
|
|
86
|
+
* Bind a decimal/currency mask to an input element.
|
|
87
|
+
*
|
|
88
|
+
* Same contract as {@link bind}: idempotent (marked with `data-masked`),
|
|
89
|
+
* returns a dispose function, and reformats on paste and keyboard-driven
|
|
90
|
+
* changes via `requestAnimationFrame`. Unlike the pattern masks, there is no
|
|
91
|
+
* fixed pattern — the integer part grows and shrinks freely; formatting is
|
|
92
|
+
* driven entirely by `options`.
|
|
93
|
+
*
|
|
94
|
+
* @param input - Any `HTMLInputElement` or `Element` that behaves like one.
|
|
95
|
+
* @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.
|
|
96
|
+
*/
|
|
97
|
+
declare function bindDecimal(input: HTMLInputElement | Element, options?: BindDecimalOptions | null): () => void;
|
|
98
|
+
declare function bindDecimal(input: HTMLInputElement | Element, onChange: ((value: string, numericValue: number) => void) | null): () => void;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/decimal-mask.d.ts
|
|
101
|
+
/**
|
|
102
|
+
* Apply a decimal/currency mask to a value, producing the masked output and
|
|
103
|
+
* a computed caret position. Digits typed before the decimal separator
|
|
104
|
+
* extend the integer part; the fraction only starts once the separator is
|
|
105
|
+
* typed, and is always displayed zero-padded to `decimalPlaces` width.
|
|
106
|
+
*/
|
|
107
|
+
declare function applyDecimalMask(value: string, inputCaret?: number, options?: DecimalMaskOptions): MaskResult;
|
|
108
|
+
/** Apply a decimal mask to a raw value and return just the masked string. */
|
|
109
|
+
declare function processDecimal(value: string, options?: DecimalMaskOptions): string;
|
|
110
|
+
/**
|
|
111
|
+
* Parse a raw or already-masked decimal value back into a JS number.
|
|
112
|
+
* Ignores prefix/suffix/thousands separator; returns `0` for an empty or
|
|
113
|
+
* digit-less value.
|
|
114
|
+
*/
|
|
115
|
+
declare function unmaskDecimal(value: string, options?: DecimalMaskOptions): number;
|
|
116
|
+
/** Format a plain JS number into its masked display string. */
|
|
117
|
+
declare function formatDecimalValue(value: number, options?: DecimalMaskOptions): string;
|
|
118
|
+
//#endregion
|
|
55
119
|
//#region src/pattern.d.ts
|
|
56
120
|
/** Maximum allowed input length for the given mask. */
|
|
57
121
|
declare function getMaxLength(mask: MaskPattern): number;
|
|
@@ -63,14 +127,15 @@ declare class Mask {
|
|
|
63
127
|
caret: number;
|
|
64
128
|
private readonly _value;
|
|
65
129
|
private readonly _mask;
|
|
66
|
-
|
|
130
|
+
private readonly _options;
|
|
131
|
+
constructor(value: string, mask: string, caret?: number, options?: ApplyMaskOptions);
|
|
67
132
|
/** Apply the mask to the value and return the masked string. */
|
|
68
133
|
process(): string;
|
|
69
134
|
}
|
|
70
135
|
/** Build a `Mask` instance, resolving array patterns by value length. */
|
|
71
|
-
declare function buildMask(value: string, mask: MaskPattern, caret?: number): Mask;
|
|
136
|
+
declare function buildMask(value: string, mask: MaskPattern, caret?: number, options?: ApplyMaskOptions): Mask;
|
|
72
137
|
/** Apply a mask pattern to a raw value string and return the masked result. */
|
|
73
|
-
declare function process(value: string, mask: MaskPattern): string;
|
|
138
|
+
declare function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string;
|
|
74
139
|
//#endregion
|
|
75
|
-
export { type BindOptions, Mask, type MaskPattern, type MaskResult, applyMask, bind, buildMask, getMaxLength, process };
|
|
140
|
+
export { type ApplyMaskOptions, type BindDecimalOptions, type BindOptions, type DecimalMaskOptions, Mask, type MaskPattern, type MaskResult, applyDecimalMask, applyMask, bind, bindDecimal, buildMask, formatDecimalValue, getMaxLength, process, processDecimal, unmaskDecimal };
|
|
76
141
|
//# sourceMappingURL=mother-mask.d.mts.map
|
package/dist/mother-mask.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function
|
|
1
|
+
function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(e){return e===`9`||e===`Z`||e===`A`}function r(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function i(e,t,n){let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(r(t,u)){i+=a+t,a=``,d=!0,c||(o<=n?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}function a(e){let t=[],r=0;for(;r<e.length;){let i=r,a=n(e[r]);for(;r<e.length&&n(e[r])===a;)r++;let o=e.slice(i,r);t.push(a?{kind:`slots`,chars:o}:{kind:`literal`,text:o})}return t}function o(e,t){let n=0;for(let r=t;r<e.length;r++){let t=e[r];t.kind===`slots`&&(n+=t.chars.length)}return n}function s(n,r){let i=0;for(let a=r;a<n.length;a++){let r=n[a];(e(r)||t(r))&&i++}return i}function c(e,t,n){let i=a(t),c=``,l=``,u=0,d=0,f=!1,p=!1;for(let t=0;t<i.length&&!p;t++){let a=i[t];if(a.kind===`literal`){l+=a.text,e.startsWith(a.text,u)&&(u+=a.text.length);continue}let m=i[t+1],h=o(i,t+1);for(let t=0;t<a.chars.length;t++){let i=a.chars[t],o=!1;for(;u<e.length;){let t=e[u];if(r(t,i)){u++,c+=l+t,l=``,o=!0,f||(u<=n?d=c.length:f=!0);break}if(m?.kind===`literal`&&e.startsWith(m.text,u)&&s(e,u)<=h)break;u++}if(!o){u>=e.length&&(p=!0);break}}}return f||(d=c.length),{value:c,caret:d}}function l(e,t,n=0,r){return e?r?.segmented===!1?i(e,t,n):c(e,t,n):{value:``,caret:0}}function u(e){let t=0;for(let n of e)(n>=`0`&&n<=`9`||n>=`a`&&n<=`z`||n>=`A`&&n<=`Z`)&&t++;return t}function d(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function f(e,t){if(!Array.isArray(t))return t;let n=u(e),r=0;for(;r<t.length-1&&n>d(t[r]);)r++;return t[r]}function p(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var m=class{caret;_value;_mask;_options;constructor(e,t,n=0,r){this._value=e,this._mask=t,this.caret=n,this._options=r}process(){let e=l(this._value,this._mask,this.caret,this._options);return this.caret=e.caret,e.value}};function h(e,t,n=0,r){return new m(e,f(e,t),n,r)}function g(e,t,n){return h(e,t,0,n).process()}let _;function v(){return _===void 0&&(_=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),_}const y=`data-masked`;function b(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function x(e,t,n){if(e.getAttribute(y)!==null)return()=>{};let{onChange:r,segmented:i}=b(n),a=[],o=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),a.push(t))};e.setAttribute(y,Array.isArray(t)?t.join(`|`):t),o(`autocomplete`,`off`),o(`autocorrect`,`off`),o(`autocapitalize`,`off`),o(`spellcheck`,`false`),o(`maxlength`,String(p(t)));let s=!1,c=v()?`keyup`:`keydown`,l=e=>{let n=e.target;requestAnimationFrame(()=>{n.value=h(n.value,t,0,{segmented:i}).process(),r?.(n.value)})},u=e=>{let n=e,a=n.target,o=a.value;if(!n.key){s=!0,requestAnimationFrame(()=>{let e=a.selectionStart??999,n=h(a.value,t,e,{segmented:i});a.value=n.process(),a.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{s=!1})});return}if(n.key===`Meta`)return;let c=n.key===`Backspace`,l=n.key===`Delete`,u=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,d=n.key===`Unidentified`;if(u&&a.selectionStart===a.selectionEnd&&o.length>=p(t)&&!v()){n.preventDefault();return}if(s){n.preventDefault();return}requestAnimationFrame(()=>{let e=a.selectionStart??999,n=h(a.value,t,e,{segmented:i});if(a.value=n.process(),d){let t=a.value.length>o.length?n.caret:e;a.setSelectionRange(t,t)}else if(l){let t=o.length===a.value.length?e+1:e;a.setSelectionRange(t,t)}else c?a.setSelectionRange(e,e):u&&a.setSelectionRange(n.caret,n.caret);r?.(a.value)})};return e.addEventListener(`paste`,l),e.addEventListener(c,u),()=>{e.removeEventListener(`paste`,l),e.removeEventListener(c,u),e.removeAttribute(y);for(let t of a)e.removeAttribute(t)}}function S(e){return e>=`0`&&e<=`9`}function C(e){let t=e?.decimalPlaces??2;return{decimalPlaces:Number.isFinite(t)?Math.max(0,Math.floor(t)):2,segmented:e?.segmented??!0,separator:e?.separator??`,`,decimalSeparator:e?.decimalSeparator??`.`,prefix:e?.prefix??``,suffix:e?.suffix??``,allowNegative:e?.allowNegative??!1}}function w(e){let t=0;for(;t<e.length-1&&e[t]===`0`;)t++;return e.slice(t)}function T(e,t){if(!t||e.length<=3)return e;let n=[],r=e.length;for(;r>3;)n.unshift(e.slice(r-3,r)),r-=3;return n.unshift(e.slice(0,r)),n.join(t)}function E(e,t){if(t<=0)return 0;let n=0;for(let r=0;r<e.length;r++)if(S(e[r])&&(n++,n===t))return r+1;return e.length}function D(e,t){let n=``,r=``,i=!1,a=!1,o=t.decimalPlaces>0;for(let s of e){if(S(s)){a?r.length<t.decimalPlaces&&(r+=s):n+=s;continue}if(o&&!a&&s===t.decimalSeparator){a=!0;continue}s===`-`&&t.allowNegative&&(i=!0)}return{isNegative:i,intDigits:n,fracDigits:r,hasSeparator:a}}function O(e,t,n){let r=n.decimalPlaces>0,i=!1,a=0;for(let o=0;o<t;o++){let t=e[o];if(S(t)){(!i||a<n.decimalPlaces)&&a++;continue}r&&!i&&t===n.decimalSeparator&&(i=!0,a=0)}return{inFraction:i,digitsBefore:a}}function k(e,t=0,n){let r=C(n);if(!e)return{value:``,caret:0};let{isNegative:i,intDigits:a,fracDigits:o,hasSeparator:s}=D(e,r);if(a===``&&o===``&&!s)return{value:``,caret:0};let c=w(a||`0`),l=r.segmented?T(c,r.separator):c,u=r.decimalPlaces>0?o.padEnd(r.decimalPlaces,`0`):``,d=l+(r.decimalPlaces>0?r.decimalSeparator+u:``),f=i?`-`:``,p=f+r.prefix+d+r.suffix,{inFraction:m,digitsBefore:h}=O(e,Math.max(0,Math.min(t,e.length)),r),g=f.length+r.prefix.length;return{value:p,caret:m?g+l.length+r.decimalSeparator.length+h:g+E(l,h)}}function A(e,t){return k(e,e.length,t).value}function j(e,t){let{isNegative:n,intDigits:r,fracDigits:i}=D(e,C(t)),a=Number(i?`${r||`0`}.${i}`:r||`0`);return n?-a:a}function M(e,t){let n=C(t);if(n.decimalPlaces<=0)return null;let{isNegative:r,intDigits:i,hasSeparator:a}=D(e,n);if(a||i.length<n.decimalPlaces+1)return null;let o=i.length-n.decimalPlaces,s=i.slice(o),c=i.slice(0,o-1),l=r?`-`:``;return k(l+c+n.decimalSeparator+s,l.length+c.length,n)}function N(e,t,n,r){let i=C(r),a=t-1;if(a<0||e[a]!==n)return null;let{isNegative:o,intDigits:s,fracDigits:c,hasSeparator:l}=D(e.slice(0,a)+e.slice(t),i);if(s!==`0`)return null;let u=+!!o+i.prefix.length;if(a<u||a>u+1)return null;let d=o?`-`:``;return k(d+n+(l?i.decimalSeparator+c:``),d.length+1,i)}function P(e,t){let n=C(t);if(!Number.isFinite(e))return``;let r=n.allowNegative&&e<0,i=Math.abs(e).toFixed(n.decimalPlaces),a=i.indexOf(`.`),o=a===-1?i:i.slice(0,a),s=a===-1?``:i.slice(a+1),c=w(o||`0`),l=(n.segmented?T(c,n.separator):c)+(n.decimalPlaces>0?n.decimalSeparator+s:``);return(r?`-`:``)+n.prefix+l+n.suffix}const F=`data-masked`;function I(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function L(e){return e.length===1&&e>=`0`&&e<=`9`}function R(e,t){if(e.getAttribute(F)!==null)return()=>{};let{onChange:n,...r}=I(t),i=r,{decimalSeparator:a,decimalPlaces:o}=C(i),s=[],c=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),s.push(t))};e.setAttribute(F,`decimal`),c(`autocomplete`,`off`),c(`autocorrect`,`off`),c(`autocapitalize`,`off`),c(`spellcheck`,`false`);let l=!1,u=v()?`keyup`:`keydown`,d=(e,t)=>{e.value=t.value,e.setSelectionRange(t.caret,t.caret),n?.(t.value,j(t.value,i))},f=e=>{let t=e.target;requestAnimationFrame(()=>{d(t,k(t.value,t.value.length,i))})},p=e=>{let t=e,n=t.target;if(!t.key){l=!0,requestAnimationFrame(()=>{let e=n.selectionStart??n.value.length;d(n,k(n.value,e,i)),requestAnimationFrame(()=>{l=!1})});return}if(t.key===`Meta`)return;if(l){t.preventDefault();return}let r=t.key===`Backspace`,s=t.key===`Delete`,c=t.key.length===1&&!t.ctrlKey&&!t.altKey&&!t.metaKey;!r&&!s&&!c||requestAnimationFrame(()=>{let e=n.selectionStart??n.value.length;if(r){let e=M(n.value,i);if(e){d(n,e);return}}o>0&&(t.key===`.`||t.key===`,`)&&t.key!==a&&e>0&&n.value[e-1]===t.key&&(n.value=n.value.slice(0,e-1)+a+n.value.slice(e)),d(n,(L(t.key)?N(n.value,e,t.key,i):null)??k(n.value,e,i))})};return e.addEventListener(`paste`,f),e.addEventListener(u,p),()=>{e.removeEventListener(`paste`,f),e.removeEventListener(u,p),e.removeAttribute(F);for(let t of s)e.removeAttribute(t)}}export{m as Mask,k as applyDecimalMask,l as applyMask,x as bind,R as bindDecimal,h as buildMask,P as formatDecimalValue,p as getMaxLength,g as process,A as processDecimal,j as unmaskDecimal};
|
|
2
2
|
//# sourceMappingURL=mother-mask.mjs.map
|
package/dist/mother-mask.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mother-mask.mjs","names":[],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts"],"sourcesContent":["import type { MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Core masking — pure function\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nexport function applyMask(value: string, mask: string, inputCaret = 0): MaskResult {\n if (!value) return { value: '', caret: 0 }\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n\n constructor(value: string, mask: string, caret = 0) {\n this._value = value\n this._mask = mask\n this.caret = caret\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(value: string, mask: MaskPattern, caret = 0): Mask {\n return new Mask(value, resolveMask(value, mask), caret)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern): string {\n return buildMask(value, mask).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask)\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mother-mask.mjs","names":["isDigitChar","MASKED_ATTR"],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts","../src/decimal-mask.ts","../src/bind-decimal.ts"],"sourcesContent":["import type { ApplyMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction isSlotChar(ch: string): boolean {\n return ch === '9' || ch === 'Z' || ch === 'A'\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Flat masking (default) — treats the mask as one continuous character\n// stream. Best for continuous identifiers (phone numbers, CPF/CNPJ, credit\n// cards) where deleting/inserting a digit anywhere is expected to reflow\n// every digit after it — this is the classic mother-mask behavior and is\n// relied on by the majority of the test suite (paste, backspace, mid-string\n// insert, etc).\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nfunction applyFlatMask(value: string, mask: string, inputCaret: number): MaskResult {\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (opt-in) — treats literal separators as hard boundaries\n// between independent fields (e.g. day/month/year in \"99/99/9999\"). Editing\n// one segment never bleeds characters into a neighboring one, so replacing\n// the \"12\" in \"25/12/2025\" with a shorter or longer value keeps the year\n// exactly where it is instead of shifting digits across the \"/\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n/**\n * Split a mask into alternating literal and slot-run tokens (e.g. \"99/99/9999\"\n * → slots\"99\", literal\"/\", slots\"99\", literal\"/\", slots\"9999\"), so the masking\n * pass can reason about segment boundaries instead of a flat character stream.\n */\nfunction tokenizeMask(mask: string): MaskToken[] {\n const tokens: MaskToken[] = []\n let i = 0\n while (i < mask.length) {\n const start = i\n const wantSlots = isSlotChar(mask[i])\n while (i < mask.length && isSlotChar(mask[i]) === wantSlots) i++\n const text = mask.slice(start, i)\n tokens.push(wantSlots ? { kind: 'slots', chars: text } : { kind: 'literal', text })\n }\n return tokens\n}\n\n/** Total slot capacity from token index `from` (inclusive) to the end of `tokens`. */\nfunction slotCapacityFrom(tokens: MaskToken[], from: number): number {\n let capacity = 0\n for (let i = from; i < tokens.length; i++) {\n const token = tokens[i]\n if (token.kind === 'slots') capacity += token.chars.length\n }\n return capacity\n}\n\n/** Count of remaining slot-matchable (digit/letter) characters in `value` from `fromIdx` onward. */\nfunction remainingDataChars(value: string, fromIdx: number): number {\n let count = 0\n for (let i = fromIdx; i < value.length; i++) {\n const ch = value[i]\n if (isDigitChar(ch) || isLetterChar(ch)) count++\n }\n return count\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but walks the mask one *segment* at\n * a time (a run of slots, or a literal) rather than one character at a time.\n * The rule that keeps an edit inside a segment from crossing into its\n * neighbor is an **early stop**: if the value hits the literal that ends the\n * current slot run before all of that run's slots are filled (e.g. only one\n * digit typed into a two-digit month slot), the run is left partially filled\n * instead of skipping past the separator to steal a digit from the next\n * segment.\n *\n * That stop is only taken when it's actually safe — i.e. every character\n * still to come in `value` fits in the slot capacity that remains *after*\n * this segment. If stopping here would strand more data than the rest of\n * the mask can hold (e.g. pasting into a later segment while an earlier one\n * still sits under-filled from before), the \"separator\" is treated as stray\n * noise instead and skipped, letting this segment take the extra slot it\n * needs so nothing at the end gets silently dropped. This is also what lets\n * an array mask grow or shrink its pattern (moving every literal after the\n * change point) reflow correctly instead of losing a digit at the boundary.\n */\nfunction applySegmentedMask(value: string, mask: string, inputCaret: number): MaskResult {\n const tokens = tokenizeMask(mask)\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n let exhausted = false\n\n for (let t = 0; t < tokens.length && !exhausted; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n pending += token.text\n if (value.startsWith(token.text, valueIdx)) valueIdx += token.text.length\n continue\n }\n\n const nextToken = tokens[t + 1]\n const capacityAfter = slotCapacityFrom(tokens, t + 1)\n\n for (let s = 0; s < token.chars.length; s++) {\n const slotCh = token.chars[s]\n let found = false\n\n while (valueIdx < value.length) {\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, slotCh)) {\n valueIdx++\n output += pending + ch\n pending = ''\n found = true\n\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n\n // This segment's ending separator showed up before the run filled.\n // Stopping here is only safe if the rest of the value still fits in\n // whatever slot capacity remains after this segment — otherwise\n // stopping would strand data, so fall through and skip this char as\n // noise instead, letting the segment take the slot it needs.\n if (\n nextToken?.kind === 'literal' &&\n value.startsWith(nextToken.text, valueIdx) &&\n remainingDataChars(value, valueIdx) <= capacityAfter\n ) {\n break\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n if (!found) {\n if (valueIdx >= value.length) exhausted = true\n break\n }\n }\n }\n\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\nexport function applyMask(\n value: string,\n mask: string,\n inputCaret = 0,\n options?: ApplyMaskOptions,\n): MaskResult {\n if (!value) return { value: '', caret: 0 }\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret)\n : applySegmentedMask(value, mask, inputCaret)\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { ApplyMaskOptions, MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n private readonly _options: ApplyMaskOptions | undefined\n\n constructor(value: string, mask: string, caret = 0, options?: ApplyMaskOptions) {\n this._value = value\n this._mask = mask\n this.caret = caret\n this._options = options\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret, this._options)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(\n value: string,\n mask: MaskPattern,\n caret = 0,\n options?: ApplyMaskOptions,\n): Mask {\n return new Mask(value, resolveMask(value, mask), caret, options)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string {\n return buildMask(value, mask, 0, options).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, segmented } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask, 0, { segmented })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n","import type { DecimalMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — mirrors apply-mask.ts)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\n// ---------------------------------------------------------------------------\n// Option resolution\n// ---------------------------------------------------------------------------\n\ninterface ResolvedDecimalOptions {\n decimalPlaces: number\n segmented: boolean\n separator: string\n decimalSeparator: string\n prefix: string\n suffix: string\n allowNegative: boolean\n}\n\n/** @internal exported for {@link bindDecimal}'s \".\" / \",\" key normalization */\nexport function resolveDecimalOptions(options?: DecimalMaskOptions): ResolvedDecimalOptions {\n const rawPlaces = options?.decimalPlaces ?? 2\n const decimalPlaces = Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : 2\n return {\n decimalPlaces,\n segmented: options?.segmented ?? true,\n separator: options?.separator ?? ',',\n decimalSeparator: options?.decimalSeparator ?? '.',\n prefix: options?.prefix ?? '',\n suffix: options?.suffix ?? '',\n allowNegative: options?.allowNegative ?? false,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Digit-stream helpers\n// ---------------------------------------------------------------------------\n\n/** Strip leading zeros from a digit string, always keeping at least one digit. */\nfunction stripLeadingZeros(s: string): string {\n let i = 0\n while (i < s.length - 1 && s[i] === '0') i++\n return s.slice(i)\n}\n\n/** Insert `sep` every 3 digits from the right (e.g. \"1234567\" → \"1,234,567\"). */\nfunction groupThousands(s: string, sep: string): string {\n if (!sep || s.length <= 3) return s\n const parts: string[] = []\n let i = s.length\n while (i > 3) {\n parts.unshift(s.slice(i - 3, i))\n i -= 3\n }\n parts.unshift(s.slice(0, i))\n return parts.join(sep)\n}\n\n/**\n * Find the position in `s` that leaves exactly `digitsBefore` digit\n * characters preceding it — the position immediately after that digit and\n * before any subsequent literal (grouping separator, ...), so the caret\n * stays glued to the last digit the user placed there.\n */\nfunction caretForDigitsBefore(s: string, digitsBefore: number): number {\n if (digitsBefore <= 0) return 0\n let count = 0\n for (let i = 0; i < s.length; i++) {\n if (isDigitChar(s[i])) {\n count++\n if (count === digitsBefore) return i + 1\n }\n }\n return s.length\n}\n\n// ---------------------------------------------------------------------------\n// Segmented parsing — integer digits before the decimal separator, fraction\n// digits after. Unlike a slot-pattern mask, the integer segment has no fixed\n// length; only the fraction is fixed-width (`decimalPlaces`), zero-padded on\n// the right so a shorter fraction reads as its low-order (trailing) digits\n// being zero rather than reflowing/shifting — e.g. editing \"423,42\" down to\n// \"423,4\" produces \"423,40\", not \"42,34\".\n//\n// The decimal separator only has meaning as the *first* occurrence of\n// `opts.decimalSeparator`; every other non-digit character (thousands\n// separator, prefix/suffix text, a second stray separator, ...) is noise and\n// is dropped. This keeps re-parsing an already-masked value idempotent.\n// ---------------------------------------------------------------------------\n\ninterface DecimalParts {\n isNegative: boolean\n intDigits: string\n fracDigits: string\n hasSeparator: boolean\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n let intDigits = ''\n let fracDigits = ''\n let isNegative = false\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces > 0\n\n for (const ch of raw) {\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else {\n intDigits += ch\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n continue\n }\n if (ch === '-' && opts.allowNegative) isNegative = true\n // Anything else — thousands separator, prefix/suffix text, a repeated\n // separator, stray letters — is noise and is dropped.\n }\n\n return { isNegative, intDigits, fracDigits, hasSeparator: inFraction }\n}\n\n/**\n * Walk `raw[0:caret]` to find which segment the caret sits in (integer or\n * fraction) and how many digits of that segment precede it, so the same\n * position can be re-derived in the freshly formatted output.\n */\nfunction locateCaretSegment(\n raw: string,\n caret: number,\n opts: ResolvedDecimalOptions,\n): { inFraction: boolean; digitsBefore: number } {\n const canHaveFraction = opts.decimalPlaces > 0\n let inFraction = false\n let digitsBefore = 0\n\n for (let i = 0; i < caret; i++) {\n const ch = raw[i]\n if (isDigitChar(ch)) {\n if (!inFraction || digitsBefore < opts.decimalPlaces) digitsBefore++\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n digitsBefore = 0\n }\n }\n\n return { inFraction, digitsBefore }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry points\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a decimal/currency mask to a value, producing the masked output and\n * a computed caret position. Digits typed before the decimal separator\n * extend the integer part; the fraction only starts once the separator is\n * typed, and is always displayed zero-padded to `decimalPlaces` width.\n */\nexport function applyDecimalMask(\n value: string,\n inputCaret = 0,\n options?: DecimalMaskOptions,\n): MaskResult {\n const opts = resolveDecimalOptions(options)\n if (!value) return { value: '', caret: 0 }\n\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (intDigits === '' && fracDigits === '' && !hasSeparator) return { value: '', caret: 0 }\n\n const intPart = stripLeadingZeros(intDigits || '0')\n const groupedInt = opts.segmented ? groupThousands(intPart, opts.separator) : intPart\n const fracPadded = opts.decimalPlaces > 0 ? fracDigits.padEnd(opts.decimalPlaces, '0') : ''\n const numberStr = groupedInt + (opts.decimalPlaces > 0 ? opts.decimalSeparator + fracPadded : '')\n const signStr = isNegative ? '-' : ''\n const output = signStr + opts.prefix + numberStr + opts.suffix\n\n const clampedCaret = Math.max(0, Math.min(inputCaret, value.length))\n const { inFraction, digitsBefore } = locateCaretSegment(value, clampedCaret, opts)\n const prefixLen = signStr.length + opts.prefix.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore)\n\n return { value: output, caret }\n}\n\n/** Apply a decimal mask to a raw value and return just the masked string. */\nexport function processDecimal(value: string, options?: DecimalMaskOptions): string {\n return applyDecimalMask(value, value.length, options).value\n}\n\n/**\n * Parse a raw or already-masked decimal value back into a JS number.\n * Ignores prefix/suffix/thousands separator; returns `0` for an empty or\n * digit-less value.\n */\nexport function unmaskDecimal(value: string, options?: DecimalMaskOptions): number {\n const opts = resolveDecimalOptions(options)\n const { isNegative, intDigits, fracDigits } = computeDecimalParts(value, opts)\n const n = Number(fracDigits ? `${intDigits || '0'}.${fracDigits}` : intDigits || '0')\n return isNegative ? -n : n\n}\n\n/**\n * After a Backspace removes the decimal separator itself, the integer and\n * fraction digit runs collapse into one continuous stream (e.g. \"25.00\"\n * with the caret right after \".\" → Backspace deletes the \".\" → \"2500\").\n * Left alone, that reads as one big integer (\"$2,500.00\"). This restores\n * the segment boundary instead: the trailing `decimalPlaces` digits are\n * still the fraction, and the digit right before them — the one that used\n * to sit at the end of the integer part — is the one Backspace actually\n * removed, so it's dropped (not kept) — \"$25.00\" → \"$2.00\".\n *\n * Every reformat re-appends `decimalSeparator` whenever `decimalPlaces > 0`,\n * so its absence from `value` is an unambiguous signal that this exact\n * keystroke just deleted it — no \"value before this keystroke\" snapshot is\n * needed. Returns `null` when there's nothing to restore (the separator is\n * still present, `decimalPlaces` is `0`, or too few digits remain), so the\n * caller falls through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskUnmergingSeparator(\n value: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n if (opts.decimalPlaces <= 0) return null\n\n const { isNegative, intDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (hasSeparator || intDigits.length < opts.decimalPlaces + 1) return null\n\n const preMergeIntLength = intDigits.length - opts.decimalPlaces\n const fracDigits = intDigits.slice(preMergeIntLength)\n const remainingInt = intDigits.slice(0, preMergeIntLength - 1)\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + remainingInt + opts.decimalSeparator + fracDigits\n return applyDecimalMask(raw, signPart.length + remainingInt.length, opts)\n}\n\n/**\n * Special-cases typing a single digit into a field whose integer part is\n * exactly the auto-inserted \"0\" placeholder: the new digit replaces that\n * zero instead of combining with it — e.g. \"$0.00\" with the caret anywhere\n * against that lone \"0\" and typing \"2\" gives \"$2.00\", not \"$20.00\"/\"$02.00\".\n * Any already-typed fraction is preserved.\n *\n * `value`/`caret` must be the state *after* the browser has already\n * inserted `digit` at `caret - 1` (the same post-insertion snapshot\n * `applyDecimalMask` itself expects from `bindDecimal`). Returns `null`\n * when the pattern doesn't apply — either the integer part has real digits\n * already, or the caret wasn't against that lone zero — so the caller falls\n * through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskReplacingLoneZero(\n value: string,\n caret: number,\n digit: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n const insertIdx = caret - 1\n if (insertIdx < 0 || value[insertIdx] !== digit) return null\n\n const withoutDigit = value.slice(0, insertIdx) + value.slice(caret)\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(withoutDigit, opts)\n if (intDigits !== '0') return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + 1) return null\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + digit + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + 1, opts)\n}\n\n/** Format a plain JS number into its masked display string. */\nexport function formatDecimalValue(value: number, options?: DecimalMaskOptions): string {\n const opts = resolveDecimalOptions(options)\n if (!Number.isFinite(value)) return ''\n\n const isNegative = opts.allowNegative && value < 0\n const fixed = Math.abs(value).toFixed(opts.decimalPlaces)\n const dotIdx = fixed.indexOf('.')\n const intRaw = dotIdx === -1 ? fixed : fixed.slice(0, dotIdx)\n const fracPart = dotIdx === -1 ? '' : fixed.slice(dotIdx + 1)\n const intPart = stripLeadingZeros(intRaw || '0')\n\n const groupedInt = opts.segmented ? groupThousands(intPart, opts.separator) : intPart\n const numberStr = groupedInt + (opts.decimalPlaces > 0 ? opts.decimalSeparator + fracPart : '')\n\n return (isNegative ? '-' : '') + opts.prefix + numberStr + opts.suffix\n}\n","import {\n applyDecimalMask,\n applyDecimalMaskReplacingLoneZero,\n applyDecimalMaskUnmergingSeparator,\n resolveDecimalOptions,\n unmaskDecimal,\n} from './decimal-mask'\nimport { isIos } from './platform'\nimport type { BindDecimalOptions, DecimalMaskOptions, MaskResult } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindDecimalOptions(\n second:\n | BindDecimalOptions\n | ((value: string, numericValue: number) => void)\n | null\n | undefined,\n): BindDecimalOptions {\n if (second == null) return {}\n if (typeof second === 'function') return { onChange: second }\n return second\n}\n\nfunction isDigitKey(key: string): boolean {\n return key.length === 1 && key >= '0' && key <= '9'\n}\n\n/**\n * Bind a decimal/currency mask to an input element.\n *\n * Same contract as {@link bind}: idempotent (marked with `data-masked`),\n * returns a dispose function, and reformats on paste and keyboard-driven\n * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no\n * fixed pattern — the integer part grows and shrinks freely; formatting is\n * driven entirely by `options`.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.\n */\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n options?: BindDecimalOptions | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n onChange: ((value: string, numericValue: number) => void) | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n second?: BindDecimalOptions | ((value: string, numericValue: number) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, ...maskOptions } = toBindDecimalOptions(second)\n const decimalOptions: DecimalMaskOptions = maskOptions\n const { decimalSeparator, decimalPlaces } = resolveDecimalOptions(decimalOptions)\n\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, 'decimal')\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n\n let lockInput = false\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n target.setSelectionRange(m.caret, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n applyResult(target, applyDecimalMask(target.value, target.value.length, decimalOptions))\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n\n // Navigation (arrows, Home/End, Tab, ...), selection, and shortcut keys\n // (Ctrl/Cmd+A, Ctrl/Cmd+C, ...) don't change the text — leave the\n // browser's native caret/selection handling alone instead of recomputing\n // and overwriting it on every keystroke.\n if (!isBackspace && !isDelete && !isCharInsert) return\n\n // Everything below reads `target.value`/`selectionStart` inside the rAF\n // callback rather than synchronously here, since the browser's native\n // character insertion for this keystroke isn't guaranteed to have landed\n // yet at the point a keydown listener runs — only by the next frame.\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n\n // Backspace that just deleted the decimal separator merges the\n // integer and fraction digit runs into one continuous stream —\n // restore the boundary instead of treating that as one big integer.\n if (isBackspace) {\n const unmerged = applyDecimalMaskUnmergingSeparator(target.value, decimalOptions)\n if (unmerged) {\n applyResult(target, unmerged)\n return\n }\n }\n\n // A numeric keypad (or a locale mismatch) may only offer \".\" or \",\".\n // Normalize whichever one the user just typed to the configured\n // `decimalSeparator` so it reliably opens the fraction segment.\n if (\n decimalPlaces > 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator &&\n pos > 0 &&\n target.value[pos - 1] === ke.key\n ) {\n target.value = target.value.slice(0, pos - 1) + decimalSeparator + target.value.slice(pos)\n }\n\n // Typing a digit into a field whose integer part is still the\n // auto-inserted \"0\" placeholder replaces that zero instead of\n // combining with it (e.g. \"$0.00\" + \"2\" → \"$2.00\", not \"$20.00\").\n const replaced = isDigitKey(ke.key)\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, ke.key, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n"],"mappings":"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"}
|
package/dist/mother-mask.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.MotherMask={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){return e>=`0`&&e<=`9`}function n(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function r(e,r){return r===`9`?t(e):r===`Z`?n(e):t(e)||n(e)}function
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.MotherMask={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){return e>=`0`&&e<=`9`}function n(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function r(e){return e===`9`||e===`Z`||e===`A`}function i(e,r){return r===`9`?t(e):r===`Z`?n(e):t(e)||n(e)}function a(e,t,n){let r=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(i(t,u)){r+=a+t,a=``,d=!0,c||(o<=n?s=r.length:c=!0);break}}if(!d)break}return c||(s=r.length),{value:r,caret:s}}function o(e){let t=[],n=0;for(;n<e.length;){let i=n,a=r(e[n]);for(;n<e.length&&r(e[n])===a;)n++;let o=e.slice(i,n);t.push(a?{kind:`slots`,chars:o}:{kind:`literal`,text:o})}return t}function s(e,t){let n=0;for(let r=t;r<e.length;r++){let t=e[r];t.kind===`slots`&&(n+=t.chars.length)}return n}function c(e,r){let i=0;for(let a=r;a<e.length;a++){let r=e[a];(t(r)||n(r))&&i++}return i}function l(e,t,n){let r=o(t),a=``,l=``,u=0,d=0,f=!1,p=!1;for(let t=0;t<r.length&&!p;t++){let o=r[t];if(o.kind===`literal`){l+=o.text,e.startsWith(o.text,u)&&(u+=o.text.length);continue}let m=r[t+1],h=s(r,t+1);for(let t=0;t<o.chars.length;t++){let r=o.chars[t],s=!1;for(;u<e.length;){let t=e[u];if(i(t,r)){u++,a+=l+t,l=``,s=!0,f||(u<=n?d=a.length:f=!0);break}if(m?.kind===`literal`&&e.startsWith(m.text,u)&&c(e,u)<=h)break;u++}if(!s){u>=e.length&&(p=!0);break}}}return f||(d=a.length),{value:a,caret:d}}function u(e,t,n=0,r){return e?r?.segmented===!1?a(e,t,n):l(e,t,n):{value:``,caret:0}}function d(e){let t=0;for(let n of e)(n>=`0`&&n<=`9`||n>=`a`&&n<=`z`||n>=`A`&&n<=`Z`)&&t++;return t}function f(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function p(e,t){if(!Array.isArray(t))return t;let n=d(e),r=0;for(;r<t.length-1&&n>f(t[r]);)r++;return t[r]}function m(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var h=class{caret;_value;_mask;_options;constructor(e,t,n=0,r){this._value=e,this._mask=t,this.caret=n,this._options=r}process(){let e=u(this._value,this._mask,this.caret,this._options);return this.caret=e.caret,e.value}};function g(e,t,n=0,r){return new h(e,p(e,t),n,r)}function _(e,t,n){return g(e,t,0,n).process()}let v;function y(){return v===void 0&&(v=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),v}let b=`data-masked`;function x(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function S(e,t,n){if(e.getAttribute(b)!==null)return()=>{};let{onChange:r,segmented:i}=x(n),a=[],o=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),a.push(t))};e.setAttribute(b,Array.isArray(t)?t.join(`|`):t),o(`autocomplete`,`off`),o(`autocorrect`,`off`),o(`autocapitalize`,`off`),o(`spellcheck`,`false`),o(`maxlength`,String(m(t)));let s=!1,c=y()?`keyup`:`keydown`,l=e=>{let n=e.target;requestAnimationFrame(()=>{n.value=g(n.value,t,0,{segmented:i}).process(),r?.(n.value)})},u=e=>{let n=e,a=n.target,o=a.value;if(!n.key){s=!0,requestAnimationFrame(()=>{let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});a.value=n.process(),a.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{s=!1})});return}if(n.key===`Meta`)return;let c=n.key===`Backspace`,l=n.key===`Delete`,u=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,d=n.key===`Unidentified`;if(u&&a.selectionStart===a.selectionEnd&&o.length>=m(t)&&!y()){n.preventDefault();return}if(s){n.preventDefault();return}requestAnimationFrame(()=>{let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});if(a.value=n.process(),d){let t=a.value.length>o.length?n.caret:e;a.setSelectionRange(t,t)}else if(l){let t=o.length===a.value.length?e+1:e;a.setSelectionRange(t,t)}else c?a.setSelectionRange(e,e):u&&a.setSelectionRange(n.caret,n.caret);r?.(a.value)})};return e.addEventListener(`paste`,l),e.addEventListener(c,u),()=>{e.removeEventListener(`paste`,l),e.removeEventListener(c,u),e.removeAttribute(b);for(let t of a)e.removeAttribute(t)}}function C(e){return e>=`0`&&e<=`9`}function w(e){let t=e?.decimalPlaces??2;return{decimalPlaces:Number.isFinite(t)?Math.max(0,Math.floor(t)):2,segmented:e?.segmented??!0,separator:e?.separator??`,`,decimalSeparator:e?.decimalSeparator??`.`,prefix:e?.prefix??``,suffix:e?.suffix??``,allowNegative:e?.allowNegative??!1}}function T(e){let t=0;for(;t<e.length-1&&e[t]===`0`;)t++;return e.slice(t)}function E(e,t){if(!t||e.length<=3)return e;let n=[],r=e.length;for(;r>3;)n.unshift(e.slice(r-3,r)),r-=3;return n.unshift(e.slice(0,r)),n.join(t)}function D(e,t){if(t<=0)return 0;let n=0;for(let r=0;r<e.length;r++)if(C(e[r])&&(n++,n===t))return r+1;return e.length}function O(e,t){let n=``,r=``,i=!1,a=!1,o=t.decimalPlaces>0;for(let s of e){if(C(s)){a?r.length<t.decimalPlaces&&(r+=s):n+=s;continue}if(o&&!a&&s===t.decimalSeparator){a=!0;continue}s===`-`&&t.allowNegative&&(i=!0)}return{isNegative:i,intDigits:n,fracDigits:r,hasSeparator:a}}function k(e,t,n){let r=n.decimalPlaces>0,i=!1,a=0;for(let o=0;o<t;o++){let t=e[o];if(C(t)){(!i||a<n.decimalPlaces)&&a++;continue}r&&!i&&t===n.decimalSeparator&&(i=!0,a=0)}return{inFraction:i,digitsBefore:a}}function A(e,t=0,n){let r=w(n);if(!e)return{value:``,caret:0};let{isNegative:i,intDigits:a,fracDigits:o,hasSeparator:s}=O(e,r);if(a===``&&o===``&&!s)return{value:``,caret:0};let c=T(a||`0`),l=r.segmented?E(c,r.separator):c,u=r.decimalPlaces>0?o.padEnd(r.decimalPlaces,`0`):``,d=l+(r.decimalPlaces>0?r.decimalSeparator+u:``),f=i?`-`:``,p=f+r.prefix+d+r.suffix,{inFraction:m,digitsBefore:h}=k(e,Math.max(0,Math.min(t,e.length)),r),g=f.length+r.prefix.length;return{value:p,caret:m?g+l.length+r.decimalSeparator.length+h:g+D(l,h)}}function j(e,t){return A(e,e.length,t).value}function M(e,t){let{isNegative:n,intDigits:r,fracDigits:i}=O(e,w(t)),a=Number(i?`${r||`0`}.${i}`:r||`0`);return n?-a:a}function N(e,t){let n=w(t);if(n.decimalPlaces<=0)return null;let{isNegative:r,intDigits:i,hasSeparator:a}=O(e,n);if(a||i.length<n.decimalPlaces+1)return null;let o=i.length-n.decimalPlaces,s=i.slice(o),c=i.slice(0,o-1),l=r?`-`:``;return A(l+c+n.decimalSeparator+s,l.length+c.length,n)}function P(e,t,n,r){let i=w(r),a=t-1;if(a<0||e[a]!==n)return null;let{isNegative:o,intDigits:s,fracDigits:c,hasSeparator:l}=O(e.slice(0,a)+e.slice(t),i);if(s!==`0`)return null;let u=+!!o+i.prefix.length;if(a<u||a>u+1)return null;let d=o?`-`:``;return A(d+n+(l?i.decimalSeparator+c:``),d.length+1,i)}function F(e,t){let n=w(t);if(!Number.isFinite(e))return``;let r=n.allowNegative&&e<0,i=Math.abs(e).toFixed(n.decimalPlaces),a=i.indexOf(`.`),o=a===-1?i:i.slice(0,a),s=a===-1?``:i.slice(a+1),c=T(o||`0`),l=(n.segmented?E(c,n.separator):c)+(n.decimalPlaces>0?n.decimalSeparator+s:``);return(r?`-`:``)+n.prefix+l+n.suffix}let I=`data-masked`;function L(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function R(e){return e.length===1&&e>=`0`&&e<=`9`}function z(e,t){if(e.getAttribute(I)!==null)return()=>{};let{onChange:n,...r}=L(t),i=r,{decimalSeparator:a,decimalPlaces:o}=w(i),s=[],c=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),s.push(t))};e.setAttribute(I,`decimal`),c(`autocomplete`,`off`),c(`autocorrect`,`off`),c(`autocapitalize`,`off`),c(`spellcheck`,`false`);let l=!1,u=y()?`keyup`:`keydown`,d=(e,t)=>{e.value=t.value,e.setSelectionRange(t.caret,t.caret),n?.(t.value,M(t.value,i))},f=e=>{let t=e.target;requestAnimationFrame(()=>{d(t,A(t.value,t.value.length,i))})},p=e=>{let t=e,n=t.target;if(!t.key){l=!0,requestAnimationFrame(()=>{let e=n.selectionStart??n.value.length;d(n,A(n.value,e,i)),requestAnimationFrame(()=>{l=!1})});return}if(t.key===`Meta`)return;if(l){t.preventDefault();return}let r=t.key===`Backspace`,s=t.key===`Delete`,c=t.key.length===1&&!t.ctrlKey&&!t.altKey&&!t.metaKey;!r&&!s&&!c||requestAnimationFrame(()=>{let e=n.selectionStart??n.value.length;if(r){let e=N(n.value,i);if(e){d(n,e);return}}o>0&&(t.key===`.`||t.key===`,`)&&t.key!==a&&e>0&&n.value[e-1]===t.key&&(n.value=n.value.slice(0,e-1)+a+n.value.slice(e)),d(n,(R(t.key)?P(n.value,e,t.key,i):null)??A(n.value,e,i))})};return e.addEventListener(`paste`,f),e.addEventListener(u,p),()=>{e.removeEventListener(`paste`,f),e.removeEventListener(u,p),e.removeAttribute(I);for(let t of s)e.removeAttribute(t)}}e.Mask=h,e.applyDecimalMask=A,e.applyMask=u,e.bind=S,e.bindDecimal=z,e.buildMask=g,e.formatDecimalValue=F,e.getMaxLength=m,e.process=_,e.processDecimal=j,e.unmaskDecimal=M});
|
|
2
2
|
//# sourceMappingURL=mother-mask.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mother-mask.umd.js","names":[],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts"],"sourcesContent":["import type { MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Core masking — pure function\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nexport function applyMask(value: string, mask: string, inputCaret = 0): MaskResult {\n if (!value) return { value: '', caret: 0 }\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n\n constructor(value: string, mask: string, caret = 0) {\n this._value = value\n this._mask = mask\n this.caret = caret\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(value: string, mask: MaskPattern, caret = 0): Mask {\n return new Mask(value, resolveMask(value, mask), caret)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern): string {\n return buildMask(value, mask).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask)\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mother-mask.umd.js","names":["isDigitChar","MASKED_ATTR"],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts","../src/decimal-mask.ts","../src/bind-decimal.ts"],"sourcesContent":["import type { ApplyMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction isSlotChar(ch: string): boolean {\n return ch === '9' || ch === 'Z' || ch === 'A'\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Flat masking (default) — treats the mask as one continuous character\n// stream. Best for continuous identifiers (phone numbers, CPF/CNPJ, credit\n// cards) where deleting/inserting a digit anywhere is expected to reflow\n// every digit after it — this is the classic mother-mask behavior and is\n// relied on by the majority of the test suite (paste, backspace, mid-string\n// insert, etc).\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nfunction applyFlatMask(value: string, mask: string, inputCaret: number): MaskResult {\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (opt-in) — treats literal separators as hard boundaries\n// between independent fields (e.g. day/month/year in \"99/99/9999\"). Editing\n// one segment never bleeds characters into a neighboring one, so replacing\n// the \"12\" in \"25/12/2025\" with a shorter or longer value keeps the year\n// exactly where it is instead of shifting digits across the \"/\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n/**\n * Split a mask into alternating literal and slot-run tokens (e.g. \"99/99/9999\"\n * → slots\"99\", literal\"/\", slots\"99\", literal\"/\", slots\"9999\"), so the masking\n * pass can reason about segment boundaries instead of a flat character stream.\n */\nfunction tokenizeMask(mask: string): MaskToken[] {\n const tokens: MaskToken[] = []\n let i = 0\n while (i < mask.length) {\n const start = i\n const wantSlots = isSlotChar(mask[i])\n while (i < mask.length && isSlotChar(mask[i]) === wantSlots) i++\n const text = mask.slice(start, i)\n tokens.push(wantSlots ? { kind: 'slots', chars: text } : { kind: 'literal', text })\n }\n return tokens\n}\n\n/** Total slot capacity from token index `from` (inclusive) to the end of `tokens`. */\nfunction slotCapacityFrom(tokens: MaskToken[], from: number): number {\n let capacity = 0\n for (let i = from; i < tokens.length; i++) {\n const token = tokens[i]\n if (token.kind === 'slots') capacity += token.chars.length\n }\n return capacity\n}\n\n/** Count of remaining slot-matchable (digit/letter) characters in `value` from `fromIdx` onward. */\nfunction remainingDataChars(value: string, fromIdx: number): number {\n let count = 0\n for (let i = fromIdx; i < value.length; i++) {\n const ch = value[i]\n if (isDigitChar(ch) || isLetterChar(ch)) count++\n }\n return count\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but walks the mask one *segment* at\n * a time (a run of slots, or a literal) rather than one character at a time.\n * The rule that keeps an edit inside a segment from crossing into its\n * neighbor is an **early stop**: if the value hits the literal that ends the\n * current slot run before all of that run's slots are filled (e.g. only one\n * digit typed into a two-digit month slot), the run is left partially filled\n * instead of skipping past the separator to steal a digit from the next\n * segment.\n *\n * That stop is only taken when it's actually safe — i.e. every character\n * still to come in `value` fits in the slot capacity that remains *after*\n * this segment. If stopping here would strand more data than the rest of\n * the mask can hold (e.g. pasting into a later segment while an earlier one\n * still sits under-filled from before), the \"separator\" is treated as stray\n * noise instead and skipped, letting this segment take the extra slot it\n * needs so nothing at the end gets silently dropped. This is also what lets\n * an array mask grow or shrink its pattern (moving every literal after the\n * change point) reflow correctly instead of losing a digit at the boundary.\n */\nfunction applySegmentedMask(value: string, mask: string, inputCaret: number): MaskResult {\n const tokens = tokenizeMask(mask)\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n let exhausted = false\n\n for (let t = 0; t < tokens.length && !exhausted; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n pending += token.text\n if (value.startsWith(token.text, valueIdx)) valueIdx += token.text.length\n continue\n }\n\n const nextToken = tokens[t + 1]\n const capacityAfter = slotCapacityFrom(tokens, t + 1)\n\n for (let s = 0; s < token.chars.length; s++) {\n const slotCh = token.chars[s]\n let found = false\n\n while (valueIdx < value.length) {\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, slotCh)) {\n valueIdx++\n output += pending + ch\n pending = ''\n found = true\n\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n\n // This segment's ending separator showed up before the run filled.\n // Stopping here is only safe if the rest of the value still fits in\n // whatever slot capacity remains after this segment — otherwise\n // stopping would strand data, so fall through and skip this char as\n // noise instead, letting the segment take the slot it needs.\n if (\n nextToken?.kind === 'literal' &&\n value.startsWith(nextToken.text, valueIdx) &&\n remainingDataChars(value, valueIdx) <= capacityAfter\n ) {\n break\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n if (!found) {\n if (valueIdx >= value.length) exhausted = true\n break\n }\n }\n }\n\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\nexport function applyMask(\n value: string,\n mask: string,\n inputCaret = 0,\n options?: ApplyMaskOptions,\n): MaskResult {\n if (!value) return { value: '', caret: 0 }\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret)\n : applySegmentedMask(value, mask, inputCaret)\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { ApplyMaskOptions, MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n private readonly _options: ApplyMaskOptions | undefined\n\n constructor(value: string, mask: string, caret = 0, options?: ApplyMaskOptions) {\n this._value = value\n this._mask = mask\n this.caret = caret\n this._options = options\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret, this._options)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(\n value: string,\n mask: MaskPattern,\n caret = 0,\n options?: ApplyMaskOptions,\n): Mask {\n return new Mask(value, resolveMask(value, mask), caret, options)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string {\n return buildMask(value, mask, 0, options).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, segmented } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask, 0, { segmented })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n","import type { DecimalMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — mirrors apply-mask.ts)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\n// ---------------------------------------------------------------------------\n// Option resolution\n// ---------------------------------------------------------------------------\n\ninterface ResolvedDecimalOptions {\n decimalPlaces: number\n segmented: boolean\n separator: string\n decimalSeparator: string\n prefix: string\n suffix: string\n allowNegative: boolean\n}\n\n/** @internal exported for {@link bindDecimal}'s \".\" / \",\" key normalization */\nexport function resolveDecimalOptions(options?: DecimalMaskOptions): ResolvedDecimalOptions {\n const rawPlaces = options?.decimalPlaces ?? 2\n const decimalPlaces = Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : 2\n return {\n decimalPlaces,\n segmented: options?.segmented ?? true,\n separator: options?.separator ?? ',',\n decimalSeparator: options?.decimalSeparator ?? '.',\n prefix: options?.prefix ?? '',\n suffix: options?.suffix ?? '',\n allowNegative: options?.allowNegative ?? false,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Digit-stream helpers\n// ---------------------------------------------------------------------------\n\n/** Strip leading zeros from a digit string, always keeping at least one digit. */\nfunction stripLeadingZeros(s: string): string {\n let i = 0\n while (i < s.length - 1 && s[i] === '0') i++\n return s.slice(i)\n}\n\n/** Insert `sep` every 3 digits from the right (e.g. \"1234567\" → \"1,234,567\"). */\nfunction groupThousands(s: string, sep: string): string {\n if (!sep || s.length <= 3) return s\n const parts: string[] = []\n let i = s.length\n while (i > 3) {\n parts.unshift(s.slice(i - 3, i))\n i -= 3\n }\n parts.unshift(s.slice(0, i))\n return parts.join(sep)\n}\n\n/**\n * Find the position in `s` that leaves exactly `digitsBefore` digit\n * characters preceding it — the position immediately after that digit and\n * before any subsequent literal (grouping separator, ...), so the caret\n * stays glued to the last digit the user placed there.\n */\nfunction caretForDigitsBefore(s: string, digitsBefore: number): number {\n if (digitsBefore <= 0) return 0\n let count = 0\n for (let i = 0; i < s.length; i++) {\n if (isDigitChar(s[i])) {\n count++\n if (count === digitsBefore) return i + 1\n }\n }\n return s.length\n}\n\n// ---------------------------------------------------------------------------\n// Segmented parsing — integer digits before the decimal separator, fraction\n// digits after. Unlike a slot-pattern mask, the integer segment has no fixed\n// length; only the fraction is fixed-width (`decimalPlaces`), zero-padded on\n// the right so a shorter fraction reads as its low-order (trailing) digits\n// being zero rather than reflowing/shifting — e.g. editing \"423,42\" down to\n// \"423,4\" produces \"423,40\", not \"42,34\".\n//\n// The decimal separator only has meaning as the *first* occurrence of\n// `opts.decimalSeparator`; every other non-digit character (thousands\n// separator, prefix/suffix text, a second stray separator, ...) is noise and\n// is dropped. This keeps re-parsing an already-masked value idempotent.\n// ---------------------------------------------------------------------------\n\ninterface DecimalParts {\n isNegative: boolean\n intDigits: string\n fracDigits: string\n hasSeparator: boolean\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n let intDigits = ''\n let fracDigits = ''\n let isNegative = false\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces > 0\n\n for (const ch of raw) {\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else {\n intDigits += ch\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n continue\n }\n if (ch === '-' && opts.allowNegative) isNegative = true\n // Anything else — thousands separator, prefix/suffix text, a repeated\n // separator, stray letters — is noise and is dropped.\n }\n\n return { isNegative, intDigits, fracDigits, hasSeparator: inFraction }\n}\n\n/**\n * Walk `raw[0:caret]` to find which segment the caret sits in (integer or\n * fraction) and how many digits of that segment precede it, so the same\n * position can be re-derived in the freshly formatted output.\n */\nfunction locateCaretSegment(\n raw: string,\n caret: number,\n opts: ResolvedDecimalOptions,\n): { inFraction: boolean; digitsBefore: number } {\n const canHaveFraction = opts.decimalPlaces > 0\n let inFraction = false\n let digitsBefore = 0\n\n for (let i = 0; i < caret; i++) {\n const ch = raw[i]\n if (isDigitChar(ch)) {\n if (!inFraction || digitsBefore < opts.decimalPlaces) digitsBefore++\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n digitsBefore = 0\n }\n }\n\n return { inFraction, digitsBefore }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry points\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a decimal/currency mask to a value, producing the masked output and\n * a computed caret position. Digits typed before the decimal separator\n * extend the integer part; the fraction only starts once the separator is\n * typed, and is always displayed zero-padded to `decimalPlaces` width.\n */\nexport function applyDecimalMask(\n value: string,\n inputCaret = 0,\n options?: DecimalMaskOptions,\n): MaskResult {\n const opts = resolveDecimalOptions(options)\n if (!value) return { value: '', caret: 0 }\n\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (intDigits === '' && fracDigits === '' && !hasSeparator) return { value: '', caret: 0 }\n\n const intPart = stripLeadingZeros(intDigits || '0')\n const groupedInt = opts.segmented ? groupThousands(intPart, opts.separator) : intPart\n const fracPadded = opts.decimalPlaces > 0 ? fracDigits.padEnd(opts.decimalPlaces, '0') : ''\n const numberStr = groupedInt + (opts.decimalPlaces > 0 ? opts.decimalSeparator + fracPadded : '')\n const signStr = isNegative ? '-' : ''\n const output = signStr + opts.prefix + numberStr + opts.suffix\n\n const clampedCaret = Math.max(0, Math.min(inputCaret, value.length))\n const { inFraction, digitsBefore } = locateCaretSegment(value, clampedCaret, opts)\n const prefixLen = signStr.length + opts.prefix.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore)\n\n return { value: output, caret }\n}\n\n/** Apply a decimal mask to a raw value and return just the masked string. */\nexport function processDecimal(value: string, options?: DecimalMaskOptions): string {\n return applyDecimalMask(value, value.length, options).value\n}\n\n/**\n * Parse a raw or already-masked decimal value back into a JS number.\n * Ignores prefix/suffix/thousands separator; returns `0` for an empty or\n * digit-less value.\n */\nexport function unmaskDecimal(value: string, options?: DecimalMaskOptions): number {\n const opts = resolveDecimalOptions(options)\n const { isNegative, intDigits, fracDigits } = computeDecimalParts(value, opts)\n const n = Number(fracDigits ? `${intDigits || '0'}.${fracDigits}` : intDigits || '0')\n return isNegative ? -n : n\n}\n\n/**\n * After a Backspace removes the decimal separator itself, the integer and\n * fraction digit runs collapse into one continuous stream (e.g. \"25.00\"\n * with the caret right after \".\" → Backspace deletes the \".\" → \"2500\").\n * Left alone, that reads as one big integer (\"$2,500.00\"). This restores\n * the segment boundary instead: the trailing `decimalPlaces` digits are\n * still the fraction, and the digit right before them — the one that used\n * to sit at the end of the integer part — is the one Backspace actually\n * removed, so it's dropped (not kept) — \"$25.00\" → \"$2.00\".\n *\n * Every reformat re-appends `decimalSeparator` whenever `decimalPlaces > 0`,\n * so its absence from `value` is an unambiguous signal that this exact\n * keystroke just deleted it — no \"value before this keystroke\" snapshot is\n * needed. Returns `null` when there's nothing to restore (the separator is\n * still present, `decimalPlaces` is `0`, or too few digits remain), so the\n * caller falls through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskUnmergingSeparator(\n value: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n if (opts.decimalPlaces <= 0) return null\n\n const { isNegative, intDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (hasSeparator || intDigits.length < opts.decimalPlaces + 1) return null\n\n const preMergeIntLength = intDigits.length - opts.decimalPlaces\n const fracDigits = intDigits.slice(preMergeIntLength)\n const remainingInt = intDigits.slice(0, preMergeIntLength - 1)\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + remainingInt + opts.decimalSeparator + fracDigits\n return applyDecimalMask(raw, signPart.length + remainingInt.length, opts)\n}\n\n/**\n * Special-cases typing a single digit into a field whose integer part is\n * exactly the auto-inserted \"0\" placeholder: the new digit replaces that\n * zero instead of combining with it — e.g. \"$0.00\" with the caret anywhere\n * against that lone \"0\" and typing \"2\" gives \"$2.00\", not \"$20.00\"/\"$02.00\".\n * Any already-typed fraction is preserved.\n *\n * `value`/`caret` must be the state *after* the browser has already\n * inserted `digit` at `caret - 1` (the same post-insertion snapshot\n * `applyDecimalMask` itself expects from `bindDecimal`). Returns `null`\n * when the pattern doesn't apply — either the integer part has real digits\n * already, or the caret wasn't against that lone zero — so the caller falls\n * through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskReplacingLoneZero(\n value: string,\n caret: number,\n digit: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n const insertIdx = caret - 1\n if (insertIdx < 0 || value[insertIdx] !== digit) return null\n\n const withoutDigit = value.slice(0, insertIdx) + value.slice(caret)\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(withoutDigit, opts)\n if (intDigits !== '0') return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + 1) return null\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + digit + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + 1, opts)\n}\n\n/** Format a plain JS number into its masked display string. */\nexport function formatDecimalValue(value: number, options?: DecimalMaskOptions): string {\n const opts = resolveDecimalOptions(options)\n if (!Number.isFinite(value)) return ''\n\n const isNegative = opts.allowNegative && value < 0\n const fixed = Math.abs(value).toFixed(opts.decimalPlaces)\n const dotIdx = fixed.indexOf('.')\n const intRaw = dotIdx === -1 ? fixed : fixed.slice(0, dotIdx)\n const fracPart = dotIdx === -1 ? '' : fixed.slice(dotIdx + 1)\n const intPart = stripLeadingZeros(intRaw || '0')\n\n const groupedInt = opts.segmented ? groupThousands(intPart, opts.separator) : intPart\n const numberStr = groupedInt + (opts.decimalPlaces > 0 ? opts.decimalSeparator + fracPart : '')\n\n return (isNegative ? '-' : '') + opts.prefix + numberStr + opts.suffix\n}\n","import {\n applyDecimalMask,\n applyDecimalMaskReplacingLoneZero,\n applyDecimalMaskUnmergingSeparator,\n resolveDecimalOptions,\n unmaskDecimal,\n} from './decimal-mask'\nimport { isIos } from './platform'\nimport type { BindDecimalOptions, DecimalMaskOptions, MaskResult } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindDecimalOptions(\n second:\n | BindDecimalOptions\n | ((value: string, numericValue: number) => void)\n | null\n | undefined,\n): BindDecimalOptions {\n if (second == null) return {}\n if (typeof second === 'function') return { onChange: second }\n return second\n}\n\nfunction isDigitKey(key: string): boolean {\n return key.length === 1 && key >= '0' && key <= '9'\n}\n\n/**\n * Bind a decimal/currency mask to an input element.\n *\n * Same contract as {@link bind}: idempotent (marked with `data-masked`),\n * returns a dispose function, and reformats on paste and keyboard-driven\n * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no\n * fixed pattern — the integer part grows and shrinks freely; formatting is\n * driven entirely by `options`.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.\n */\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n options?: BindDecimalOptions | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n onChange: ((value: string, numericValue: number) => void) | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n second?: BindDecimalOptions | ((value: string, numericValue: number) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, ...maskOptions } = toBindDecimalOptions(second)\n const decimalOptions: DecimalMaskOptions = maskOptions\n const { decimalSeparator, decimalPlaces } = resolveDecimalOptions(decimalOptions)\n\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, 'decimal')\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n\n let lockInput = false\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n target.setSelectionRange(m.caret, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n applyResult(target, applyDecimalMask(target.value, target.value.length, decimalOptions))\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n\n // Navigation (arrows, Home/End, Tab, ...), selection, and shortcut keys\n // (Ctrl/Cmd+A, Ctrl/Cmd+C, ...) don't change the text — leave the\n // browser's native caret/selection handling alone instead of recomputing\n // and overwriting it on every keystroke.\n if (!isBackspace && !isDelete && !isCharInsert) return\n\n // Everything below reads `target.value`/`selectionStart` inside the rAF\n // callback rather than synchronously here, since the browser's native\n // character insertion for this keystroke isn't guaranteed to have landed\n // yet at the point a keydown listener runs — only by the next frame.\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n\n // Backspace that just deleted the decimal separator merges the\n // integer and fraction digit runs into one continuous stream —\n // restore the boundary instead of treating that as one big integer.\n if (isBackspace) {\n const unmerged = applyDecimalMaskUnmergingSeparator(target.value, decimalOptions)\n if (unmerged) {\n applyResult(target, unmerged)\n return\n }\n }\n\n // A numeric keypad (or a locale mismatch) may only offer \".\" or \",\".\n // Normalize whichever one the user just typed to the configured\n // `decimalSeparator` so it reliably opens the fraction segment.\n if (\n decimalPlaces > 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator &&\n pos > 0 &&\n target.value[pos - 1] === ke.key\n ) {\n target.value = target.value.slice(0, pos - 1) + decimalSeparator + target.value.slice(pos)\n }\n\n // Typing a digit into a field whose integer part is still the\n // auto-inserted \"0\" placeholder replaces that zero instead of\n // combining with it (e.g. \"$0.00\" + \"2\" → \"$2.00\", not \"$20.00\").\n const replaced = isDigitKey(ke.key)\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, ke.key, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n"],"mappings":"kRAMA,SAASA,EAAY,EAAqB,CACxC,OAAO,GAAM,KAAO,GAAM,IAG5B,SAAS,EAAa,EAAqB,CACzC,OAAQ,GAAM,KAAO,GAAM,KAAS,GAAM,KAAO,GAAM,IAGzD,SAAS,EAAW,EAAqB,CACvC,OAAO,IAAO,KAAO,IAAO,KAAO,IAAO,IAG5C,SAAS,EAAY,EAAY,EAAuB,CAItD,OAHI,IAAS,IAAYA,EAAY,EAAG,CACpC,IAAS,IAAY,EAAa,EAAG,CAElCA,EAAY,EAAG,EAAI,EAAa,EAAG,CAuB5C,SAAS,EAAc,EAAe,EAAc,EAAgC,CAClF,IAAI,EAAS,GACT,EAAU,GACV,EAAW,EACX,EAAc,EACd,EAAgB,GAEpB,IAAK,IAAI,EAAU,EAAG,EAAU,EAAK,OAAQ,IAAW,CACtD,IAAM,EAAS,EAAK,GAEpB,GAAI,IAAW,KAAO,IAAW,KAAO,IAAW,IAAK,CACtD,GAAW,EACX,SAIF,IAAI,EAAQ,GACZ,KAAO,EAAW,EAAM,QAAQ,CAC9B,IAAM,EAAK,EAAM,KAEjB,GAAI,EAAY,EAAI,EAAO,CAAE,CAE3B,GAAU,EAAU,EACpB,EAAU,GACV,EAAQ,GAIH,IACC,GAAY,EACd,EAAc,EAAO,OAErB,EAAgB,IAGpB,OAKJ,GAAI,CAAC,EAAO,MAOd,OAFK,IAAe,EAAc,EAAO,QAElC,CAAE,MAAO,EAAQ,MAAO,EAAa,CAkB9C,SAAS,EAAa,EAA2B,CAC/C,IAAM,EAAsB,EAAE,CAC1B,EAAI,EACR,KAAO,EAAI,EAAK,QAAQ,CACtB,IAAM,EAAQ,EACR,EAAY,EAAW,EAAK,GAAG,CACrC,KAAO,EAAI,EAAK,QAAU,EAAW,EAAK,GAAG,GAAK,GAAW,IAC7D,IAAM,EAAO,EAAK,MAAM,EAAO,EAAE,CACjC,EAAO,KAAK,EAAY,CAAE,KAAM,QAAS,MAAO,EAAM,CAAG,CAAE,KAAM,UAAW,OAAM,CAAC,CAErF,OAAO,EAIT,SAAS,EAAiB,EAAqB,EAAsB,CACnE,IAAI,EAAW,EACf,IAAK,IAAI,EAAI,EAAM,EAAI,EAAO,OAAQ,IAAK,CACzC,IAAM,EAAQ,EAAO,GACjB,EAAM,OAAS,UAAS,GAAY,EAAM,MAAM,QAEtD,OAAO,EAIT,SAAS,EAAmB,EAAe,EAAyB,CAClE,IAAI,EAAQ,EACZ,IAAK,IAAI,EAAI,EAAS,EAAI,EAAM,OAAQ,IAAK,CAC3C,IAAM,EAAK,EAAM,IACbA,EAAY,EAAG,EAAI,EAAa,EAAG,GAAE,IAE3C,OAAO,EAuBT,SAAS,EAAmB,EAAe,EAAc,EAAgC,CACvF,IAAM,EAAS,EAAa,EAAK,CAE7B,EAAS,GACT,EAAU,GACV,EAAW,EACX,EAAc,EACd,EAAgB,GAChB,EAAY,GAEhB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,QAAU,CAAC,EAAW,IAAK,CACpD,IAAM,EAAQ,EAAO,GAErB,GAAI,EAAM,OAAS,UAAW,CAC5B,GAAW,EAAM,KACb,EAAM,WAAW,EAAM,KAAM,EAAS,GAAE,GAAY,EAAM,KAAK,QACnE,SAGF,IAAM,EAAY,EAAO,EAAI,GACvB,EAAgB,EAAiB,EAAQ,EAAI,EAAE,CAErD,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,MAAM,OAAQ,IAAK,CAC3C,IAAM,EAAS,EAAM,MAAM,GACvB,EAAQ,GAEZ,KAAO,EAAW,EAAM,QAAQ,CAC9B,IAAM,EAAK,EAAM,GAEjB,GAAI,EAAY,EAAI,EAAO,CAAE,CAC3B,IACA,GAAU,EAAU,EACpB,EAAU,GACV,EAAQ,GAEH,IACC,GAAY,EACd,EAAc,EAAO,OAErB,EAAgB,IAGpB,MAQF,GACE,GAAW,OAAS,WACpB,EAAM,WAAW,EAAU,KAAM,EAAS,EAC1C,EAAmB,EAAO,EAAS,EAAI,EAEvC,MAGF,IAGF,GAAI,CAAC,EAAO,CACN,GAAY,EAAM,SAAQ,EAAY,IAC1C,QAON,OAFK,IAAe,EAAc,EAAO,QAElC,CAAE,MAAO,EAAQ,MAAO,EAAa,CAO9C,SAAgB,EACd,EACA,EACA,EAAa,EACb,EACY,CAEZ,OADK,EACE,GAAS,YAAc,GAC1B,EAAc,EAAO,EAAM,EAAW,CACtC,EAAmB,EAAO,EAAM,EAAW,CAH5B,CAAE,MAAO,GAAI,MAAO,EAAG,CC5O5C,SAAS,EAAe,EAAuB,CAC7C,IAAI,EAAI,EACR,IAAK,IAAM,KAAM,GAEZ,GAAM,KAAO,GAAM,KACnB,GAAM,KAAO,GAAM,KACnB,GAAM,KAAO,GAAM,MAEpB,IAEJ,OAAO,EAIT,SAAS,EAAe,EAAsB,CAC5C,IAAI,EAAI,EACR,IAAK,IAAM,KAAM,GACX,IAAO,KAAO,IAAO,KAAO,IAAO,MAAK,IAE9C,OAAO,EAWT,SAAgB,EAAY,EAAe,EAA2B,CACpE,GAAI,CAAC,MAAM,QAAQ,EAAK,CAAE,OAAO,EACjC,IAAM,EAAY,EAAe,EAAM,CACnC,EAAI,EACR,KAAO,EAAI,EAAK,OAAS,GAAK,EAAY,EAAe,EAAK,GAAG,EAAE,IACnE,OAAO,EAAK,GAId,SAAgB,EAAa,EAA2B,CAItD,OAHI,MAAM,QAAQ,EAAK,CACd,EAAK,OAAS,EAAI,KAAK,IAAI,GAAG,EAAK,IAAK,GAAM,EAAE,OAAO,CAAC,CAAG,EAE7D,EAAK,OC1Cd,IAAa,EAAb,KAAkB,CAEhB,MAEA,OACA,MACA,SAEA,YAAY,EAAe,EAAc,EAAQ,EAAG,EAA4B,CAC9E,KAAK,OAAS,EACd,KAAK,MAAQ,EACb,KAAK,MAAQ,EACb,KAAK,SAAW,EAIlB,SAAkB,CAChB,IAAM,EAAS,EAAU,KAAK,OAAQ,KAAK,MAAO,KAAK,MAAO,KAAK,SAAS,CAE5E,MADA,MAAK,MAAQ,EAAO,MACb,EAAO,QAKlB,SAAgB,EACd,EACA,EACA,EAAQ,EACR,EACM,CACN,OAAO,IAAI,EAAK,EAAO,EAAY,EAAO,EAAK,CAAE,EAAO,EAAQ,CAIlE,SAAgB,EAAQ,EAAe,EAAmB,EAAoC,CAC5F,OAAO,EAAU,EAAO,EAAM,EAAG,EAAQ,CAAC,SAAS,CC9CrD,IAAI,EAGJ,SAAgB,GAAiB,CAI/B,OAHI,IAAgB,IAAA,KACpB,EACE,OAAO,UAAc,KAAe,oBAAoB,KAAK,UAAU,UAAU,EAF7C,ECAxC,IAAMC,EAAc,cAEpB,SAAS,EACP,EACa,CAGb,OAFI,GAAS,KAAa,EAAE,CACxB,OAAO,GAAU,WAAmB,CAAE,SAAU,EAAO,CACpD,EA0BT,SAAgB,EACd,EACA,EACA,EACY,CACZ,GAAI,EAAM,aAAaA,EAAY,GAAK,KAAM,UAAa,GAE3D,GAAM,CAAE,WAAU,aAAc,EAAc,EAAM,CAG9C,EAAyB,EAAE,CAC3B,GAAgB,EAAc,IAAwB,CACrD,EAAM,aAAa,EAAK,GAC3B,EAAM,aAAa,EAAM,EAAM,CAC/B,EAAa,KAAK,EAAK,GAI3B,EAAM,aAAaA,EAAa,MAAM,QAAQ,EAAK,CAAG,EAAK,KAAK,IAAI,CAAG,EAAK,CAC5E,EAAa,eAAgB,MAAM,CACnC,EAAa,cAAe,MAAM,CAClC,EAAa,iBAAkB,MAAM,CACrC,EAAa,aAAc,QAAQ,CACnC,EAAa,YAAa,OAAO,EAAa,EAAK,CAAC,CAAC,CAErD,IAAI,EAAY,GAEV,EAAe,GAAO,CAAG,QAAU,UAEnC,EAAW,GAAmB,CAClC,IAAM,EAAS,EAAE,OACjB,0BAA4B,CAE1B,EAAO,MADG,EAAU,EAAO,MAAO,EAAM,EAAG,CAAE,YAAW,CACxC,CAAC,SAAS,CAC1B,IAAW,EAAO,MAAM,EACxB,EAGE,EAAS,GAAmB,CAChC,IAAM,EAAK,EACL,EAAS,EAAG,OACZ,EAAW,EAAO,MAGxB,GAAI,CAAE,EAAwB,IAAK,CACjC,EAAY,GACZ,0BAA4B,CAC1B,IAAM,EAAM,EAAO,gBAAkB,IAC/B,EAAI,EAAU,EAAO,MAAO,EAAM,EAAK,CAAE,YAAW,CAAC,CAC3D,EAAO,MAAQ,EAAE,SAAS,CAC1B,EAAO,kBAAkB,EAAE,MAAO,EAAE,MAAM,CAC1C,0BAA4B,CAC1B,EAAY,IACZ,EACF,CACF,OAGF,GAAI,EAAG,MAAQ,OAAQ,OAEvB,IAAM,EAAc,EAAG,MAAQ,YACzB,EAAW,EAAG,MAAQ,SACtB,EAAe,EAAG,IAAI,SAAW,GAAK,CAAC,EAAG,SAAW,CAAC,EAAG,QAAU,CAAC,EAAG,QACvE,EAAiB,EAAG,MAAQ,eAGlC,GAAI,GAAgB,EAAO,iBAAmB,EAAO,cAC/C,EAAS,QAAU,EAAa,EAAK,EAAI,CAAC,GAAO,CAAE,CACrD,EAAG,gBAAgB,CACnB,OAMJ,GAAI,EAAW,CACb,EAAG,gBAAgB,CACnB,OAGF,0BAA4B,CAC1B,IAAM,EAAM,EAAO,gBAAkB,IAC/B,EAAI,EAAU,EAAO,MAAO,EAAM,EAAK,CAAE,YAAW,CAAC,CAG3D,GAFA,EAAO,MAAQ,EAAE,SAAS,CAEtB,EAAgB,CAClB,IAAM,EAAS,EAAO,MAAM,OAAS,EAAS,OAAS,EAAE,MAAQ,EACjE,EAAO,kBAAkB,EAAQ,EAAO,SAC/B,EAAU,CACnB,IAAM,EAAS,EAAS,SAAW,EAAO,MAAM,OAAS,EAAM,EAAI,EACnE,EAAO,kBAAkB,EAAQ,EAAO,MAC/B,EACT,EAAO,kBAAkB,EAAK,EAAI,CACzB,GACT,EAAO,kBAAkB,EAAE,MAAO,EAAE,MAAM,CAG5C,IAAW,EAAO,MAAM,EACxB,EAMJ,OAHA,EAAM,iBAAiB,QAAS,EAAQ,CACxC,EAAM,iBAAiB,EAAc,EAAM,KAE9B,CACX,EAAM,oBAAoB,QAAS,EAAQ,CAC3C,EAAM,oBAAoB,EAAc,EAAM,CAC9C,EAAM,gBAAgBA,EAAY,CAClC,IAAK,IAAM,KAAQ,EAAc,EAAM,gBAAgB,EAAK,EC3IhE,SAAS,EAAY,EAAqB,CACxC,OAAO,GAAM,KAAO,GAAM,IAkB5B,SAAgB,EAAsB,EAAsD,CAC1F,IAAM,EAAY,GAAS,eAAiB,EAE5C,MAAO,CACL,cAFoB,OAAO,SAAS,EAAU,CAAG,KAAK,IAAI,EAAG,KAAK,MAAM,EAAU,CAAC,CAAG,EAGtF,UAAW,GAAS,WAAa,GACjC,UAAW,GAAS,WAAa,IACjC,iBAAkB,GAAS,kBAAoB,IAC/C,OAAQ,GAAS,QAAU,GAC3B,OAAQ,GAAS,QAAU,GAC3B,cAAe,GAAS,eAAiB,GAC1C,CAQH,SAAS,EAAkB,EAAmB,CAC5C,IAAI,EAAI,EACR,KAAO,EAAI,EAAE,OAAS,GAAK,EAAE,KAAO,KAAK,IACzC,OAAO,EAAE,MAAM,EAAE,CAInB,SAAS,EAAe,EAAW,EAAqB,CACtD,GAAI,CAAC,GAAO,EAAE,QAAU,EAAG,OAAO,EAClC,IAAM,EAAkB,EAAE,CACtB,EAAI,EAAE,OACV,KAAO,EAAI,GACT,EAAM,QAAQ,EAAE,MAAM,EAAI,EAAG,EAAE,CAAC,CAChC,GAAK,EAGP,OADA,EAAM,QAAQ,EAAE,MAAM,EAAG,EAAE,CAAC,CACrB,EAAM,KAAK,EAAI,CASxB,SAAS,EAAqB,EAAW,EAA8B,CACrE,GAAI,GAAgB,EAAG,MAAO,GAC9B,IAAI,EAAQ,EACZ,IAAK,IAAI,EAAI,EAAG,EAAI,EAAE,OAAQ,IAC5B,GAAI,EAAY,EAAE,GAAG,GACnB,IACI,IAAU,GAAc,OAAO,EAAI,EAG3C,OAAO,EAAE,OAwBX,SAAS,EAAoB,EAAa,EAA4C,CACpF,IAAI,EAAY,GACZ,EAAa,GACb,EAAa,GACb,EAAa,GACX,EAAkB,EAAK,cAAgB,EAE7C,IAAK,IAAM,KAAM,EAAK,CACpB,GAAI,EAAY,EAAG,CAAE,CACf,EACE,EAAW,OAAS,EAAK,gBAAe,GAAc,GAE1D,GAAa,EAEf,SAEF,GAAI,GAAmB,CAAC,GAAc,IAAO,EAAK,iBAAkB,CAClE,EAAa,GACb,SAEE,IAAO,KAAO,EAAK,gBAAe,EAAa,IAKrD,MAAO,CAAE,aAAY,YAAW,aAAY,aAAc,EAAY,CAQxE,SAAS,EACP,EACA,EACA,EAC+C,CAC/C,IAAM,EAAkB,EAAK,cAAgB,EACzC,EAAa,GACb,EAAe,EAEnB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,IAAK,CAC9B,IAAM,EAAK,EAAI,GACf,GAAI,EAAY,EAAG,CAAE,EACf,CAAC,GAAc,EAAe,EAAK,gBAAe,IACtD,SAEE,GAAmB,CAAC,GAAc,IAAO,EAAK,mBAChD,EAAa,GACb,EAAe,GAInB,MAAO,CAAE,aAAY,eAAc,CAarC,SAAgB,EACd,EACA,EAAa,EACb,EACY,CACZ,IAAM,EAAO,EAAsB,EAAQ,CAC3C,GAAI,CAAC,EAAO,MAAO,CAAE,MAAO,GAAI,MAAO,EAAG,CAE1C,GAAM,CAAE,aAAY,YAAW,aAAY,gBAAiB,EAAoB,EAAO,EAAK,CAC5F,GAAI,IAAc,IAAM,IAAe,IAAM,CAAC,EAAc,MAAO,CAAE,MAAO,GAAI,MAAO,EAAG,CAE1F,IAAM,EAAU,EAAkB,GAAa,IAAI,CAC7C,EAAa,EAAK,UAAY,EAAe,EAAS,EAAK,UAAU,CAAG,EACxE,EAAa,EAAK,cAAgB,EAAI,EAAW,OAAO,EAAK,cAAe,IAAI,CAAG,GACnF,EAAY,GAAc,EAAK,cAAgB,EAAI,EAAK,iBAAmB,EAAa,IACxF,EAAU,EAAa,IAAM,GAC7B,EAAS,EAAU,EAAK,OAAS,EAAY,EAAK,OAGlD,CAAE,aAAY,gBAAiB,EAAmB,EADnC,KAAK,IAAI,EAAG,KAAK,IAAI,EAAY,EAAM,OAAO,CACQ,CAAE,EAAK,CAC5E,EAAY,EAAQ,OAAS,EAAK,OAAO,OAK/C,MAAO,CAAE,MAAO,EAAQ,MAJV,EACV,EAAY,EAAW,OAAS,EAAK,iBAAiB,OAAS,EAC/D,EAAY,EAAqB,EAAY,EAAa,CAE/B,CAIjC,SAAgB,EAAe,EAAe,EAAsC,CAClF,OAAO,EAAiB,EAAO,EAAM,OAAQ,EAAQ,CAAC,MAQxD,SAAgB,EAAc,EAAe,EAAsC,CAEjF,GAAM,CAAE,aAAY,YAAW,cAAe,EAAoB,EADrD,EAAsB,EAC0C,CAAC,CACxE,EAAI,OAAO,EAAa,GAAG,GAAa,IAAI,GAAG,IAAe,GAAa,IAAI,CACrF,OAAO,EAAa,CAAC,EAAI,EAoB3B,SAAgB,EACd,EACA,EACmB,CACnB,IAAM,EAAO,EAAsB,EAAQ,CAC3C,GAAI,EAAK,eAAiB,EAAG,OAAO,KAEpC,GAAM,CAAE,aAAY,YAAW,gBAAiB,EAAoB,EAAO,EAAK,CAChF,GAAI,GAAgB,EAAU,OAAS,EAAK,cAAgB,EAAG,OAAO,KAEtE,IAAM,EAAoB,EAAU,OAAS,EAAK,cAC5C,EAAa,EAAU,MAAM,EAAkB,CAC/C,EAAe,EAAU,MAAM,EAAG,EAAoB,EAAE,CAExD,EAAW,EAAa,IAAM,GAEpC,OAAO,EADK,EAAW,EAAe,EAAK,iBAAmB,EACjC,EAAS,OAAS,EAAa,OAAQ,EAAK,CAiB3E,SAAgB,EACd,EACA,EACA,EACA,EACmB,CACnB,IAAM,EAAO,EAAsB,EAAQ,CACrC,EAAY,EAAQ,EAC1B,GAAI,EAAY,GAAK,EAAM,KAAe,EAAO,OAAO,KAGxD,GAAM,CAAE,aAAY,YAAW,aAAY,gBAAiB,EADvC,EAAM,MAAM,EAAG,EAAU,CAAG,EAAM,MAAM,EAAM,CAC2B,EAAK,CACnG,GAAI,IAAc,IAAK,OAAO,KAE9B,IAAM,EAAa,KAAsB,EAAK,OAAO,OACrD,GAAI,EAAY,GAAa,EAAY,EAAY,EAAG,OAAO,KAE/D,IAAM,EAAW,EAAa,IAAM,GAEpC,OAAO,EADK,EAAW,GAAS,EAAe,EAAK,iBAAmB,EAAa,IACvD,EAAS,OAAS,EAAG,EAAK,CAIzD,SAAgB,EAAmB,EAAe,EAAsC,CACtF,IAAM,EAAO,EAAsB,EAAQ,CAC3C,GAAI,CAAC,OAAO,SAAS,EAAM,CAAE,MAAO,GAEpC,IAAM,EAAa,EAAK,eAAiB,EAAQ,EAC3C,EAAQ,KAAK,IAAI,EAAM,CAAC,QAAQ,EAAK,cAAc,CACnD,EAAS,EAAM,QAAQ,IAAI,CAC3B,EAAS,IAAW,GAAK,EAAQ,EAAM,MAAM,EAAG,EAAO,CACvD,EAAW,IAAW,GAAK,GAAK,EAAM,MAAM,EAAS,EAAE,CACvD,EAAU,EAAkB,GAAU,IAAI,CAG1C,GADa,EAAK,UAAY,EAAe,EAAS,EAAK,UAAU,CAAG,IAC9C,EAAK,cAAgB,EAAI,EAAK,iBAAmB,EAAW,IAE5F,OAAQ,EAAa,IAAM,IAAM,EAAK,OAAS,EAAY,EAAK,OCnSlE,IAAM,EAAc,cAEpB,SAAS,EACP,EAKoB,CAGpB,OAFI,GAAU,KAAa,EAAE,CACzB,OAAO,GAAW,WAAmB,CAAE,SAAU,EAAQ,CACtD,EAGT,SAAS,EAAW,EAAsB,CACxC,OAAO,EAAI,SAAW,GAAK,GAAO,KAAO,GAAO,IAuBlD,SAAgB,EACd,EACA,EACY,CACZ,GAAI,EAAM,aAAa,EAAY,GAAK,KAAM,UAAa,GAE3D,GAAM,CAAE,WAAU,GAAG,GAAgB,EAAqB,EAAO,CAC3D,EAAqC,EACrC,CAAE,mBAAkB,iBAAkB,EAAsB,EAAe,CAE3E,EAAyB,EAAE,CAC3B,GAAgB,EAAc,IAAwB,CACrD,EAAM,aAAa,EAAK,GAC3B,EAAM,aAAa,EAAM,EAAM,CAC/B,EAAa,KAAK,EAAK,GAI3B,EAAM,aAAa,EAAa,UAAU,CAC1C,EAAa,eAAgB,MAAM,CACnC,EAAa,cAAe,MAAM,CAClC,EAAa,iBAAkB,MAAM,CACrC,EAAa,aAAc,QAAQ,CAEnC,IAAI,EAAY,GACV,EAAe,GAAO,CAAG,QAAU,UAEnC,GAAe,EAA0B,IAAwB,CACrE,EAAO,MAAQ,EAAE,MACjB,EAAO,kBAAkB,EAAE,MAAO,EAAE,MAAM,CAC1C,IAAW,EAAE,MAAO,EAAc,EAAE,MAAO,EAAe,CAAC,EAGvD,EAAW,GAAmB,CAClC,IAAM,EAAS,EAAE,OACjB,0BAA4B,CAC1B,EAAY,EAAQ,EAAiB,EAAO,MAAO,EAAO,MAAM,OAAQ,EAAe,CAAC,EACxF,EAGE,EAAS,GAAmB,CAChC,IAAM,EAAK,EACL,EAAS,EAAG,OAGlB,GAAI,CAAE,EAAwB,IAAK,CACjC,EAAY,GACZ,0BAA4B,CAC1B,IAAM,EAAM,EAAO,gBAAkB,EAAO,MAAM,OAClD,EAAY,EAAQ,EAAiB,EAAO,MAAO,EAAK,EAAe,CAAC,CACxE,0BAA4B,CAC1B,EAAY,IACZ,EACF,CACF,OAGF,GAAI,EAAG,MAAQ,OAAQ,OAIvB,GAAI,EAAW,CACb,EAAG,gBAAgB,CACnB,OAGF,IAAM,EAAc,EAAG,MAAQ,YACzB,EAAW,EAAG,MAAQ,SACtB,EAAe,EAAG,IAAI,SAAW,GAAK,CAAC,EAAG,SAAW,CAAC,EAAG,QAAU,CAAC,EAAG,QAMzE,CAAC,GAAe,CAAC,GAAY,CAAC,GAMlC,0BAA4B,CAC1B,IAAM,EAAM,EAAO,gBAAkB,EAAO,MAAM,OAKlD,GAAI,EAAa,CACf,IAAM,EAAW,EAAmC,EAAO,MAAO,EAAe,CACjF,GAAI,EAAU,CACZ,EAAY,EAAQ,EAAS,CAC7B,QAQF,EAAgB,IACf,EAAG,MAAQ,KAAO,EAAG,MAAQ,MAC9B,EAAG,MAAQ,GACX,EAAM,GACN,EAAO,MAAM,EAAM,KAAO,EAAG,MAE7B,EAAO,MAAQ,EAAO,MAAM,MAAM,EAAG,EAAM,EAAE,CAAG,EAAmB,EAAO,MAAM,MAAM,EAAI,EAU5F,EAAY,GAJK,EAAW,EAAG,IAAI,CAC/B,EAAkC,EAAO,MAAO,EAAK,EAAG,IAAK,EAAe,CAC5E,OAE4B,EAAiB,EAAO,MAAO,EAAK,EAAe,CAAC,EACpF,EAMJ,OAHA,EAAM,iBAAiB,QAAS,EAAQ,CACxC,EAAM,iBAAiB,EAAc,EAAM,KAE9B,CACX,EAAM,oBAAoB,QAAS,EAAQ,CAC3C,EAAM,oBAAoB,EAAc,EAAM,CAC9C,EAAM,gBAAgB,EAAY,CAClC,IAAK,IAAM,KAAQ,EAAc,EAAM,gBAAgB,EAAK"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mother-mask",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Lightweight input mask library for browsers",
|
|
7
7
|
"author": "Danilo Celestino de Castro <dan2dev>",
|
|
@@ -34,18 +34,18 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@eslint/js": "^10.0.1",
|
|
36
36
|
"@eslint/json": "^1.2.0",
|
|
37
|
-
"@eslint/markdown": "^8.0.
|
|
38
|
-
"@types/node": "^25.5
|
|
37
|
+
"@eslint/markdown": "^8.0.3",
|
|
38
|
+
"@types/node": "^25.9.5",
|
|
39
39
|
"@typescript/native-preview": "7.0.0-dev.20260405.1",
|
|
40
|
-
"@vitest/coverage-v8": "^4.1.
|
|
41
|
-
"bumpp": "^11.0
|
|
42
|
-
"eslint": "^10.
|
|
43
|
-
"globals": "^17.
|
|
44
|
-
"jsdom": "^29.
|
|
45
|
-
"tsdown": "^0.21.
|
|
46
|
-
"typescript": "^6.0.
|
|
47
|
-
"typescript-eslint": "^8.
|
|
48
|
-
"vitest": "^4.1.
|
|
40
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
41
|
+
"bumpp": "^11.1.0",
|
|
42
|
+
"eslint": "^10.7.0",
|
|
43
|
+
"globals": "^17.7.0",
|
|
44
|
+
"jsdom": "^29.1.1",
|
|
45
|
+
"tsdown": "^0.21.10",
|
|
46
|
+
"typescript": "^6.0.3",
|
|
47
|
+
"typescript-eslint": "^8.65.0",
|
|
48
|
+
"vitest": "^4.1.10"
|
|
49
49
|
},
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|