mother-mask 2.0.5 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,122 +1,148 @@
1
1
  # mother-mask
2
2
 
3
- Lightweight input mask library for browsers. Zero runtime dependencies, TypeScript-first, ships **ESM**, **CJS**, and **UMD**.
3
+ Lightweight input masks for browser forms. Zero runtime dependencies, written in TypeScript, and published with ESM, CJS, and UMD builds.
4
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)**
5
+ [npm](https://www.npmjs.com/package/mother-mask) | [Live demo](https://stackblitz.com/edit/mother-mask-simple-demo?file=src%2Fmain.ts)
10
6
 
11
7
  ## Install
12
8
 
13
9
  ```bash
14
10
  npm install mother-mask
15
- # or
16
- pnpm add mother-mask
17
11
  ```
18
12
 
19
- ## Usage
20
-
21
- ### `bind(input, mask, options?)`
22
-
23
- Attach a mask to any input element — this is the main API.
13
+ ```bash
14
+ pnpm add mother-mask
15
+ ```
24
16
 
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.
17
+ ## Basic Usage
28
18
 
29
19
  ```ts
30
20
  import { bind } from 'mother-mask'
31
21
 
32
- const input = document.getElementById('phone') as HTMLInputElement
22
+ const input = document.querySelector<HTMLInputElement>('#phone')!
33
23
 
34
- // Fixed mask
35
24
  const dispose = bind(input, '(99) 99999-9999')
36
25
 
37
- // Dynamic mask picks the pattern from an ordered list (shortest → longest)
26
+ // Later, remove listeners and allow rebinding.
27
+ dispose()
28
+ ```
29
+
30
+ Use an ordered mask array for values with more than one length:
31
+
32
+ ```ts
38
33
  bind(input, ['(99) 9999-9999', '(99) 99999-9999'])
34
+ ```
39
35
 
40
- // Callback after paste or keyboard-driven changes
36
+ Listen for changes with either a callback or an options object:
37
+
38
+ ```ts
41
39
  bind(input, '999.999.999-99', (value) => {
42
- console.log(value) // e.g. "123.456.789-01"
40
+ document.querySelector<HTMLInputElement>('#cpf-value')!.value = value
43
41
  })
44
42
 
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()
43
+ bind(input, '999.999.999-99', {
44
+ onChange: (value) => {
45
+ document.querySelector<HTMLInputElement>('#cpf-value')!.value = value
46
+ },
47
+ })
50
48
  ```
51
49
 
52
- ## Pattern syntax
50
+ ## Decimal Inputs
53
51
 
54
- | Character | Matches |
55
- |-----------|---------|
56
- | `9` | Digit (`0`–`9`) |
57
- | `Z` | Letter (`a`–`z`, `A`–`Z`) |
58
- | `A` | Alphanumeric (digit or letter) |
59
- | Anything else | Literal — inserted as the user fills slots |
52
+ Use `bindDecimal` for numbers, currency fields, and values where the integer part should grow freely.
60
53
 
61
- ## Array masks
54
+ ```ts
55
+ import { bindDecimal } from 'mother-mask'
56
+
57
+ bindDecimal(input, {
58
+ decimalPlaces: 2,
59
+ separator: ',',
60
+ decimalSeparator: '.',
61
+ prefix: '$',
62
+ allowNegative: false,
63
+ onChange: (value, numericValue) => {
64
+ document.querySelector<HTMLInputElement>('#amount-label')!.value = value
65
+ document.querySelector<HTMLInputElement>('#amount-value')!.value = String(numericValue)
66
+ },
67
+ })
68
+ ```
62
69
 
63
- 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.
70
+ For Brazilian-style formatting:
64
71
 
65
72
  ```ts
66
- bind(input, ['(99) 9999-9999', '(99) 99999-9999'])
67
- bind(input, ['999.999.999-99', 'AA.AAA.AAA/AAAA-99'])
73
+ bindDecimal(input, {
74
+ separator: '.',
75
+ decimalSeparator: ',',
76
+ })
68
77
  ```
69
78
 
70
- ## UMD / CDN
79
+ ## Pattern Syntax
71
80
 
72
- ```html
73
- <script src="https://unpkg.com/mother-mask/dist/mother-mask.umd.js"></script>
74
- <script>
75
- const dispose = MotherMask.bind(document.getElementById('cpf'), '999.999.999-99')
76
- </script>
81
+ | Character | Matches |
82
+ | --- | --- |
83
+ | `9` | Digit |
84
+ | `Z` | Letter |
85
+ | `A` | Letter or digit |
86
+ | Anything else | Literal separator |
87
+
88
+ Examples:
89
+
90
+ ```ts
91
+ bind(input, '999.999.999-99')
92
+ bind(input, '99/99/9999')
93
+ bind(input, 'AA.AAA.AAA/AAAA-99')
77
94
  ```
78
95
 
79
- The global name is **`MotherMask`**.
96
+ ## Segmented Editing
80
97
 
81
- ## API reference
98
+ Masks are segmented by default. Separators behave like boundaries, which keeps fields such as dates from bleeding into each other while editing:
82
99
 
83
- ### `bind` (primary)
100
+ ```ts
101
+ bind(input, '99/99/9999')
102
+ ```
84
103
 
85
- | | |
86
- |--|--|
87
- | **Signature** | `bind(input, mask, options?)` |
88
- | **Returns** | `() => void` — call to remove listeners and attributes so the input can be bound again |
89
- | **Third argument** | `{ onChange?: (value: string) => void }`, or a legacy `(value) => void` callback |
104
+ For classic reflow behavior, pass `segmented: false`:
90
105
 
91
- ### Other exports
106
+ ```ts
107
+ bind(input, '999.999.999-99', { segmented: false })
108
+ ```
92
109
 
93
- | Export | Description |
94
- |--------|-------------|
95
- | `buildMask(value, mask, caret?)` | Build a `Mask` instance (array `mask` is resolved to one string first). |
96
- | `getMaxLength(mask)` | Maximum string length for the mask (for array masks, the longest pattern). |
97
- | `applyMask(value, mask, inputCaret?)` | Low-level: apply a **single** mask string; returns `{ value, caret }`. |
110
+ ## CDN
98
111
 
99
- ### `Mask` class
112
+ ```html
113
+ <script src="https://unpkg.com/mother-mask/dist/mother-mask.umd.js"></script>
114
+ <script>
115
+ MotherMask.bind(document.getElementById('cpf'), '999.999.999-99')
116
+ </script>
117
+ ```
100
118
 
101
- `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).
119
+ The global name is `MotherMask`.
102
120
 
103
- ### Types
121
+ ## API
104
122
 
105
- ```ts
106
- type MaskPattern = string | string[]
123
+ Main exports:
107
124
 
108
- interface MaskResult {
109
- readonly value: string
110
- readonly caret: number
111
- }
125
+ - `bind(input, mask, options?)`
126
+ - `bindDecimal(input, options?)`
127
+ - `applyMask(value, mask, inputCaret?, options?)`
128
+ - `process(value, mask, options?)`
129
+ - `buildMask(value, mask, caret?, options?)`
130
+ - `getMaxLength(mask)`
131
+ - `applyDecimalMask(value, inputCaret?, options?)`
132
+ - `processDecimal(value, options?)`
133
+ - `unmaskDecimal(value, options?)`
134
+ - `formatDecimalValue(value, options?)`
135
+ - `Mask`
112
136
 
113
- interface BindOptions {
114
- onChange?: (value: string) => void
115
- }
116
- ```
137
+ Exported types:
117
138
 
118
- `MaskPattern`, `MaskResult`, and `BindOptions` are exported as types.
139
+ - `MaskPattern`
140
+ - `MaskResult`
141
+ - `ApplyMaskOptions`
142
+ - `BindOptions`
143
+ - `DecimalMaskOptions`
144
+ - `BindDecimalOptions`
119
145
 
120
146
  ## License
121
147
 
122
- MIT [Danilo Celestino de Castro](https://github.com/dan2dev)
148
+ MIT - [Danilo Celestino de Castro](https://github.com/dan2dev)
@@ -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 r(e,t,r=0){if(!e)return{value:``,caret:0};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(n(t,u)){i+=a+t,a=``,d=!0,c||(o<=r?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}function i(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 a(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function o(e,t){if(!Array.isArray(t))return t;let n=i(e),r=0;for(;r<t.length-1&&n>a(t[r]);)r++;return t[r]}function s(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var c=class{caret;_value;_mask;constructor(e,t,n=0){this._value=e,this._mask=t,this.caret=n}process(){let e=r(this._value,this._mask,this.caret);return this.caret=e.caret,e.value}};function l(e,t,n=0){return new c(e,o(e,t),n)}function u(e,t){return l(e,t).process()}let d;function f(){return d===void 0&&(d=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),d}const p=`data-masked`;function m(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function h(e,t,n){if(e.getAttribute(p)!==null)return()=>{};let{onChange:r}=m(n),i=[],a=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),i.push(t))};e.setAttribute(p,Array.isArray(t)?t.join(`|`):t),a(`autocomplete`,`off`),a(`autocorrect`,`off`),a(`autocapitalize`,`off`),a(`spellcheck`,`false`),a(`maxlength`,String(s(t)));let o=!1,c=f()?`keyup`:`keydown`,u=e=>{let n=e.target;requestAnimationFrame(()=>{n.value=l(n.value,t).process(),r?.(n.value)})},d=e=>{let n=e,i=n.target,a=i.value;if(!n.key){o=!0,requestAnimationFrame(()=>{let e=i.selectionStart??999,n=l(i.value,t,e);i.value=n.process(),i.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{o=!1})});return}if(n.key===`Meta`)return;let c=n.key===`Backspace`,u=n.key===`Delete`,d=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,p=n.key===`Unidentified`;if(d&&i.selectionStart===i.selectionEnd&&a.length>=s(t)&&!f()){n.preventDefault();return}if(o){n.preventDefault();return}requestAnimationFrame(()=>{let e=i.selectionStart??999,n=l(i.value,t,e);if(i.value=n.process(),p){let t=i.value.length>a.length?n.caret:e;i.setSelectionRange(t,t)}else if(u){let t=a.length===i.value.length?e+1:e;i.setSelectionRange(t,t)}else c?i.setSelectionRange(e,e):d&&i.setSelectionRange(n.caret,n.caret);r?.(i.value)})};return e.addEventListener(`paste`,u),e.addEventListener(c,d),()=>{e.removeEventListener(`paste`,u),e.removeEventListener(c,d),e.removeAttribute(p);for(let t of i)e.removeAttribute(t)}}exports.Mask=c,exports.applyMask=r,exports.bind=h,exports.buildMask=l,exports.getMaxLength=s,exports.process=u;
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}}const a=new Map;function o(e){let t=a.get(e);if(t)return t;let r=[],i=0;for(;i<e.length;){let t=i,a=n(e[i]);for(;i<e.length&&n(e[i])===a;)i++;let o=e.slice(t,i);r.push(a?{kind:`slots`,chars:o}:{kind:`literal`,text:o})}return a.set(e,r),r}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(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 l(e,t,n){let i=o(t),a=``,l=``,u=0,d=0,f=!1,p=!1;for(let t=0;t<i.length&&!p;t++){let o=i[t];if(o.kind===`literal`){l+=o.text,e.startsWith(o.text,u)&&(u+=o.text.length);continue}let m=i[t+1],h=s(i,t+1);for(let t=0;t<o.chars.length;t++){let i=o.chars[t],s=!1;for(;u<e.length;){let t=e[u];if(r(t,i)){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?i(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}const 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))},s=m(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(s));let c=!1,l=y()?`keyup`:`keydown`,u=new Set,d=e=>{let t=requestAnimationFrame(()=>{u.delete(t),e()});u.add(t)},f=e=>{let n=e.target;d(()=>{n.value=g(n.value,t,0,{segmented:i}).process(),r?.(n.value)})},p=e=>{let n=e,a=n.target,o=a.value;if(!n.key){c=!0,d(()=>{let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});a.value=n.process(),a.setSelectionRange(n.caret,n.caret),d(()=>{c=!1})});return}if(n.key===`Meta`)return;let l=n.key===`Backspace`,u=n.key===`Delete`,f=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,p=n.key===`Unidentified`;if(f&&a.selectionStart===a.selectionEnd&&o.length>=s&&!y()){n.preventDefault();return}if(c){n.preventDefault();return}d(()=>{let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});if(a.value=n.process(),p){let t=a.value.length>o.length?n.caret:e;a.setSelectionRange(t,t)}else if(u){let t=o.length===a.value.length?e+1:e;a.setSelectionRange(t,t)}else l?a.setSelectionRange(e,e):f&&a.setSelectionRange(n.caret,n.caret);r?.(a.value)})};return e.addEventListener(`paste`,f),e.addEventListener(l,p),()=>{e.removeEventListener(`paste`,f),e.removeEventListener(l,p),e.removeAttribute(b);for(let t of a)e.removeAttribute(t);for(let e of u)cancelAnimationFrame(e);u.clear()}}function C(e){return e>=`0`&&e<=`9`}function w(e){let t=e?.decimalPlaces,n=t!=null&&Number.isFinite(t)?Math.max(0,Math.floor(t)):void 0,r=e?.numberPlaces;return{decimalPlaces:n,numberPlaces:r!=null&&Number.isFinite(r)?Math.max(1,Math.floor(r)):void 0,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?(t.decimalPlaces==null||r.length<t.decimalPlaces)&&(r+=s):(t.numberPlaces==null||n.length<t.numberPlaces)&&(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?(n.decimalPlaces==null||a<n.decimalPlaces)&&a++:(n.numberPlaces==null||a<n.numberPlaces)&&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.numberPlaces==null?c:c.padStart(r.numberPlaces,`0`),u=r.segmented?E(l,r.separator):l,d=r.decimalPlaces!=null&&r.decimalPlaces>0?o.padEnd(r.decimalPlaces,`0`):o,f=u+(r.decimalPlaces!==0&&(r.decimalPlaces!=null||s)?r.decimalSeparator+d:``),p=i?`-`:``,m=p+r.prefix+f+r.suffix,{inFraction:h,digitsBefore:g}=k(e,Math.max(0,Math.min(t,e.length)),r),_=p.length+r.prefix.length,v=l.length-c.length;return{value:m,caret:h?_+u.length+r.decimalSeparator.length+g:_+D(u,g+v)}}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==null||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),u=T(s||`0`),d=u!==`0`,f=d&&i.numberPlaces!=null&&u.length<i.numberPlaces;if(d&&!f)return null;let p=+!!o+i.prefix.length;if(a<p||a>p+s.length)return null;let m=o?`-`:``,h=(d?u:``)+n;return A(m+h+(l?i.decimalSeparator+c:``),m.length+h.length,i)}function F(e,t){let n=w(t);if(!Number.isFinite(e))return``;let r=n.allowNegative&&e<0,i=Math.abs(e),a=n.decimalPlaces==null?String(i):i.toFixed(n.decimalPlaces),o=a.indexOf(`.`),s=o===-1?a:a.slice(0,o),c=o===-1?``:a.slice(o+1),l=T(s||`0`),u=n.numberPlaces==null?l:l.padStart(n.numberPlaces,`0`),d=(n.segmented?E(u,n.separator):u)+(n.decimalPlaces!==0&&c!==``?n.decimalSeparator+c:``);return(r?`-`:``)+n.prefix+d+n.suffix}const 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=new Set,f=e=>{let t=requestAnimationFrame(()=>{d.delete(t),e()});d.add(t)},p=(e,t)=>{e.value=t.value,e.setSelectionRange(t.caret,t.caret),n?.(t.value,M(t.value,i))},m=e=>{let t=e.target;f(()=>{p(t,A(t.value,t.value.length,i))})},h=e=>{let t=e,n=t.target;if(!t.key){l=!0,f(()=>{let e=n.selectionStart??n.value.length;p(n,A(n.value,e,i)),f(()=>{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||f(()=>{let e=n.selectionStart??n.value.length;if(r){let e=N(n.value,i);if(e){p(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)),p(n,(R(t.key)?P(n.value,e,t.key,i):null)??A(n.value,e,i))})};return e.addEventListener(`paste`,m),e.addEventListener(u,h),()=>{e.removeEventListener(`paste`,m),e.removeEventListener(u,h),e.removeAttribute(I);for(let t of s)e.removeAttribute(t);for(let e of d)cancelAnimationFrame(e);d.clear()}}exports.Mask=h,exports.applyDecimalMask=A,exports.applyMask=u,exports.bind=S,exports.bindDecimal=z,exports.buildMask=g,exports.formatDecimalValue=F,exports.getMaxLength=m,exports.process=_,exports.processDecimal=j,exports.unmaskDecimal=M;
2
2
  //# sourceMappingURL=mother-mask.cjs.map
@@ -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// A bound input re-applies the same (static) mask string on every keystroke,\n// so tokenizing it is pure, repeated work — cache by mask string instead of\n// re-walking and re-allocating tokens on every keystroke.\nconst tokenCache = new Map<string, MaskToken[]>()\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 cached = tokenCache.get(mask)\n if (cached) return cached\n\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 tokenCache.set(mask, tokens)\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 // Computed once here rather than inside `onKey` — the mask never changes\n // for the lifetime of this binding, and `onKey` is a synchronous, hot,\n // input-blocking path run on every keystroke.\n const maxLength = getMaxLength(mask)\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(maxLength))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\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 scheduleFrame(() => {\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 scheduleFrame(() => {\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 >= maxLength && !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 scheduleFrame(() => {\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 for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\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 /**\n * `undefined` means an optional, uncapped fraction (default) — the\n * decimal separator and fraction only appear once the user actually types\n * them, and there's no limit on how many digits follow. `0` means no\n * fraction at all. A positive number is a fixed, zero-padded width that's\n * always shown, even before the user types anything.\n */\n decimalPlaces: number | undefined\n /** `undefined` means unlimited (default) — the integer part grows freely. */\n numberPlaces: number | undefined\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\n const decimalPlaces =\n rawPlaces != null && Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : undefined\n const rawNumberPlaces = options?.numberPlaces\n const numberPlaces =\n rawNumberPlaces != null && Number.isFinite(rawNumberPlaces)\n ? Math.max(1, Math.floor(rawNumberPlaces))\n : undefined\n return {\n decimalPlaces,\n numberPlaces,\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. The fraction is only fixed-width when `decimalPlaces` is set to a\n// positive number — zero-padded on the right so a shorter fraction reads as\n// its low-order (trailing) digits being zero rather than reflowing/shifting\n// (e.g. editing \"423,42\" down to \"423,4\" produces \"423,40\", not \"42,34\").\n// When `decimalPlaces` is left unset, the fraction is optional and uncapped:\n// it only exists once the user types the separator, and grows to however\n// many digits they type.\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 (opts.decimalPlaces == null || fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else if (opts.numberPlaces == null || intDigits.length < opts.numberPlaces) {\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) {\n if (opts.decimalPlaces == null || digitsBefore < opts.decimalPlaces) digitsBefore++\n } else if (opts.numberPlaces == null || digitsBefore < opts.numberPlaces) {\n digitsBefore++\n }\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. With a fixed `decimalPlaces` it's always displayed zero-padded to\n * that width, even before the user types it; left unset, the fraction is\n * optional — it only appears once the separator is typed, and is shown\n * exactly as typed (no padding, no cap on how many digits).\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 paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const fracPadded =\n opts.decimalPlaces != null && opts.decimalPlaces > 0\n ? fracDigits.padEnd(opts.decimalPlaces, '0')\n : fracDigits\n const showFraction = opts.decimalPlaces === 0 ? false : opts.decimalPlaces != null || hasSeparator\n const numberStr = groupedInt + (showFraction ? 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 // Left-padding zeros are synthetic — prepended ahead of every real typed\n // digit — so they shift where the caret's `digitsBefore`-th real digit\n // lands in `groupedInt` by the padding's width.\n const padLength = paddedInt.length - intPart.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore + padLength)\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 * Only applies when `decimalPlaces` is a fixed positive number — that's\n * what \"the trailing N digits are the fraction\" means. Every reformat\n * re-appends `decimalSeparator` in that case, so its absence from `value`\n * is an unambiguous signal that this exact keystroke just deleted it — no\n * \"value before this keystroke\" snapshot is needed. With `decimalPlaces`\n * unset (optional, uncapped fraction) there's no fixed width to reconstruct\n * from, so the merged digits are left as one continuous integer instead —\n * the same reasoning `numberPlaces` uses for an unbounded integer part.\n * Returns `null` when there's nothing to restore (the separator is still\n * present, `decimalPlaces` is `0` or unset, or too few digits remain), so\n * the 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 == null || 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 an integer segment that isn't\n * yet full of *real* digits — either because it's still the auto-inserted\n * zero placeholder (\"0\", or a wider \"00\" from a `numberPlaces`-padded field\n * that hasn't been touched), or because `numberPlaces` is left-padding a\n * partially-typed segment with synthetic zeros (e.g. \"02\" is really just\n * the one real digit \"2\", padded out to width 2 for display). Those padding\n * zeros aren't editable content, so the new digit extends the real digit\n * stream instead of combining with a padding zero at the caret:\n *\n * - \"$0.00\" + \"2\" → \"$2.00\" (not \"$20.00\")\n * - a `numberPlaces: 2` time field's untouched \"00:00\" + \"5\" → \"05:00\"\n * - that same field's \"02:00\" (one real digit, one padding zero) + \"4\" →\n * \"24:00\" — the real \"2\" is kept, the padding \"0\" is not\n *\n * A segment that's already full of real digits — e.g. \"23:00\", both digits\n * genuinely typed — doesn't match here and falls through to the default\n * {@link applyDecimalMask}, which already drops the overflow keystroke\n * (typing a 3rd real digit leaves \"23:00\" unchanged).\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, so the caller falls through to a plain\n * {@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\n const realDigits = stripLeadingZeros(intDigits || '0')\n const hasRealDigits = realDigits !== '0'\n const hasPaddingRoom =\n hasRealDigits && opts.numberPlaces != null && realDigits.length < opts.numberPlaces\n if (hasRealDigits && !hasPaddingRoom) return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + intDigits.length) return null\n\n const signPart = isNegative ? '-' : ''\n const newIntDigits = (hasRealDigits ? realDigits : '') + digit\n const raw = signPart + newIntDigits + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + newIntDigits.length, opts)\n}\n\n/**\n * Format a plain JS number into its masked display string. With a fixed\n * `decimalPlaces` the fraction is rounded/padded to that exact width, even\n * for a whole number; left unset, the fraction is only shown when the value\n * actually has one, with as many digits as `value` naturally carries (no\n * padding, no rounding).\n */\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 abs = Math.abs(value)\n const fixed = opts.decimalPlaces != null ? abs.toFixed(opts.decimalPlaces) : String(abs)\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 const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const showFraction = opts.decimalPlaces === 0 ? false : fracPart !== ''\n const numberStr = groupedInt + (showFraction ? 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 // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\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 scheduleFrame(() => {\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 scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n scheduleFrame(() => {\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 scheduleFrame(() => {\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 isn't yet full of\n // real digits — the auto-inserted \"0\" placeholder, or a\n // `numberPlaces`-padded segment with fewer real digits than its width\n // — extends the real digit stream instead of combining with a\n // padding zero (e.g. \"$0.00\" + \"2\" → \"$2.00\", \"02:00\" + \"4\" →\n // \"24:00\"). A segment already full of real digits (e.g. \"23:00\")\n // falls through to the default mask, which drops the keystroke.\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 for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n"],"mappings":"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"}
@@ -15,25 +15,68 @@ 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
+ /**
44
+ * Number of fixed fractional digits, zero-padded and always shown once
45
+ * set. Negative/fractional values are floored to `0`. Left unset, the
46
+ * fraction is optional and uncapped: the decimal separator and any digits
47
+ * after it only appear once the user actually types them, and there's no
48
+ * limit on how many digits they can type. @default undefined (optional, unlimited)
49
+ */
50
+ decimalPlaces?: number;
51
+ /**
52
+ * Fixed width for the integer part, left-padded with zeros to that width.
53
+ * Digits typed beyond this width are dropped instead of shifting the
54
+ * window — the mirror image of {@link decimalPlaces} for the fraction.
55
+ * Useful for fixed-width segments like a time field (`"00:00"`, hours
56
+ * capped and padded to 2 digits). @default undefined (no limit)
57
+ */
58
+ numberPlaces?: number;
59
+ /** Group the integer part into thousands using `separator`. @default true */
60
+ segmented?: boolean;
61
+ /** Thousands grouping separator, used when `segmented` is `true`. @default ',' */
62
+ separator?: string;
63
+ /** Separator between the integer and fractional parts. @default '.' */
64
+ decimalSeparator?: string;
65
+ /** Fixed text prepended to the formatted number (after the sign, if negative). @default '' */
66
+ prefix?: string;
67
+ /** Fixed text appended to the formatted number. @default '' */
68
+ suffix?: string;
69
+ /** Allow a leading `-` to produce a negative value. @default false */
70
+ allowNegative?: boolean;
71
+ }
72
+ /** Options for {@link bindDecimal}. */
73
+ interface BindDecimalOptions extends DecimalMaskOptions {
74
+ /** Fires with the masked string and its parsed numeric value after paste or keyboard-driven changes. */
75
+ onChange?: (value: string, numericValue: number) => void;
76
+ }
23
77
  //#endregion
24
78
  //#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;
79
+ declare function applyMask(value: string, mask: string, inputCaret?: number, options?: ApplyMaskOptions): MaskResult;
37
80
  //#endregion
38
81
  //#region src/bind.d.ts
39
82
  /**
@@ -52,6 +95,50 @@ declare function applyMask(value: string, mask: string, inputCaret?: number): Ma
52
95
  declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, options?: BindOptions | null): () => void;
53
96
  declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, onChange: ((value: string) => void) | null): () => void;
54
97
  //#endregion
98
+ //#region src/bind-decimal.d.ts
99
+ /**
100
+ * Bind a decimal/currency mask to an input element.
101
+ *
102
+ * Same contract as {@link bind}: idempotent (marked with `data-masked`),
103
+ * returns a dispose function, and reformats on paste and keyboard-driven
104
+ * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no
105
+ * fixed pattern — the integer part grows and shrinks freely; formatting is
106
+ * driven entirely by `options`.
107
+ *
108
+ * @param input - Any `HTMLInputElement` or `Element` that behaves like one.
109
+ * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.
110
+ */
111
+ declare function bindDecimal(input: HTMLInputElement | Element, options?: BindDecimalOptions | null): () => void;
112
+ declare function bindDecimal(input: HTMLInputElement | Element, onChange: ((value: string, numericValue: number) => void) | null): () => void;
113
+ //#endregion
114
+ //#region src/decimal-mask.d.ts
115
+ /**
116
+ * Apply a decimal/currency mask to a value, producing the masked output and
117
+ * a computed caret position. Digits typed before the decimal separator
118
+ * extend the integer part; the fraction only starts once the separator is
119
+ * typed. With a fixed `decimalPlaces` it's always displayed zero-padded to
120
+ * that width, even before the user types it; left unset, the fraction is
121
+ * optional — it only appears once the separator is typed, and is shown
122
+ * exactly as typed (no padding, no cap on how many digits).
123
+ */
124
+ declare function applyDecimalMask(value: string, inputCaret?: number, options?: DecimalMaskOptions): MaskResult;
125
+ /** Apply a decimal mask to a raw value and return just the masked string. */
126
+ declare function processDecimal(value: string, options?: DecimalMaskOptions): string;
127
+ /**
128
+ * Parse a raw or already-masked decimal value back into a JS number.
129
+ * Ignores prefix/suffix/thousands separator; returns `0` for an empty or
130
+ * digit-less value.
131
+ */
132
+ declare function unmaskDecimal(value: string, options?: DecimalMaskOptions): number;
133
+ /**
134
+ * Format a plain JS number into its masked display string. With a fixed
135
+ * `decimalPlaces` the fraction is rounded/padded to that exact width, even
136
+ * for a whole number; left unset, the fraction is only shown when the value
137
+ * actually has one, with as many digits as `value` naturally carries (no
138
+ * padding, no rounding).
139
+ */
140
+ declare function formatDecimalValue(value: number, options?: DecimalMaskOptions): string;
141
+ //#endregion
55
142
  //#region src/pattern.d.ts
56
143
  /** Maximum allowed input length for the given mask. */
57
144
  declare function getMaxLength(mask: MaskPattern): number;
@@ -63,14 +150,15 @@ declare class Mask {
63
150
  caret: number;
64
151
  private readonly _value;
65
152
  private readonly _mask;
66
- constructor(value: string, mask: string, caret?: number);
153
+ private readonly _options;
154
+ constructor(value: string, mask: string, caret?: number, options?: ApplyMaskOptions);
67
155
  /** Apply the mask to the value and return the masked string. */
68
156
  process(): string;
69
157
  }
70
158
  /** Build a `Mask` instance, resolving array patterns by value length. */
71
- declare function buildMask(value: string, mask: MaskPattern, caret?: number): Mask;
159
+ declare function buildMask(value: string, mask: MaskPattern, caret?: number, options?: ApplyMaskOptions): Mask;
72
160
  /** Apply a mask pattern to a raw value string and return the masked result. */
73
- declare function process(value: string, mask: MaskPattern): string;
161
+ declare function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string;
74
162
  //#endregion
75
- export { type BindOptions, Mask, type MaskPattern, type MaskResult, applyMask, bind, buildMask, getMaxLength, process };
163
+ 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
164
  //# sourceMappingURL=mother-mask.d.cts.map