mother-mask 3.9.0 → 3.11.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 +26 -1
- package/dist/mother-mask.cjs +1 -1
- package/dist/mother-mask.cjs.map +1 -1
- package/dist/mother-mask.d.cts +56 -2
- package/dist/mother-mask.d.mts +56 -2
- 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 +6 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Lightweight input masks for browser forms. Zero runtime dependencies, written in TypeScript, and published with ESM, CJS, and UMD builds.
|
|
4
4
|
|
|
5
|
-
[npm](https://www.npmjs.com/package/mother-mask) | [Live demo](https://
|
|
5
|
+
[npm](https://www.npmjs.com/package/mother-mask) | [Live demo](https://dan2dev.github.io/mother-mask/)
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -76,6 +76,21 @@ bindDecimal(input, {
|
|
|
76
76
|
})
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
`prefix` and `suffix` are chrome, not content. Typing with the caret parked inside them lands the character at the nearest edge of the number, so every spot that looks like the start of the number behaves like it:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
bindDecimal(input, { prefix: '$', decimalPlaces: 2 })
|
|
83
|
+
// "$0.00" — caret at the far left, type "2"
|
|
84
|
+
// → "$2|.00" same as typing just after the "$"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Their text is never read back as part of the number either, so an affix carrying a digit or the decimal separator stays out of the value:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
bindDecimal(input, { prefix: 'Q1 ', decimalPlaces: 2 })
|
|
91
|
+
// typing 1234 → "Q1 1,234.00", and unmaskDecimal() reports 1234
|
|
92
|
+
```
|
|
93
|
+
|
|
79
94
|
## Pattern Syntax
|
|
80
95
|
|
|
81
96
|
| Character | Matches |
|
|
@@ -101,6 +116,16 @@ Masks are segmented by default. Separators behave like boundaries, which keeps f
|
|
|
101
116
|
bind(input, '99/99/9999')
|
|
102
117
|
```
|
|
103
118
|
|
|
119
|
+
Separators left in the value also anchor the characters around them, so an edit that replaces whole fields leaves the rest where it was:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
bind(input, '999.999.999-99')
|
|
123
|
+
// "012.153.441-39" — select "012.153.441", type "015"
|
|
124
|
+
// → "015.|-39" the "-" keeps "39" in the last field
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Anchoring is only as precise as the separators allow. A mask whose separators are all the same character can produce a genuinely ambiguous value — with `'99/99/9999'`, `"1/2025"` reads equally well as `1 / 20 / 25` — and resolves it to the earliest field that fits. Masks with distinct separators (CPF, CNPJ, phone numbers) have no such gap.
|
|
128
|
+
|
|
104
129
|
For classic reflow behavior, pass `segmented: false`:
|
|
105
130
|
|
|
106
131
|
```ts
|
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(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){try{return e.selectionStart??e.value.length}catch{return e.value.length}}function C(e,t){try{e.setSelectionRange(t,t)}catch{}}function w(e){return e===`deleteContentBackward`?`backspace`:e===`deleteContentForward`?`delete`:e&&e.startsWith(`insert`)?`insert`:`unidentified`}function T(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=!1,u=!1,d=e.value??``,f=y()?`keyup`:`keydown`,p=new Set,h=e=>{let t=requestAnimationFrame(()=>{p.delete(t),e()});p.add(t)},_=()=>{for(let e of p)cancelAnimationFrame(e);p.clear()},v=e=>{let n=e.target;h(()=>{let e=g(n.value,t,0,{segmented:i});n.value=e.process(),r?.(n.value)})},T=e=>{let n=e,a=e.target;_(),c=!1,u=!0;let o=S(a),s=d.length,l=g(a.value,t,o,{segmented:i});a.value=l.process();let f=w(n.inputType);f===`unidentified`?C(a,a.value.length>s?l.caret:o):f===`delete`?C(a,s===a.value.length?o+1:o):f===`backspace`?C(a,o):C(a,l.caret),d=a.value,r?.(a.value)},E=()=>{l=!0,_(),c=!1},D=e=>{l=!1,u=!0;let n=e.target,a=S(n),o=g(n.value,t,a,{segmented:i});n.value=o.process(),C(n,o.caret),d=n.value,r?.(n.value)},O=e=>{let n=e,a=n.target,o=a.value;if(l)return;if(f===`keyup`&&u){u=!1;return}if(!n.key){c=!0,h(()=>{if(a.value===o&&a.selectionStart!==a.selectionEnd){c=!1;return}let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});a.value=n.process(),a.setSelectionRange(n.caret,n.caret),h(()=>{c=!1})});return}if(n.key===`Meta`)return;let d=n.key===`Backspace`,p=n.key===`Delete`,m=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,_=n.key===`Unidentified`;if(m&&a.selectionStart===a.selectionEnd&&o.length>=s&&!y()){n.preventDefault();return}if(c){n.preventDefault();return}!d&&!p&&!m&&!_||h(()=>{if(a.value===o&&a.selectionStart!==a.selectionEnd)return;let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});if(a.value=n.process(),_){let t=a.value.length>o.length?n.caret:e;a.setSelectionRange(t,t)}else if(p){let t=o.length===a.value.length?e+1:e;a.setSelectionRange(t,t)}else d?a.setSelectionRange(e,e):m&&a.setSelectionRange(n.caret,n.caret);r?.(a.value)})};return e.addEventListener(`paste`,v),e.addEventListener(`input`,T),e.addEventListener(`compositionstart`,E),e.addEventListener(`compositionend`,D),e.addEventListener(f,O),()=>{e.removeEventListener(`paste`,v),e.removeEventListener(`input`,T),e.removeEventListener(`compositionstart`,E),e.removeEventListener(`compositionend`,D),e.removeEventListener(f,O),e.removeAttribute(b);for(let t of a)e.removeAttribute(t);_()}}function E(e){return e>=`0`&&e<=`9`}function D(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 O(e){let t=0;for(;t<e.length-1&&e[t]===`0`;)t++;return e.slice(t)}function k(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 A(e,t){if(t<=0)return 0;let n=0;for(let r=0;r<e.length;r++)if(E(e[r])&&(n++,n===t))return r+1;return e.length}function j(e,t){let n=``,r=``,i=!1,a=!1,o=t.decimalPlaces!==0;for(let s of e){if(E(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 M(e,t,n){let r=n.decimalPlaces!==0,i=!1,a=0;for(let o=0;o<t;o++){let t=e[o];if(E(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 N(e,t=0,n){let r=D(n);if(!e)return{value:``,caret:0};let{isNegative:i,intDigits:a,fracDigits:o,hasSeparator:s}=j(e,r);if(a===``&&o===``&&!s)return{value:``,caret:0};let c=O(a||`0`),l=r.numberPlaces==null?c:c.padStart(r.numberPlaces,`0`),u=r.segmented?k(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}=M(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:_+A(u,g+v)}}function P(e,t){return N(e,e.length,t).value}function F(e,t){let{isNegative:n,intDigits:r,fracDigits:i}=j(e,D(t)),a=Number(i?`${r||`0`}.${i}`:r||`0`);return n?-a:a}function I(e,t){let n=D(t);if(n.decimalPlaces==null||n.decimalPlaces<=0)return null;let{isNegative:r,intDigits:i,hasSeparator:a}=j(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 N(l+c+n.decimalSeparator+s,l.length+c.length,n)}function L(e,t,n,r){let i=D(r),a=t-1;if(a<0||e[a]!==n)return null;let{isNegative:o,intDigits:s,fracDigits:c,hasSeparator:l}=j(e.slice(0,a)+e.slice(t),i),u=O(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 N(m+h+(l?i.decimalSeparator+c:``),m.length+h.length,i)}function R(e,t){let n=D(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=O(s||`0`),u=n.numberPlaces==null?l:l.padStart(n.numberPlaces,`0`),d=(n.segmented?k(u,n.separator):u)+(n.decimalPlaces!==0&&c!==``?n.decimalSeparator+c:``);return(r?`-`:``)+n.prefix+d+n.suffix}const z=`data-masked`;function B(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function V(e){return e.length===1&&e>=`0`&&e<=`9`}function H(e){try{return e.selectionStart??e.value.length}catch{return e.value.length}}function U(e,t){try{e.setSelectionRange(t,t)}catch{}}function W(e,t){if(e.getAttribute(z)!==null)return()=>{};let{onChange:n,...r}=B(t),i=r,{decimalSeparator:a,decimalPlaces:o}=D(i),s=[],c=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),s.push(t))};e.setAttribute(z,`decimal`),c(`autocomplete`,`off`),c(`autocorrect`,`off`),c(`autocapitalize`,`off`),c(`spellcheck`,`false`);let l=!1,u=!1,d=!1,f=null,p=y()?`keyup`:`keydown`,m=new Set,h=e=>{let t=requestAnimationFrame(()=>{m.delete(t),e()});m.add(t)},g=()=>{for(let e of m)cancelAnimationFrame(e);m.clear()},_=(e,t)=>{e.value=t.value,U(e,t.caret),n?.(t.value,F(t.value,i))},v=(e,t={})=>{let n=H(e),r=(t,r)=>{if(o===0||t.length!==1||t!==`.`&&t!==`,`||t===a)return!1;for(let i of r)if(i!=null&&i>=0&&e.value.slice(i,i+t.length)===t)return e.value=e.value.slice(0,i)+a+e.value.slice(i+t.length),n=i+t.length<=n?n+a.length-t.length:n,U(e,n),!0;return!1};if(f){let{text:e,starts:t}=f;f=null,r(e,t)}if(t.inputType===`deleteContentBackward`){let t=I(e.value,i);if(t){_(e,t);return}}let s=t.insertedText;s!=null&&r(s,[t.insertedAt,n-s.length]);let c=s!=null&&s.length===1&&V(s)?s:void 0,l=c?L(e.value,n,c,i):null;_(e,l??N(e.value,n,i))},b=e=>{let t=e.target;h(()=>{v(t)})},x=e=>{let t=e,n=e.target;g(),l=!1,f=null,d=!0,v(n,{insertedText:typeof t.data==`string`?t.data:null,inputType:t.inputType})},S=()=>{u=!0,g(),l=!1,f=null},C=e=>{u=!1,d=!0,v(e.target)},w=e=>{let t=e,n=t.target,r=H(n),i=n.value;if(u)return;if(p===`keyup`&&d){d=!1;return}if(!t.key){l=!0,h(()=>{if(n.value===i&&n.selectionStart!==n.selectionEnd){l=!1;return}v(n),h(()=>{l=!1})});return}if(t.key===`Meta`)return;if(l){t.preventDefault();return}let s=t.key===`Backspace`,c=t.key===`Delete`,m=t.key.length===1&&!t.ctrlKey&&!t.altKey&&!t.metaKey,g=t.key===`Unidentified`;m&&o!==0&&(t.key===`.`||t.key===`,`)&&t.key!==a&&(f={text:t.key,starts:[r,r-t.key.length]}),!(!s&&!c&&!m&&!g)&&h(()=>{(n.value!==i||n.selectionStart===n.selectionEnd)&&v(n,{insertedText:m?t.key:null,insertedAt:m?r:void 0,inputType:s?`deleteContentBackward`:c?`deleteContentForward`:void 0})})};return e.addEventListener(`paste`,b),e.addEventListener(`input`,x),e.addEventListener(`compositionstart`,S),e.addEventListener(`compositionend`,C),e.addEventListener(p,w),()=>{e.removeEventListener(`paste`,b),e.removeEventListener(`input`,x),e.removeEventListener(`compositionstart`,S),e.removeEventListener(`compositionend`,C),e.removeEventListener(p,w),e.removeAttribute(z);for(let t of s)e.removeAttribute(t);g()}}exports.Mask=h,exports.applyDecimalMask=N,exports.applyMask=u,exports.bind=T,exports.bindDecimal=W,exports.buildMask=g,exports.formatDecimalValue=R,exports.getMaxLength=m,exports.process=_,exports.processDecimal=P,exports.unmaskDecimal=F;
|
|
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,i){let a=``,o=``,s=0,c=0,l=!1;for(let i=0;i<t.length;i++){let u=t[i];if(u!==`9`&&u!==`Z`&&u!==`A`){o+=u;continue}let d=!1;for(;s<e.length;){let t=e[s++];if(r(t,u)){a+=o+t,o=``,d=!0,l||(s<=n?c=a.length:l=!0);break}}if(!d)break}if(l||(c=a.length),i&&o){let e=c===a.length;a+=o,e&&(c=a.length)}return{value:a,caret:c}}const a=new Map;function o(e,t){if(a.size>=64){let e=a.keys().next();e.done||a.delete(e.value)}a.set(e,t)}function s(e){let t=a.get(e);if(t)return t;let r=[],i=[],s=[],c=[],l=0;for(;l<e.length;){let t=l,a=n(e[l]);for(;l<e.length&&n(e[l])===a;)l++;let o=e.slice(t,l);a?(i.push(s.length),c.push(r.length),s.push(o),r.push({kind:`slots`,chars:o})):(i.push(-1),r.push({kind:`literal`,text:o}))}let u=s.length,d=Array(u),f=Array(u),p=Array(u+1),m=Array(r.length).fill(-1),h=Array(r.length).fill(-1),g=0;for(let e=0;e<u;e++)d[e]=g,g+=s[e].length,f[e]=c[e]>0?c[e]-1:-1;p[u]=0;for(let e=u-1;e>=0;e--)p[e]=p[e+1]+s[e].length;for(let e=0;e<r.length;e++)r[e].kind===`literal`&&(m[e]=e>0?i[e-1]:-1,h[e]=e+1<r.length?i[e+1]:-1);let _={tokens:r,runChars:s,runOffset:d,literalBeforeRun:f,capacityFromRun:p,runOfToken:i,runBeforeLiteral:m,runAfterLiteral:h,totalSlots:g};return o(e,_),_}function c(e,t){return e.tokens[e.literalBeforeRun[t]].text}function l(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 u(e,t,n,r){let i=l(e,t);for(let a=r+1;a<n.runChars.length;a++)if(e.startsWith(c(n,a),t))return i<=n.capacityFromRun[a]?a:-1;return-1}function d(e,t){let n=t.runChars.length,i=Array(t.totalSlots).fill(``),a=Array(t.totalSlots).fill(-1),o=Array(n).fill(0),s=Array(t.tokens.length).fill(-1),l=0,d=0,f=0;for(;f<e.length&&l<n;){let p=t.runChars[l],m=e[f];if(r(m,p[d])){let r=t.runOffset[l]+d;if(i[r]=m,a[r]=f,o[l]=d+1,f++,d++,d===p.length&&(l++,d=0,l<n)){let n=c(t,l);e.startsWith(n,f)&&(s[t.literalBeforeRun[l]]=f,f+=n.length)}continue}let h=u(e,f,t,l);if(h>=0){s[t.literalBeforeRun[h]]=f,f+=c(t,h).length,l=h,d=0;continue}f++}return{slotChar:i,slotSource:a,runFilled:o,literalSource:s}}function f(e,t,n){let{tokens:r,runBeforeLiteral:i,runAfterLiteral:a,literalBeforeRun:o,runChars:s}=e,{runFilled:c}=t,l=Array(r.length).fill(!1);for(let e=0;e<r.length;e++){if(r[e].kind!==`literal`)continue;let t=a[e],o=i[e];l[e]=t>=0&&c[t]>0||n&&(o<0||c[o]===s[o].length)}let u=-1;for(let e=0;e<s.length;e++){if(c[e]===0)continue;let t=o[e];if(t>=0){let n=r[t].text;for(let t=u+1;t<e;t++){let e=o[t];e<0||r[e].text===n&&(l[e]=!0)}}u=e}return l}function p(e,t,n,r,i){let{tokens:a,runChars:o,runOffset:s,runBeforeLiteral:c,runAfterLiteral:l,runOfToken:u}=e,{slotChar:d,slotSource:f,runFilled:p}=t,m=``,h=0,g=!1;for(let e=0;e<a.length;e++){let _=a[e];if(_.kind===`literal`){if(!n[e])continue;let a=c[e],s=l[e],u=s<0||p[s]===0,d=i&&(a<0||p[a]===o[a].length),f=t.literalSource[e],v=!g&&h===m.length;m+=_.text,v&&(d&&u||f>=0&&f<r)&&(h=m.length);continue}let v=u[e],y=s[v];for(let e=0;e<p[v];e++)m+=d[y+e],!g&&(f[y+e]<r?h=m.length:g=!0)}return{value:m,caret:h}}function m(e,t,n,r){let i=s(t),a=d(e,i);return p(i,a,f(i,a,r),n,r)}function h(e,t,n=0,r){if(!e)return{value:``,caret:0};let a=r?.eager!==!1;return r?.segmented===!1?i(e,t,n,a):m(e,t,n,a)}function g(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 _(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function v(e,t){if(!Array.isArray(t))return t;let n=g(e),r=0;for(;r<t.length-1&&n>_(t[r]);)r++;return t[r]}function y(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var b=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=h(this._value,this._mask,this.caret,this._options);return this.caret=e.caret,e.value}};function x(e,t,n=0,r){return new b(e,v(e,t),n,r)}function S(e,t,n){return x(e,t,0,n).process()}let C;function w(){return C===void 0&&(C=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),C}const T=`data-masked`;function E(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function D(e){try{return e.selectionStart??e.value.length}catch{return e.value.length}}function O(e,t){try{e.setSelectionRange(t,t)}catch{}}function k(e){return e===`deleteContentBackward`?`backspace`:e===`deleteContentForward`?`delete`:e&&e.startsWith(`insert`)?`insert`:`unidentified`}function A(e,t){return!t&&e}function j(e,t,n){if(e.getAttribute(T)!==null)return()=>{};let{onChange:r,segmented:i,eager:a}=E(n),o=[],s=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),o.push(t))},c=y(t);e.setAttribute(T,Array.isArray(t)?t.join(`|`):t),s(`autocomplete`,`off`),s(`autocorrect`,`off`),s(`autocapitalize`,`off`),s(`spellcheck`,`false`),s(`maxlength`,String(c));let l=!1,u=!1,d=!1,f=e.value??``,p=w()?`keyup`:`keydown`,m=new Set,h=e=>{let t=requestAnimationFrame(()=>{m.delete(t),e()});m.add(t)},g=()=>{for(let e of m)cancelAnimationFrame(e);m.clear()},_=e=>{let n=e.target;h(()=>{let e=x(n.value,t,0,{segmented:i,eager:a});n.value=e.process(),r?.(n.value)})},v=e=>{let n=e,o=e.target;g(),l=!1,d=!0;let s=D(o),c=f.length,u=k(n.inputType),p=x(o.value,t,s,{segmented:i,eager:A(a,u===`backspace`||u===`delete`)});o.value=p.process(),u===`unidentified`?O(o,o.value.length>c?p.caret:s):u===`delete`?O(o,c===o.value.length?s+1:s):u===`backspace`?O(o,s):O(o,p.caret),f=o.value,r?.(o.value)},b=()=>{u=!0,g(),l=!1},S=e=>{u=!1,d=!0;let n=e.target,o=D(n),s=x(n.value,t,o,{segmented:i,eager:a});n.value=s.process(),O(n,s.caret),f=n.value,r?.(n.value)},C=e=>{let n=e,o=n.target,s=o.value;if(u)return;if(p===`keyup`&&d){d=!1;return}if(!n.key){l=!0,h(()=>{if(o.value===s&&o.selectionStart!==o.selectionEnd){l=!1;return}let e=o.selectionStart??999,n=o.value.length<s.length,r=x(o.value,t,e,{segmented:i,eager:A(a,n)});o.value=r.process(),o.setSelectionRange(r.caret,r.caret),h(()=>{l=!1})});return}if(n.key===`Meta`)return;let f=n.key===`Backspace`,m=n.key===`Delete`,g=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,_=n.key===`Unidentified`;if(g&&o.selectionStart===o.selectionEnd&&s.length>=c&&!w()){n.preventDefault();return}if(l){n.preventDefault();return}!f&&!m&&!g&&!_||h(()=>{if(o.value===s&&o.selectionStart!==o.selectionEnd)return;let e=o.selectionStart??999,n=x(o.value,t,e,{segmented:i,eager:A(a,f||m)});if(o.value=n.process(),_){let t=o.value.length>s.length?n.caret:e;o.setSelectionRange(t,t)}else if(m){let t=s.length===o.value.length?e+1:e;o.setSelectionRange(t,t)}else f?o.setSelectionRange(e,e):g&&o.setSelectionRange(n.caret,n.caret);r?.(o.value)})};return e.addEventListener(`paste`,_),e.addEventListener(`input`,v),e.addEventListener(`compositionstart`,b),e.addEventListener(`compositionend`,S),e.addEventListener(p,C),()=>{e.removeEventListener(`paste`,_),e.removeEventListener(`input`,v),e.removeEventListener(`compositionstart`,b),e.removeEventListener(`compositionend`,S),e.removeEventListener(p,C),e.removeAttribute(T);for(let t of o)e.removeAttribute(t);g()}}function M(e){return e>=`0`&&e<=`9`}function N(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 P(e){let t=0;for(;t<e.length-1&&e[t]===`0`;)t++;return e.slice(t)}function F(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 I(e,t){if(t<=0)return 0;let n=0;for(let r=0;r<e.length;r++)if(M(e[r])&&(n++,n===t))return r+1;return e.length}function L(e,t){let n=0,r=``;t.prefix&&e.startsWith(t.prefix)?n=t.prefix.length:(t.allowNegative&&e[0]===`-`&&(r=`-`,n=1),t.prefix&&e.startsWith(t.prefix,n)&&(n+=t.prefix.length));let i=e.length;return t.suffix&&e.endsWith(t.suffix)&&i-t.suffix.length>=n&&(i-=t.suffix.length),{sign:r,body:e.slice(n,i),bodyStart:n}}function R(e,t){let n=L(e,t),r=``,i=``,a=n.sign===`-`,o=!1,s=t.decimalPlaces!==0;for(let e of n.body){if(M(e)){o?(t.decimalPlaces==null||i.length<t.decimalPlaces)&&(i+=e):(t.numberPlaces==null||r.length<t.numberPlaces)&&(r+=e);continue}if(s&&!o&&e===t.decimalSeparator){o=!0;continue}e===`-`&&t.allowNegative&&(a=!0)}return{isNegative:a,intDigits:r,fracDigits:i,hasSeparator:o}}function z(e,t,n){let r=n.decimalPlaces!==0,i=!1,a=0;for(let o=0;o<t;o++){let t=e[o];if(M(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 B(e,t=0,n){let r=N(n);if(!e)return{value:``,caret:0};let{isNegative:i,intDigits:a,fracDigits:o,hasSeparator:s}=R(e,r);if(a===``&&o===``&&!s)return{value:``,caret:0};let c=P(a||`0`),l=r.numberPlaces==null?c:c.padStart(r.numberPlaces,`0`),u=r.segmented?F(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,h=L(e,r),g=Math.max(0,Math.min(t-h.bodyStart,h.body.length)),{inFraction:_,digitsBefore:v}=z(h.body,g,r),y=p.length+r.prefix.length,b=l.length-c.length;return{value:m,caret:_?y+u.length+r.decimalSeparator.length+v:y+I(u,v+b)}}function V(e,t){return B(e,e.length,t).value}function H(e,t){let{isNegative:n,intDigits:r,fracDigits:i}=R(e,N(t)),a=Number(i?`${r||`0`}.${i}`:r||`0`);return n?-a:a}function U(e,t){let n=N(t);if(n.decimalPlaces==null||n.decimalPlaces<=0)return null;let{isNegative:r,intDigits:i,hasSeparator:a}=R(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 B(l+c+n.decimalSeparator+s,l.length+c.length,n)}function W(e,t,n,r){if(n<=0)return null;let i=t-n;if(i<0||t>e.length)return null;let a=e.slice(0,i)+e.slice(t),{bodyStart:o,body:s}=L(a,N(r)),c=o+s.length,l=i<o?o:i>c?c:-1;if(l<0)return null;let u=e.slice(i,t);return{value:a.slice(0,l)+u+a.slice(l),caret:l+n}}function G(e,t,n,r){let i=N(r),a=t-1;if(a<0||e[a]!==n)return null;let{isNegative:o,intDigits:s,fracDigits:c,hasSeparator:l}=R(e.slice(0,a)+e.slice(t),i),u=P(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 B(m+h+(l?i.decimalSeparator+c:``),m.length+h.length,i)}function K(e,t){let n=N(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=P(s||`0`),u=n.numberPlaces==null?l:l.padStart(n.numberPlaces,`0`),d=(n.segmented?F(u,n.separator):u)+(n.decimalPlaces!==0&&c!==``?n.decimalSeparator+c:``);return(r?`-`:``)+n.prefix+d+n.suffix}const q=`data-masked`;function J(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function Y(e){return e.length===1&&e>=`0`&&e<=`9`}function X(e){try{return e.selectionStart??e.value.length}catch{return e.value.length}}function Z(e,t){try{e.setSelectionRange(t,t)}catch{}}function Q(e,t){if(e.getAttribute(q)!==null)return()=>{};let{onChange:n,...r}=J(t),i=r,{decimalSeparator:a,decimalPlaces:o}=N(i),s=[],c=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),s.push(t))};e.setAttribute(q,`decimal`),c(`autocomplete`,`off`),c(`autocorrect`,`off`),c(`autocapitalize`,`off`),c(`spellcheck`,`false`);let l=!1,u=!1,d=!1,f=null,p=w()?`keyup`:`keydown`,m=new Set,h=e=>{let t=requestAnimationFrame(()=>{m.delete(t),e()});m.add(t)},g=()=>{for(let e of m)cancelAnimationFrame(e);m.clear()},_=(e,t)=>{e.value=t.value,Z(e,t.caret),n?.(t.value,H(t.value,i))},v=(e,t={})=>{let n=X(e),r=(t,r)=>{if(o===0||t.length!==1||t!==`.`&&t!==`,`||t===a)return!1;for(let i of r)if(i!=null&&i>=0&&e.value.slice(i,i+t.length)===t)return e.value=e.value.slice(0,i)+a+e.value.slice(i+t.length),n=i+t.length<=n?n+a.length-t.length:n,Z(e,n),!0;return!1};if(f){let{text:e,starts:t}=f;f=null,r(e,t)}if(t.inputType===`deleteContentBackward`){let t=U(e.value,i);if(t){_(e,t);return}}let s=t.insertedText;if(s!=null&&r(s,[t.insertedAt,n-s.length]),s!=null&&s.length>0){let t=W(e.value,n,s.length,i);t&&(e.value=t.value,n=t.caret)}let c=s!=null&&s.length===1&&Y(s)?s:void 0,l=c?G(e.value,n,c,i):null;_(e,l??B(e.value,n,i))},y=e=>{let t=e.target;h(()=>{v(t)})},b=e=>{let t=e,n=e.target;g(),l=!1,f=null,d=!0,v(n,{insertedText:typeof t.data==`string`?t.data:null,inputType:t.inputType})},x=()=>{u=!0,g(),l=!1,f=null},S=e=>{u=!1,d=!0,v(e.target)},C=e=>{let t=e,n=t.target,r=X(n),i=n.value;if(u)return;if(p===`keyup`&&d){d=!1;return}if(!t.key){l=!0,h(()=>{if(n.value===i&&n.selectionStart!==n.selectionEnd){l=!1;return}v(n),h(()=>{l=!1})});return}if(t.key===`Meta`)return;if(l){t.preventDefault();return}let s=t.key===`Backspace`,c=t.key===`Delete`,m=t.key.length===1&&!t.ctrlKey&&!t.altKey&&!t.metaKey,g=t.key===`Unidentified`;m&&o!==0&&(t.key===`.`||t.key===`,`)&&t.key!==a&&(f={text:t.key,starts:[r,r-t.key.length]}),!(!s&&!c&&!m&&!g)&&h(()=>{(n.value!==i||n.selectionStart===n.selectionEnd)&&v(n,{insertedText:m?t.key:null,insertedAt:m?r:void 0,inputType:s?`deleteContentBackward`:c?`deleteContentForward`:void 0})})};return e.addEventListener(`paste`,y),e.addEventListener(`input`,b),e.addEventListener(`compositionstart`,x),e.addEventListener(`compositionend`,S),e.addEventListener(p,C),()=>{e.removeEventListener(`paste`,y),e.removeEventListener(`input`,b),e.removeEventListener(`compositionstart`,x),e.removeEventListener(`compositionend`,S),e.removeEventListener(p,C),e.removeAttribute(q);for(let t of s)e.removeAttribute(t);g()}}exports.Mask=b,exports.applyDecimalMask=B,exports.applyMask=h,exports.bind=j,exports.bindDecimal=Q,exports.buildMask=x,exports.formatDecimalValue=K,exports.getMaxLength=y,exports.process=S,exports.processDecimal=V,exports.unmaskDecimal=H;
|
|
2
2
|
//# sourceMappingURL=mother-mask.cjs.map
|
package/dist/mother-mask.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mother-mask.cjs","names":["isDigitChar","MASKED_ATTR","getCaret","setCaret"],"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\nfunction getCaret(target: HTMLInputElement): number {\n try {\n return target.selectionStart ?? target.value.length\n } catch {\n return target.value.length\n }\n}\n\nfunction setCaret(target: HTMLInputElement, caret: number): void {\n try {\n target.setSelectionRange(caret, caret)\n } catch {\n // Some input types (for example type=\"number\") do not support text selection.\n }\n}\n\ntype InputEditKind = 'insert' | 'backspace' | 'delete' | 'unidentified'\n\n/** Classify a native `InputEvent.inputType` the same way `onKey` classifies `KeyboardEvent.key`. */\nfunction classifyInputType(inputType: string | undefined): InputEditKind {\n if (inputType === 'deleteContentBackward') return 'backspace'\n if (inputType === 'deleteContentForward') return 'delete'\n if (inputType && inputType.startsWith('insert')) return 'insert'\n return 'unidentified'\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 * Reformats post-mutation `input` events (the reliable, timing-safe signal\n * on every modern browser, including mobile IME/autocorrect) with a\n * `keydown`/`requestAnimationFrame` fallback for older browsers.\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 let isComposing = false\n let skipNextKeyup = false\n // Baseline the `input`-event path compares against to detect growth/no-op\n // edits (mirrors the role `oldValue` plays in `onKey`, but persisted\n // across calls since `input` fires once per real mutation — see `onInput`).\n let lastMaskedValue = (input as HTMLInputElement).value ?? ''\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 const cancelPendingFrames = (): void => {\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\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 // `input` fires synchronously, once per real DOM mutation, right after the\n // browser (or IME/autocorrect) has already applied the edit — unlike\n // `keydown` + `requestAnimationFrame`, there's no batching window where\n // several keystrokes can queue up before we read `selectionStart`, which is\n // what let fast typing (especially Android Chrome, where composed/\n // autocorrected characters often arrive with an unreliable or missing\n // `key`) drift the caret. This is now the primary formatting path; `onKey`\n // below stays as a `requestAnimationFrame` fallback for browsers that don't\n // fire `input` reliably.\n //\n // Deliberately does NOT bail out while `isComposing` is true. Android's\n // on-screen keyboard wraps essentially all typing in a full-QWERTY text\n // field into an IME composition session for its own autocorrect/\n // suggestion-strip bookkeeping — not just genuine multi-candidate input\n // (Pinyin, Kana, …). Since a mask's alphabet is always plain ASCII\n // digits/letters (see `matchesSlot`), whatever Android is \"composing\" is\n // already the final intended character, not a provisional candidate — so\n // reformatting immediately is safe. Waiting for `compositionend` instead\n // (as this used to) meant the mask never visibly applied while typing a\n // space-less value like a plate number, since Android only ends the\n // composition on a word boundary (space/punctuation) or blur, which may\n // never happen mid-entry.\n const onInput = (e: Event): void => {\n const inputEvent = e as InputEvent\n const target = e.target as HTMLInputElement\n cancelPendingFrames()\n lockInput = false\n skipNextKeyup = true\n\n const pos = getCaret(target)\n const previousLength = lastMaskedValue.length\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n const kind = classifyInputType(inputEvent.inputType)\n if (kind === 'unidentified') {\n const newPos = target.value.length > previousLength ? m.caret : pos\n setCaret(target, newPos)\n } else if (kind === 'delete') {\n const newPos = previousLength === target.value.length ? pos + 1 : pos\n setCaret(target, newPos)\n } else if (kind === 'backspace') {\n setCaret(target, pos)\n } else {\n setCaret(target, m.caret)\n }\n\n lastMaskedValue = target.value\n onChange?.(target.value)\n }\n\n const onCompositionStart = (): void => {\n isComposing = true\n cancelPendingFrames()\n lockInput = false\n }\n\n const onCompositionEnd = (e: Event): void => {\n isComposing = false\n skipNextKeyup = true\n\n const target = e.target as HTMLInputElement\n const pos = getCaret(target)\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n setCaret(target, m.caret)\n\n lastMaskedValue = target.value\n onChange?.(target.value)\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 if (isComposing) return\n\n // `input` already handled this keystroke (it fires before `keyup`); skip\n // the redundant iOS `keyup` pass so we don't reformat the value twice.\n if (keyEventName === 'keyup' && skipNextKeyup) {\n skipNextKeyup = false\n return\n }\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 // The browser hasn't applied this keystroke's default action yet —\n // see the comment on the identical check below.\n if (target.value === oldValue && target.selectionStart !== target.selectionEnd) {\n lockInput = false\n return\n }\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 // 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. Without this guard, a key like\n // Ctrl+A schedules a reformat frame that never gets cancelled (select-all\n // fires no `input` event), and that stray frame's `target.value =\n // m.process()` reassignment races the browser's own pending selection —\n // the reported Firefox bug where selecting all and retyping fast\n // occasionally drops the caret to the start instead of replacing the\n // selection.\n if (!isBackspace && !isDelete && !isCharInsert && !isUnidentified) return\n\n scheduleFrame(() => {\n // The scheduled frame can fire before the browser has actually applied\n // this keystroke's default action (confirmed via real-Firefox\n // tracing: `target.value` is still unchanged here). If the selection\n // is still a real range at that point, it's the range the user had\n // *before* typing — not a post-edit collapsed caret — and the browser\n // still intends to use it to replace-with-the-typed-character. Calling\n // `setSelectionRange` below would collapse that range out from under\n // the pending native edit, so the character gets inserted at the\n // collapsed point instead of replacing the selection (or dropped\n // entirely). Bail and let the authoritative `input` handler take over\n // once the edit actually lands — a collapsed caret (the case every\n // other test/path exercises) is unaffected by this check.\n if (target.value === oldValue && target.selectionStart !== target.selectionEnd) return\n\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('input', onInput)\n input.addEventListener('compositionstart', onCompositionStart)\n input.addEventListener('compositionend', onCompositionEnd)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener('input', onInput)\n input.removeEventListener('compositionstart', onCompositionStart)\n input.removeEventListener('compositionend', onCompositionEnd)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n cancelPendingFrames()\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\ninterface DecimalEdit {\n insertedText?: string | null\n insertedAt?: number\n inputType?: string\n}\n\nfunction getCaret(target: HTMLInputElement): number {\n try {\n return target.selectionStart ?? target.value.length\n } catch {\n return target.value.length\n }\n}\n\nfunction setCaret(target: HTMLInputElement, caret: number): void {\n try {\n target.setSelectionRange(caret, caret)\n } catch {\n // Some input types (for example type=\"number\") do not support text selection.\n }\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 post-mutation `input` events with\n * a keyboard/paste fallback for older browsers. Unlike the pattern masks,\n * there is no fixed pattern — the integer part grows and shrinks freely;\n * formatting is 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 let isComposing = false\n let skipNextKeyup = false\n let pendingSeparatorEdit: { text: string; starts: number[] } | null = null\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 const cancelPendingFrames = (): void => {\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n setCaret(target, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const formatCurrentValue = (target: HTMLInputElement, edit: DecimalEdit = {}): void => {\n let pos = getCaret(target)\n\n const normalizeSeparator = (text: string, starts: Array<number | undefined>): boolean => {\n if (\n decimalPlaces === 0 ||\n text.length !== 1 ||\n (text !== '.' && text !== ',') ||\n text === decimalSeparator\n ) {\n return false\n }\n\n for (const start of starts) {\n if (\n start != null &&\n start >= 0 &&\n target.value.slice(start, start + text.length) === text\n ) {\n target.value =\n target.value.slice(0, start) +\n decimalSeparator +\n target.value.slice(start + text.length)\n pos = start + text.length <= pos ? pos + decimalSeparator.length - text.length : pos\n setCaret(target, pos)\n return true\n }\n }\n\n return false\n }\n\n if (pendingSeparatorEdit) {\n const { text, starts } = pendingSeparatorEdit\n pendingSeparatorEdit = null\n normalizeSeparator(text, starts)\n }\n\n // Backspace that just deleted the decimal separator merges the integer and\n // fraction digit runs into one continuous stream. On mobile this can arrive\n // as an `input` event without a reliable keyboard event, so the special case\n // lives in the shared post-mutation formatter.\n if (edit.inputType === 'deleteContentBackward') {\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 was just inserted to the configured\n // `decimalSeparator` before parsing, so mobile `input`-only edits still open\n // the fraction segment correctly.\n const insertedText = edit.insertedText\n if (insertedText != null) {\n normalizeSeparator(insertedText, [edit.insertedAt, pos - insertedText.length])\n }\n\n // Typing a digit into a field whose integer part isn't yet full of real\n // digits extends the real digit stream instead of combining with a padding\n // zero.\n const insertedDigit =\n insertedText != null && insertedText.length === 1 && isDigitKey(insertedText)\n ? insertedText\n : undefined\n const replaced = insertedDigit\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, insertedDigit, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n formatCurrentValue(target)\n })\n }\n\n // Deliberately does NOT bail out while `isComposing` is true — see the\n // matching comment in `bind.ts`. Android wraps typing in a full-QWERTY\n // text field (e.g. a decimal input without `inputmode=\"decimal\"`) into an\n // IME composition session for autocorrect bookkeeping, not just genuine\n // multi-candidate input, and that composition may never end while the\n // user is still entering a space-less value — so waiting for\n // `compositionend` before formatting made the mask appear broken there.\n const onInput = (e: Event): void => {\n const inputEvent = e as InputEvent\n const target = e.target as HTMLInputElement\n cancelPendingFrames()\n lockInput = false\n pendingSeparatorEdit = null\n skipNextKeyup = true\n formatCurrentValue(target, {\n insertedText: typeof inputEvent.data === 'string' ? inputEvent.data : null,\n inputType: inputEvent.inputType,\n })\n }\n\n const onCompositionStart = (): void => {\n isComposing = true\n cancelPendingFrames()\n lockInput = false\n pendingSeparatorEdit = null\n }\n\n const onCompositionEnd = (e: Event): void => {\n isComposing = false\n skipNextKeyup = true\n formatCurrentValue(e.target as HTMLInputElement)\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const keyStart = getCaret(target)\n const oldValue = target.value\n\n if (isComposing) return\n\n if (keyEventName === 'keyup' && skipNextKeyup) {\n skipNextKeyup = false\n return\n }\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 // The browser hasn't applied this keystroke's default action yet —\n // see the comment on the identical check below.\n if (target.value === oldValue && target.selectionStart !== target.selectionEnd) {\n lockInput = false\n return\n }\n formatCurrentValue(target)\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 const isUnidentified = ke.key === 'Unidentified'\n if (\n isCharInsert &&\n decimalPlaces !== 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator\n ) {\n pendingSeparatorEdit = { text: ke.key, starts: [keyStart, keyStart - ke.key.length] }\n }\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 && !isUnidentified) 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 // The frame can fire before the browser has actually applied this\n // keystroke's default action. If the selection is still a real range\n // at that point, it's the range the user had *before* typing — not a\n // post-edit collapsed caret — and the browser still intends to use it\n // to replace-with-the-typed-character. Formatting now would collapse\n // that range via `setCaret` inside `formatCurrentValue` before the\n // pending native edit can use it, dropping or corrupting the\n // keystroke instead of replacing the selection with it (the same\n // Firefox race fixed in `bind.ts`). A collapsed caret (every other\n // path/test) is unaffected by this check.\n if (target.value === oldValue && target.selectionStart !== target.selectionEnd) return\n\n formatCurrentValue(target, {\n insertedText: isCharInsert ? ke.key : null,\n insertedAt: isCharInsert ? keyStart : undefined,\n inputType: isBackspace\n ? 'deleteContentBackward'\n : isDelete\n ? 'deleteContentForward'\n : undefined,\n })\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener('input', onInput)\n input.addEventListener('compositionstart', onCompositionStart)\n input.addEventListener('compositionend', onCompositionEnd)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener('input', onInput)\n input.removeEventListener('compositionstart', onCompositionStart)\n input.removeEventListener('compositionend', onCompositionEnd)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n cancelPendingFrames()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mother-mask.cjs","names":["isDigitChar","MASKED_ATTR","getCaret","setCaret"],"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(\n value: string,\n mask: string,\n inputCaret: number,\n eager: boolean,\n): 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 // Eager mode: `pending` only survives to here holding the literal(s) that\n // directly follow the slot(s) just filled — see the doc comment on\n // `ApplyMaskOptions.eager`. Reveal it now instead of waiting for the next\n // matching keystroke, and carry the caret past it only if the caret was\n // already sitting at the end of the typed content (never yank it forward\n // during a mid-string edit).\n if (eager && pending) {\n const wasAtEnd = outputCaret === output.length\n output += pending\n if (wasAtEnd) outputCaret = output.length\n }\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (default) — 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// This runs in two passes rather than emitting characters as it scans:\n//\n// 1. **assign** — decide which mask slot each character of `value` lands in,\n// using the separators still present in `value` as positional anchors.\n// 2. **render** — walk the mask and emit the assigned characters plus the\n// literals that are actually justified, tracking the caret as it goes.\n//\n// Splitting them is what makes characters \"stick\" to their segment. A single\n// emit-as-you-scan pass can only ever look one separator ahead, so a value\n// like \"015-39\" (what the browser leaves behind when you select \"012.153.441\"\n// out of \"012.153.441-39\" and type \"015\") had no way to see that the \"-\"\n// pins \"39\" to the *last* segment — it treated the \"-\" as noise and repacked\n// the digits from the left into \"015.39\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n/**\n * A mask string pre-chewed into the lookups both passes need.\n *\n * \"Run\" throughout means one uninterrupted stretch of slot characters — the\n * segment a user thinks of as a single field (\"999\", \"9999\", …). Runs are\n * numbered in mask order; token indices index {@link CompiledMask.tokens}.\n */\ninterface CompiledMask {\n /** Alternating literal / slot-run tokens, in mask order. */\n tokens: MaskToken[]\n /** Slot characters of each run (e.g. `[\"999\", \"999\", \"999\", \"99\"]`). */\n runChars: string[]\n /** Index into a flat, run-concatenated slot array where each run starts. */\n runOffset: number[]\n /** Token index of the literal directly before run `i`, or `-1` at the mask start. */\n literalBeforeRun: number[]\n /** Total slot capacity of runs `i..end`; `capacityFromRun[runCount]` is `0`. */\n capacityFromRun: number[]\n /** For each token, the run it *is* (`-1` for literals). */\n runOfToken: number[]\n /** For each literal token, the run directly before it (`-1` when it opens the mask). */\n runBeforeLiteral: number[]\n /** For each literal token, the run directly after it (`-1` when it closes the mask). */\n runAfterLiteral: number[]\n /** Total number of slots in the mask. */\n totalSlots: number\n}\n\n// A bound input re-applies the same (static) mask string on every keystroke,\n// so compiling it is pure, repeated work — cache by mask string instead of\n// re-walking and re-allocating on every keystroke.\n//\n// Bounded, because the key is caller-supplied: an app that builds mask\n// strings dynamically (a width that varies per row, a mask derived from user\n// input) would otherwise grow this map for the lifetime of the page. Real\n// apps use a handful of static masks, so the cap is far above any honest\n// working set and eviction effectively never runs; when it does, the only\n// cost is recompiling a mask, which is linear in its length.\nconst MAX_COMPILED_MASKS = 64\nconst compiledCache = new Map<string, CompiledMask>()\n\nfunction cacheCompiled(mask: string, compiled: CompiledMask): void {\n if (compiledCache.size >= MAX_COMPILED_MASKS) {\n // Map iterates in insertion order, so this drops the oldest entry.\n const oldest = compiledCache.keys().next()\n if (!oldest.done) compiledCache.delete(oldest.value)\n }\n compiledCache.set(mask, compiled)\n}\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\") and derive the\n * run/literal adjacency both passes rely on.\n */\nfunction compileMask(mask: string): CompiledMask {\n const cached = compiledCache.get(mask)\n if (cached) return cached\n\n const tokens: MaskToken[] = []\n const runOfToken: number[] = []\n const runChars: string[] = []\n const runToken: number[] = []\n\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 if (wantSlots) {\n runOfToken.push(runChars.length)\n runToken.push(tokens.length)\n runChars.push(text)\n tokens.push({ kind: 'slots', chars: text })\n } else {\n runOfToken.push(-1)\n tokens.push({ kind: 'literal', text })\n }\n }\n\n const runCount = runChars.length\n const runOffset: number[] = new Array(runCount)\n const literalBeforeRun: number[] = new Array(runCount)\n const capacityFromRun: number[] = new Array(runCount + 1)\n const runBeforeLiteral: number[] = new Array(tokens.length).fill(-1)\n const runAfterLiteral: number[] = new Array(tokens.length).fill(-1)\n\n let offset = 0\n for (let r = 0; r < runCount; r++) {\n runOffset[r] = offset\n offset += runChars[r].length\n // Tokens alternate, so the token just before a run is always a literal\n // when it exists at all.\n literalBeforeRun[r] = runToken[r] > 0 ? runToken[r] - 1 : -1\n }\n\n capacityFromRun[runCount] = 0\n for (let r = runCount - 1; r >= 0; r--) {\n capacityFromRun[r] = capacityFromRun[r + 1] + runChars[r].length\n }\n\n for (let t = 0; t < tokens.length; t++) {\n if (tokens[t].kind !== 'literal') continue\n runBeforeLiteral[t] = t > 0 ? runOfToken[t - 1] : -1\n runAfterLiteral[t] = t + 1 < tokens.length ? runOfToken[t + 1] : -1\n }\n\n const compiled: CompiledMask = {\n tokens,\n runChars,\n runOffset,\n literalBeforeRun,\n capacityFromRun,\n runOfToken,\n runBeforeLiteral,\n runAfterLiteral,\n totalSlots: offset,\n }\n cacheCompiled(mask, compiled)\n return compiled\n}\n\n/**\n * Text of the separator that introduces run `run`.\n *\n * Tokens alternate, so every run except the very first is preceded by a\n * literal. Both callers only ask about a run they are advancing *into* — never\n * run 0 — so the lookup is always defined.\n */\nfunction separatorBefore(plan: CompiledMask, run: number): string {\n return (plan.tokens[plan.literalBeforeRun[run]] as { text: string }).text\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/** Where each character of `value` ended up, as produced by {@link assignToSlots}. */\ninterface Assignment {\n /** Flat, run-concatenated slot array: the character in each slot, or `''` when empty. */\n slotChar: string[]\n /** `value` index each filled slot came from; meaningless where `slotChar` is `''`. */\n slotSource: number[]\n /** How many slots of each run are filled (always a prefix of the run). */\n runFilled: number[]\n /** For each literal token, the `value` index it was consumed from, or `-1` if it wasn't. */\n literalSource: number[]\n}\n\n/**\n * Find the segment that a separator sitting at `valueIdx` anchors the rest of\n * the value to, or `-1` when the character is just noise.\n *\n * A separator is only trusted as an anchor when everything still left in\n * `value` actually fits in the slot capacity from that segment onward.\n * Otherwise honoring it would strand data the mask can no longer hold (e.g.\n * pasting into a later segment while an earlier one is still under-filled),\n * so the character is treated as stray noise instead and the current segment\n * takes the slot it needs. Because capacity only shrinks as you move right\n * through the mask, a nearer candidate that can't fit rules out every farther\n * one too — so the search stops at the first literal that matches by text.\n */\nfunction findAnchorRun(\n value: string,\n valueIdx: number,\n plan: CompiledMask,\n fromRun: number,\n): number {\n const remaining = remainingDataChars(value, valueIdx)\n for (let run = fromRun + 1; run < plan.runChars.length; run++) {\n if (!value.startsWith(separatorBefore(plan, run), valueIdx)) continue\n return remaining <= plan.capacityFromRun[run] ? run : -1\n }\n return -1\n}\n\n/**\n * Pass 1 — place every character of `value` into a mask slot.\n *\n * Walks left to right filling the current run. Characters that don't match\n * the slot they land on are either an *anchor* (a separator that belongs to a\n * later segment, see {@link findAnchorRun} — jump there and keep the segments\n * in between empty) or noise (skip them). A run that fills up completely\n * advances to the next one, swallowing that segment's separator from `value`\n * if it's sitting right there.\n *\n * Within a run, filled slots are always a prefix — the walk never goes\n * backwards, so a run can be partially filled but never has holes.\n */\nfunction assignToSlots(value: string, plan: CompiledMask): Assignment {\n const runCount = plan.runChars.length\n const slotChar: string[] = new Array(plan.totalSlots).fill('')\n const slotSource: number[] = new Array(plan.totalSlots).fill(-1)\n const runFilled: number[] = new Array(runCount).fill(0)\n const literalSource: number[] = new Array(plan.tokens.length).fill(-1)\n\n let runIdx = 0\n let slotIdx = 0\n let valueIdx = 0\n\n while (valueIdx < value.length && runIdx < runCount) {\n const chars = plan.runChars[runIdx]\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, chars[slotIdx])) {\n const flat = plan.runOffset[runIdx] + slotIdx\n slotChar[flat] = ch\n slotSource[flat] = valueIdx\n runFilled[runIdx] = slotIdx + 1\n valueIdx++\n slotIdx++\n\n if (slotIdx === chars.length) {\n runIdx++\n slotIdx = 0\n // This segment is done: if its separator is the next thing in\n // `value`, consume it here so the following run starts clean.\n if (runIdx < runCount) {\n const text = separatorBefore(plan, runIdx)\n if (value.startsWith(text, valueIdx)) {\n literalSource[plan.literalBeforeRun[runIdx]] = valueIdx\n valueIdx += text.length\n }\n }\n }\n continue\n }\n\n const anchor = findAnchorRun(value, valueIdx, plan, runIdx)\n if (anchor >= 0) {\n literalSource[plan.literalBeforeRun[anchor]] = valueIdx\n valueIdx += separatorBefore(plan, anchor).length\n runIdx = anchor\n slotIdx = 0\n continue\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n return { slotChar, slotSource, runFilled, literalSource }\n}\n\n/**\n * Decide which literals the rendered value actually shows.\n *\n * A separator earns its place two ways:\n *\n * - **anchor** — the segment right after it holds data, so the separator is\n * what tells the reader (and the next parse) where that data belongs.\n * - **eager** — the segment right before it is completely filled, so the\n * separator is revealed before the user types the character that would\n * normally pull it in. A literal that opens the mask counts as eager too:\n * there's no segment in front of it to fill. See `ApplyMaskOptions.eager`.\n *\n * Separators around segments that are simply empty are dropped, which is what\n * collapses \"015\" + skipped middle + \"-39\" down to `015.-39` rather than\n * padding the gap with every separator in between.\n *\n * The second loop is a round-trip guard. `bind()` feeds the rendered value\n * straight back through this masking on the next keystroke, so a render that\n * doesn't parse back to the same assignment would make characters drift while\n * the user types. Dropping a separator is only safe when it can't be confused\n * for the next visible one: with `99/99/9999` holding \"1\" and \"2025\", hiding\n * the first \"/\" would leave \"1/2025\", which re-parses as 1 / 20 / 25. So any\n * hidden separator between two filled segments that reads the same as the one\n * introducing the later segment is put back — `1//2025`, which re-parses to\n * exactly what it renders. Masks with distinct separators (dates aside, most\n * of them: CPF, CNPJ, phone numbers) never hit this and stay compact.\n */\nfunction resolveLiteralVisibility(\n plan: CompiledMask,\n assignment: Assignment,\n eager: boolean,\n): boolean[] {\n const { tokens, runBeforeLiteral, runAfterLiteral, literalBeforeRun, runChars } = plan\n const { runFilled } = assignment\n const visible: boolean[] = new Array(tokens.length).fill(false)\n\n for (let t = 0; t < tokens.length; t++) {\n if (tokens[t].kind !== 'literal') continue\n const after = runAfterLiteral[t]\n const before = runBeforeLiteral[t]\n visible[t] =\n (after >= 0 && runFilled[after] > 0) ||\n (eager && (before < 0 || runFilled[before] === runChars[before].length))\n }\n\n let previousFilledRun = -1\n for (let run = 0; run < runChars.length; run++) {\n if (runFilled[run] === 0) continue\n const litToken = literalBeforeRun[run]\n if (litToken >= 0) {\n const text = (tokens[litToken] as { text: string }).text\n for (let skipped = previousFilledRun + 1; skipped < run; skipped++) {\n const skippedLit = literalBeforeRun[skipped]\n if (skippedLit < 0) continue\n if ((tokens[skippedLit] as { text: string }).text === text) visible[skippedLit] = true\n }\n }\n previousFilledRun = run\n }\n\n return visible\n}\n\n/**\n * Pass 2 — emit the assigned characters and justified literals, tracking the caret.\n *\n * **Caret algorithm**: every emitted character that came from a `value`\n * position *before* `inputCaret` pushes the output caret to the current\n * output length; the first character from at-or-after `inputCaret` freezes\n * it. A literal only carries the caret past itself while the caret is still\n * sitting at the frontier (everything emitted so far is behind it), and then\n * only when the literal isn't standing between the caret and text the user\n * hasn't reached yet: either it was revealed eagerly right after the segment\n * being typed, or it was already in `value` ahead of the caret. That's what\n * puts the caret at `015.|-39` — past the separator the just-completed \"015\"\n * revealed, but not past the \"-\" that anchors the untouched \"39\".\n */\nfunction renderAssignment(\n plan: CompiledMask,\n assignment: Assignment,\n visible: boolean[],\n inputCaret: number,\n eager: boolean,\n): MaskResult {\n const { tokens, runChars, runOffset, runBeforeLiteral, runAfterLiteral, runOfToken } = plan\n const { slotChar, slotSource, runFilled } = assignment\n\n let output = ''\n let outputCaret = 0\n let caretResolved = false\n\n for (let t = 0; t < tokens.length; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n if (!visible[t]) continue\n const before = runBeforeLiteral[t]\n const after = runAfterLiteral[t]\n // Revealed ahead of the user rather than typed by them, *and* nothing\n // waiting on the far side of it — this separator is the frontier of\n // what's been entered, so the caret belongs past it, ready for the next\n // segment. A separator dividing two segments that both already hold\n // text is not a frontier: the caret stays exactly where the browser\n // put it instead of jumping over content the user didn't touch.\n const opensEmptySegment = after < 0 || runFilled[after] === 0\n const revealedEagerly =\n eager && (before < 0 || runFilled[before] === runChars[before].length)\n const source = assignment.literalSource[t]\n const atFrontier = !caretResolved && outputCaret === output.length\n output += token.text\n if (\n atFrontier &&\n ((revealedEagerly && opensEmptySegment) || (source >= 0 && source < inputCaret))\n ) {\n outputCaret = output.length\n }\n continue\n }\n\n const run = runOfToken[t]\n const offset = runOffset[run]\n for (let s = 0; s < runFilled[run]; s++) {\n output += slotChar[offset + s]\n if (caretResolved) continue\n if (slotSource[offset + s] < inputCaret) outputCaret = output.length\n else caretResolved = true\n }\n }\n\n return { value: output, caret: outputCaret }\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but keeps every character in the\n * segment it belongs to instead of repacking the whole value from the left.\n */\nfunction applySegmentedMask(\n value: string,\n mask: string,\n inputCaret: number,\n eager: boolean,\n): MaskResult {\n const plan = compileMask(mask)\n const assignment = assignToSlots(value, plan)\n const visible = resolveLiteralVisibility(plan, assignment, eager)\n return renderAssignment(plan, assignment, visible, inputCaret, eager)\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 const eager = options?.eager !== false\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret, eager)\n : applySegmentedMask(value, mask, inputCaret, eager)\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\nfunction getCaret(target: HTMLInputElement): number {\n try {\n return target.selectionStart ?? target.value.length\n } catch {\n return target.value.length\n }\n}\n\nfunction setCaret(target: HTMLInputElement, caret: number): void {\n try {\n target.setSelectionRange(caret, caret)\n } catch {\n // Some input types (for example type=\"number\") do not support text selection.\n }\n}\n\ntype InputEditKind = 'insert' | 'backspace' | 'delete' | 'unidentified'\n\n/** Classify a native `InputEvent.inputType` the same way `onKey` classifies `KeyboardEvent.key`. */\nfunction classifyInputType(inputType: string | undefined): InputEditKind {\n if (inputType === 'deleteContentBackward') return 'backspace'\n if (inputType === 'deleteContentForward') return 'delete'\n if (inputType && inputType.startsWith('insert')) return 'insert'\n return 'unidentified'\n}\n\n/**\n * `eager`, unless this particular edit is a deletion.\n *\n * `applyMask`/`buildMask` are pure functions of `(value, caret)` — they have\n * no memory of *how* the value got there. That's a problem for eager mode\n * specifically: deleting the separator eager just added (e.g. backspacing\n * the \".\" off \"012.\") produces the exact same `(value, caret)` — raw digits,\n * caret right where the separator used to be — as the moment right before\n * that separator first appeared. A stateless recompute can't tell those two\n * apart, so eager would immediately re-add the separator the user just\n * deleted, making backspace look like it does nothing.\n *\n * `bind()` is the one layer that *does* know which happened (the DOM event\n * says so), so it's the right place to break the tie: suppress eager for the\n * single recompute that follows a delete-type edit, and let it resume on the\n * next insert. This never removes anything eager wouldn't otherwise have\n * added — it only stops eager from resurrecting a literal the user just\n * removed.\n */\nfunction eagerForEdit(eager: boolean | undefined, isDeleteLike: boolean): boolean | undefined {\n return isDeleteLike ? false : eager\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 * Reformats post-mutation `input` events (the reliable, timing-safe signal\n * on every modern browser, including mobile IME/autocorrect) with a\n * `keydown`/`requestAnimationFrame` fallback for older browsers.\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, eager } = 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 let isComposing = false\n let skipNextKeyup = false\n // Baseline the `input`-event path compares against to detect growth/no-op\n // edits (mirrors the role `oldValue` plays in `onKey`, but persisted\n // across calls since `input` fires once per real mutation — see `onInput`).\n let lastMaskedValue = (input as HTMLInputElement).value ?? ''\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 const cancelPendingFrames = (): void => {\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\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, eager })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n // `input` fires synchronously, once per real DOM mutation, right after the\n // browser (or IME/autocorrect) has already applied the edit — unlike\n // `keydown` + `requestAnimationFrame`, there's no batching window where\n // several keystrokes can queue up before we read `selectionStart`, which is\n // what let fast typing (especially Android Chrome, where composed/\n // autocorrected characters often arrive with an unreliable or missing\n // `key`) drift the caret. This is now the primary formatting path; `onKey`\n // below stays as a `requestAnimationFrame` fallback for browsers that don't\n // fire `input` reliably.\n //\n // Deliberately does NOT bail out while `isComposing` is true. Android's\n // on-screen keyboard wraps essentially all typing in a full-QWERTY text\n // field into an IME composition session for its own autocorrect/\n // suggestion-strip bookkeeping — not just genuine multi-candidate input\n // (Pinyin, Kana, …). Since a mask's alphabet is always plain ASCII\n // digits/letters (see `matchesSlot`), whatever Android is \"composing\" is\n // already the final intended character, not a provisional candidate — so\n // reformatting immediately is safe. Waiting for `compositionend` instead\n // (as this used to) meant the mask never visibly applied while typing a\n // space-less value like a plate number, since Android only ends the\n // composition on a word boundary (space/punctuation) or blur, which may\n // never happen mid-entry.\n const onInput = (e: Event): void => {\n const inputEvent = e as InputEvent\n const target = e.target as HTMLInputElement\n cancelPendingFrames()\n lockInput = false\n skipNextKeyup = true\n\n const pos = getCaret(target)\n const previousLength = lastMaskedValue.length\n const kind = classifyInputType(inputEvent.inputType)\n const m = buildMask(target.value, mask, pos, {\n segmented,\n eager: eagerForEdit(eager, kind === 'backspace' || kind === 'delete'),\n })\n target.value = m.process()\n\n if (kind === 'unidentified') {\n const newPos = target.value.length > previousLength ? m.caret : pos\n setCaret(target, newPos)\n } else if (kind === 'delete') {\n const newPos = previousLength === target.value.length ? pos + 1 : pos\n setCaret(target, newPos)\n } else if (kind === 'backspace') {\n setCaret(target, pos)\n } else {\n setCaret(target, m.caret)\n }\n\n lastMaskedValue = target.value\n onChange?.(target.value)\n }\n\n const onCompositionStart = (): void => {\n isComposing = true\n cancelPendingFrames()\n lockInput = false\n }\n\n const onCompositionEnd = (e: Event): void => {\n isComposing = false\n skipNextKeyup = true\n\n const target = e.target as HTMLInputElement\n const pos = getCaret(target)\n const m = buildMask(target.value, mask, pos, { segmented, eager })\n target.value = m.process()\n setCaret(target, m.caret)\n\n lastMaskedValue = target.value\n onChange?.(target.value)\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 if (isComposing) return\n\n // `input` already handled this keystroke (it fires before `keyup`); skip\n // the redundant iOS `keyup` pass so we don't reformat the value twice.\n if (keyEventName === 'keyup' && skipNextKeyup) {\n skipNextKeyup = false\n return\n }\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 // The browser hasn't applied this keystroke's default action yet —\n // see the comment on the identical check below.\n if (target.value === oldValue && target.selectionStart !== target.selectionEnd) {\n lockInput = false\n return\n }\n const pos = target.selectionStart ?? 999\n // No reliable `key` here, so infer delete-vs-insert from the length\n // delta the browser's already-applied default action left behind.\n const isDeleteLike = target.value.length < oldValue.length\n const m = buildMask(target.value, mask, pos, {\n segmented,\n eager: eagerForEdit(eager, isDeleteLike),\n })\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 // 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. Without this guard, a key like\n // Ctrl+A schedules a reformat frame that never gets cancelled (select-all\n // fires no `input` event), and that stray frame's `target.value =\n // m.process()` reassignment races the browser's own pending selection —\n // the reported Firefox bug where selecting all and retyping fast\n // occasionally drops the caret to the start instead of replacing the\n // selection.\n if (!isBackspace && !isDelete && !isCharInsert && !isUnidentified) return\n\n scheduleFrame(() => {\n // The scheduled frame can fire before the browser has actually applied\n // this keystroke's default action (confirmed via real-Firefox\n // tracing: `target.value` is still unchanged here). If the selection\n // is still a real range at that point, it's the range the user had\n // *before* typing — not a post-edit collapsed caret — and the browser\n // still intends to use it to replace-with-the-typed-character. Calling\n // `setSelectionRange` below would collapse that range out from under\n // the pending native edit, so the character gets inserted at the\n // collapsed point instead of replacing the selection (or dropped\n // entirely). Bail and let the authoritative `input` handler take over\n // once the edit actually lands — a collapsed caret (the case every\n // other test/path exercises) is unaffected by this check.\n if (target.value === oldValue && target.selectionStart !== target.selectionEnd) return\n\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, {\n segmented,\n eager: eagerForEdit(eager, isBackspace || isDelete),\n })\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('input', onInput)\n input.addEventListener('compositionstart', onCompositionStart)\n input.addEventListener('compositionend', onCompositionEnd)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener('input', onInput)\n input.removeEventListener('compositionstart', onCompositionStart)\n input.removeEventListener('compositionend', onCompositionEnd)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n cancelPendingFrames()\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\n/** A raw value split into its sign, its editable number text, and where that text starts. */\ninterface AffixSplit {\n /** `'-'` when a leading sign was stripped, `''` otherwise. */\n sign: string\n /** The value with sign, prefix and suffix removed — the part the user is really editing. */\n body: string\n /** Index in the original string where `body` begins. */\n bodyStart: number\n}\n\n/**\n * Peel the sign, prefix and suffix off a raw value.\n *\n * The prefix and suffix are chrome, not content, so they must not reach the\n * digit parser at all: a prefix like `\"Q1 \"` would otherwise donate its `1`\n * to the number on every keystroke, and one like `\"No. \"` would have its `.`\n * read as the decimal separator and collapse the whole value. Everything\n * downstream works on `body` so affix text simply cannot be misread.\n *\n * Matching is deliberately strict — an affix is only peeled when it is\n * actually sitting at its edge. Mid-edit a value may have a partly deleted\n * prefix, and then nothing is stripped and the old \"drop unknown characters\n * as noise\" behavior still applies.\n */\nfunction splitAffixes(raw: string, opts: ResolvedDecimalOptions): AffixSplit {\n let start = 0\n let sign = ''\n // The prefix is tried at index 0 *before* a leading \"-\" is read as a sign,\n // so a prefix that itself begins with \"-\" is not mistaken for one. Only\n // when the prefix does not match there does a leading \"-\" become the sign,\n // and the prefix is then looked for just after it — which is exactly how\n // the formatter lays a negative value out (\"-\" + prefix + number).\n if (opts.prefix && raw.startsWith(opts.prefix)) {\n start = opts.prefix.length\n } else {\n if (opts.allowNegative && raw[0] === '-') {\n sign = '-'\n start = 1\n }\n if (opts.prefix && raw.startsWith(opts.prefix, start)) start += opts.prefix.length\n }\n\n let end = raw.length\n if (opts.suffix && raw.endsWith(opts.suffix) && end - opts.suffix.length >= start) {\n end -= opts.suffix.length\n }\n\n return { sign, body: raw.slice(start, end), bodyStart: start }\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n const split = splitAffixes(raw, opts)\n let intDigits = ''\n let fracDigits = ''\n let isNegative = split.sign === '-'\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces !== 0\n\n for (const ch of split.body) {\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 // The caret is resolved inside the *body* for the same reason parsing is:\n // affix characters must not be counted as digits, and a caret parked in the\n // prefix (or out past the suffix) collapses to the nearest edge of the\n // number rather than landing somewhere inside the chrome.\n const split = splitAffixes(value, opts)\n const bodyCaret = Math.max(0, Math.min(inputCaret - split.bodyStart, split.body.length))\n const { inFraction, digitsBefore } = locateCaretSegment(split.body, bodyCaret, 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 * Move a character the browser just inserted outside the editable number — at\n * or before the sign/prefix, at or after the suffix — to the nearest edge of\n * the number instead.\n *\n * The affixes are chrome, not content. Clicking at the far left of `\"$0.00\"`\n * and typing `\"2\"` means \"make this two dollars\", not \"put a 2 to the left of\n * the dollar sign\". Left alone the keystroke lands outside the number, reads\n * back as an extra leading digit (`\"$20.00\"`), and — worse — a caret one\n * position further right, which looks identical to the user, behaves\n * completely differently.\n *\n * Takes the inserted *length* rather than the text so it stays correct after\n * an earlier pass has rewritten the character in place (a numeric keypad's\n * `\",\"` normalized to the configured decimal separator, say); whatever now\n * occupies that span is what gets moved.\n *\n * `value`/`caret` are the state *after* the browser applied the insertion,\n * the same post-insertion snapshot the rest of this module expects. Returns\n * `null` when the character already landed inside the number, so the caller\n * carries on with the value it has.\n */\nexport function relocateAffixInsertion(\n value: string,\n caret: number,\n insertedLength: number,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n if (insertedLength <= 0) return null\n const insertIdx = caret - insertedLength\n if (insertIdx < 0 || caret > value.length) return null\n\n // The value as it stood before this keystroke — the only form in which the\n // affixes are guaranteed to still be sitting at their own edges.\n const before = value.slice(0, insertIdx) + value.slice(caret)\n const opts = resolveDecimalOptions(options)\n const { bodyStart, body } = splitAffixes(before, opts)\n const editEnd = bodyStart + body.length\n\n const moveTo = insertIdx < bodyStart ? bodyStart : insertIdx > editEnd ? editEnd : -1\n if (moveTo < 0) return null\n\n const inserted = value.slice(insertIdx, caret)\n return {\n value: before.slice(0, moveTo) + inserted + before.slice(moveTo),\n caret: moveTo + insertedLength,\n }\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 relocateAffixInsertion,\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\ninterface DecimalEdit {\n insertedText?: string | null\n insertedAt?: number\n inputType?: string\n}\n\nfunction getCaret(target: HTMLInputElement): number {\n try {\n return target.selectionStart ?? target.value.length\n } catch {\n return target.value.length\n }\n}\n\nfunction setCaret(target: HTMLInputElement, caret: number): void {\n try {\n target.setSelectionRange(caret, caret)\n } catch {\n // Some input types (for example type=\"number\") do not support text selection.\n }\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 post-mutation `input` events with\n * a keyboard/paste fallback for older browsers. Unlike the pattern masks,\n * there is no fixed pattern — the integer part grows and shrinks freely;\n * formatting is 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 let isComposing = false\n let skipNextKeyup = false\n let pendingSeparatorEdit: { text: string; starts: number[] } | null = null\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 const cancelPendingFrames = (): void => {\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n setCaret(target, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const formatCurrentValue = (target: HTMLInputElement, edit: DecimalEdit = {}): void => {\n let pos = getCaret(target)\n\n const normalizeSeparator = (text: string, starts: Array<number | undefined>): boolean => {\n if (\n decimalPlaces === 0 ||\n text.length !== 1 ||\n (text !== '.' && text !== ',') ||\n text === decimalSeparator\n ) {\n return false\n }\n\n for (const start of starts) {\n if (\n start != null &&\n start >= 0 &&\n target.value.slice(start, start + text.length) === text\n ) {\n target.value =\n target.value.slice(0, start) +\n decimalSeparator +\n target.value.slice(start + text.length)\n pos = start + text.length <= pos ? pos + decimalSeparator.length - text.length : pos\n setCaret(target, pos)\n return true\n }\n }\n\n return false\n }\n\n if (pendingSeparatorEdit) {\n const { text, starts } = pendingSeparatorEdit\n pendingSeparatorEdit = null\n normalizeSeparator(text, starts)\n }\n\n // Backspace that just deleted the decimal separator merges the integer and\n // fraction digit runs into one continuous stream. On mobile this can arrive\n // as an `input` event without a reliable keyboard event, so the special case\n // lives in the shared post-mutation formatter.\n if (edit.inputType === 'deleteContentBackward') {\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 was just inserted to the configured\n // `decimalSeparator` before parsing, so mobile `input`-only edits still open\n // the fraction segment correctly.\n const insertedText = edit.insertedText\n if (insertedText != null) {\n normalizeSeparator(insertedText, [edit.insertedAt, pos - insertedText.length])\n }\n\n // The prefix and suffix are chrome, not content. A character the browser\n // dropped into them — caret parked at the far left of \"$0.00\", or out past\n // a suffix — is pulled to the nearest edge of the number, so every caret\n // position that *looks* like \"the start of the number\" behaves like it.\n // Runs after separator normalization so it moves whatever now occupies\n // that span, not the key the user originally pressed.\n if (insertedText != null && insertedText.length > 0) {\n const relocated = relocateAffixInsertion(\n target.value,\n pos,\n insertedText.length,\n decimalOptions,\n )\n if (relocated) {\n target.value = relocated.value\n pos = relocated.caret\n }\n }\n\n // Typing a digit into a field whose integer part isn't yet full of real\n // digits extends the real digit stream instead of combining with a padding\n // zero.\n const insertedDigit =\n insertedText != null && insertedText.length === 1 && isDigitKey(insertedText)\n ? insertedText\n : undefined\n const replaced = insertedDigit\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, insertedDigit, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n formatCurrentValue(target)\n })\n }\n\n // Deliberately does NOT bail out while `isComposing` is true — see the\n // matching comment in `bind.ts`. Android wraps typing in a full-QWERTY\n // text field (e.g. a decimal input without `inputmode=\"decimal\"`) into an\n // IME composition session for autocorrect bookkeeping, not just genuine\n // multi-candidate input, and that composition may never end while the\n // user is still entering a space-less value — so waiting for\n // `compositionend` before formatting made the mask appear broken there.\n const onInput = (e: Event): void => {\n const inputEvent = e as InputEvent\n const target = e.target as HTMLInputElement\n cancelPendingFrames()\n lockInput = false\n pendingSeparatorEdit = null\n skipNextKeyup = true\n formatCurrentValue(target, {\n insertedText: typeof inputEvent.data === 'string' ? inputEvent.data : null,\n inputType: inputEvent.inputType,\n })\n }\n\n const onCompositionStart = (): void => {\n isComposing = true\n cancelPendingFrames()\n lockInput = false\n pendingSeparatorEdit = null\n }\n\n const onCompositionEnd = (e: Event): void => {\n isComposing = false\n skipNextKeyup = true\n formatCurrentValue(e.target as HTMLInputElement)\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const keyStart = getCaret(target)\n const oldValue = target.value\n\n if (isComposing) return\n\n if (keyEventName === 'keyup' && skipNextKeyup) {\n skipNextKeyup = false\n return\n }\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 // The browser hasn't applied this keystroke's default action yet —\n // see the comment on the identical check below.\n if (target.value === oldValue && target.selectionStart !== target.selectionEnd) {\n lockInput = false\n return\n }\n formatCurrentValue(target)\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 const isUnidentified = ke.key === 'Unidentified'\n if (\n isCharInsert &&\n decimalPlaces !== 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator\n ) {\n pendingSeparatorEdit = { text: ke.key, starts: [keyStart, keyStart - ke.key.length] }\n }\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 && !isUnidentified) 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 // The frame can fire before the browser has actually applied this\n // keystroke's default action. If the selection is still a real range\n // at that point, it's the range the user had *before* typing — not a\n // post-edit collapsed caret — and the browser still intends to use it\n // to replace-with-the-typed-character. Formatting now would collapse\n // that range via `setCaret` inside `formatCurrentValue` before the\n // pending native edit can use it, dropping or corrupting the\n // keystroke instead of replacing the selection with it (the same\n // Firefox race fixed in `bind.ts`). A collapsed caret (every other\n // path/test) is unaffected by this check.\n if (target.value === oldValue && target.selectionStart !== target.selectionEnd) return\n\n formatCurrentValue(target, {\n insertedText: isCharInsert ? ke.key : null,\n insertedAt: isCharInsert ? keyStart : undefined,\n inputType: isBackspace\n ? 'deleteContentBackward'\n : isDelete\n ? 'deleteContentForward'\n : undefined,\n })\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener('input', onInput)\n input.addEventListener('compositionstart', onCompositionStart)\n input.addEventListener('compositionend', onCompositionEnd)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener('input', onInput)\n input.removeEventListener('compositionstart', onCompositionStart)\n input.removeEventListener('compositionend', onCompositionEnd)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n cancelPendingFrames()\n }\n}\n"],"mappings":"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"}
|