mother-mask 3.2.0 → 3.4.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/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,t,n){if(e.getAttribute(b)!==null)return()=>{};let{onChange:r,segmented:i}=x(n),a=[],o=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),a.push(t))},s=m(t);e.setAttribute(b,Array.isArray(t)?t.join(`|`):t),o(`autocomplete`,`off`),o(`autocorrect`,`off`),o(`autocapitalize`,`off`),o(`spellcheck`,`false`),o(`maxlength`,String(s));let c=!1,l=y()?`keyup`:`keydown`,u=new Set,d=e=>{let t=requestAnimationFrame(()=>{u.delete(t),e()});u.add(t)},f=e=>{let n=e.target;d(()=>{
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(e){return e===`9`||e===`Z`||e===`A`}function r(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function i(e,t,n){let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(r(t,u)){i+=a+t,a=``,d=!0,c||(o<=n?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}const a=new Map;function o(e){let t=a.get(e);if(t)return t;let r=[],i=0;for(;i<e.length;){let t=i,a=n(e[i]);for(;i<e.length&&n(e[i])===a;)i++;let o=e.slice(t,i);r.push(a?{kind:`slots`,chars:o}:{kind:`literal`,text:o})}return a.set(e,r),r}function s(e,t){let n=0;for(let r=t;r<e.length;r++){let t=e[r];t.kind===`slots`&&(n+=t.chars.length)}return n}function c(n,r){let i=0;for(let a=r;a<n.length;a++){let r=n[a];(e(r)||t(r))&&i++}return i}function l(e,t,n){let i=o(t),a=``,l=``,u=0,d=0,f=!1,p=!1;for(let t=0;t<i.length&&!p;t++){let o=i[t];if(o.kind===`literal`){l+=o.text,e.startsWith(o.text,u)&&(u+=o.text.length);continue}let m=i[t+1],h=s(i,t+1);for(let t=0;t<o.chars.length;t++){let i=o.chars[t],s=!1;for(;u<e.length;){let t=e[u];if(r(t,i)){u++,a+=l+t,l=``,s=!0,f||(u<=n?d=a.length:f=!0);break}if(m?.kind===`literal`&&e.startsWith(m.text,u)&&c(e,u)<=h)break;u++}if(!s){u>=e.length&&(p=!0);break}}}return f||(d=a.length),{value:a,caret:d}}function u(e,t,n=0,r){return e?r?.segmented===!1?i(e,t,n):l(e,t,n):{value:``,caret:0}}function d(e){let t=0;for(let n of e)(n>=`0`&&n<=`9`||n>=`a`&&n<=`z`||n>=`A`&&n<=`Z`)&&t++;return t}function f(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function p(e,t){if(!Array.isArray(t))return t;let n=d(e),r=0;for(;r<t.length-1&&n>f(t[r]);)r++;return t[r]}function m(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var h=class{caret;_value;_mask;_options;constructor(e,t,n=0,r){this._value=e,this._mask=t,this.caret=n,this._options=r}process(){let e=u(this._value,this._mask,this.caret,this._options);return this.caret=e.caret,e.value}};function g(e,t,n=0,r){return new h(e,p(e,t),n,r)}function _(e,t,n){return g(e,t,0,n).process()}let v;function y(){return v===void 0&&(v=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),v}const b=`data-masked`;function x(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function S(e,t,n){if(e.getAttribute(b)!==null)return()=>{};let{onChange:r,segmented:i}=x(n),a=[],o=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),a.push(t))},s=m(t);e.setAttribute(b,Array.isArray(t)?t.join(`|`):t),o(`autocomplete`,`off`),o(`autocorrect`,`off`),o(`autocapitalize`,`off`),o(`spellcheck`,`false`),o(`maxlength`,String(s));let c=!1,l=y()?`keyup`:`keydown`,u=new Set,d=e=>{let t=requestAnimationFrame(()=>{u.delete(t),e()});u.add(t)},f=e=>{let n=e.target;d(()=>{let e=g(n.value,t,0,{segmented:i});n.value=e.process(),r?.(n.value)})},p=e=>{let n=e,a=n.target,o=a.value;if(!n.key){c=!0,d(()=>{let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});a.value=n.process(),a.setSelectionRange(n.caret,n.caret),d(()=>{c=!1})});return}if(n.key===`Meta`)return;let l=n.key===`Backspace`,u=n.key===`Delete`,f=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,p=n.key===`Unidentified`;if(f&&a.selectionStart===a.selectionEnd&&o.length>=s&&!y()){n.preventDefault();return}if(c){n.preventDefault();return}d(()=>{let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});if(a.value=n.process(),p){let t=a.value.length>o.length?n.caret:e;a.setSelectionRange(t,t)}else if(u){let t=o.length===a.value.length?e+1:e;a.setSelectionRange(t,t)}else l?a.setSelectionRange(e,e):f&&a.setSelectionRange(n.caret,n.caret);r?.(a.value)})};return e.addEventListener(`paste`,f),e.addEventListener(l,p),()=>{e.removeEventListener(`paste`,f),e.removeEventListener(l,p),e.removeAttribute(b);for(let t of a)e.removeAttribute(t);for(let e of u)cancelAnimationFrame(e);u.clear()}}function C(e){return e>=`0`&&e<=`9`}function w(e){let t=e?.decimalPlaces,n=t!=null&&Number.isFinite(t)?Math.max(0,Math.floor(t)):void 0,r=e?.numberPlaces;return{decimalPlaces:n,numberPlaces:r!=null&&Number.isFinite(r)?Math.max(1,Math.floor(r)):void 0,segmented:e?.segmented??!0,separator:e?.separator??`,`,decimalSeparator:e?.decimalSeparator??`.`,prefix:e?.prefix??``,suffix:e?.suffix??``,allowNegative:e?.allowNegative??!1}}function T(e){let t=0;for(;t<e.length-1&&e[t]===`0`;)t++;return e.slice(t)}function E(e,t){if(!t||e.length<=3)return e;let n=[],r=e.length;for(;r>3;)n.unshift(e.slice(r-3,r)),r-=3;return n.unshift(e.slice(0,r)),n.join(t)}function D(e,t){if(t<=0)return 0;let n=0;for(let r=0;r<e.length;r++)if(C(e[r])&&(n++,n===t))return r+1;return e.length}function O(e,t){let n=``,r=``,i=!1,a=!1,o=t.decimalPlaces!==0;for(let s of e){if(C(s)){a?(t.decimalPlaces==null||r.length<t.decimalPlaces)&&(r+=s):(t.numberPlaces==null||n.length<t.numberPlaces)&&(n+=s);continue}if(o&&!a&&s===t.decimalSeparator){a=!0;continue}s===`-`&&t.allowNegative&&(i=!0)}return{isNegative:i,intDigits:n,fracDigits:r,hasSeparator:a}}function k(e,t,n){let r=n.decimalPlaces!==0,i=!1,a=0;for(let o=0;o<t;o++){let t=e[o];if(C(t)){i?(n.decimalPlaces==null||a<n.decimalPlaces)&&a++:(n.numberPlaces==null||a<n.numberPlaces)&&a++;continue}r&&!i&&t===n.decimalSeparator&&(i=!0,a=0)}return{inFraction:i,digitsBefore:a}}function A(e,t=0,n){let r=w(n);if(!e)return{value:``,caret:0};let{isNegative:i,intDigits:a,fracDigits:o,hasSeparator:s}=O(e,r);if(a===``&&o===``&&!s)return{value:``,caret:0};let c=T(a||`0`),l=r.numberPlaces==null?c:c.padStart(r.numberPlaces,`0`),u=r.segmented?E(l,r.separator):l,d=r.decimalPlaces!=null&&r.decimalPlaces>0?o.padEnd(r.decimalPlaces,`0`):o,f=u+(r.decimalPlaces!==0&&(r.decimalPlaces!=null||s)?r.decimalSeparator+d:``),p=i?`-`:``,m=p+r.prefix+f+r.suffix,{inFraction:h,digitsBefore:g}=k(e,Math.max(0,Math.min(t,e.length)),r),_=p.length+r.prefix.length,v=l.length-c.length;return{value:m,caret:h?_+u.length+r.decimalSeparator.length+g:_+D(u,g+v)}}function j(e,t){return A(e,e.length,t).value}function M(e,t){let{isNegative:n,intDigits:r,fracDigits:i}=O(e,w(t)),a=Number(i?`${r||`0`}.${i}`:r||`0`);return n?-a:a}function N(e,t){let n=w(t);if(n.decimalPlaces==null||n.decimalPlaces<=0)return null;let{isNegative:r,intDigits:i,hasSeparator:a}=O(e,n);if(a||i.length<n.decimalPlaces+1)return null;let o=i.length-n.decimalPlaces,s=i.slice(o),c=i.slice(0,o-1),l=r?`-`:``;return A(l+c+n.decimalSeparator+s,l.length+c.length,n)}function P(e,t,n,r){let i=w(r),a=t-1;if(a<0||e[a]!==n)return null;let{isNegative:o,intDigits:s,fracDigits:c,hasSeparator:l}=O(e.slice(0,a)+e.slice(t),i),u=T(s||`0`),d=u!==`0`,f=d&&i.numberPlaces!=null&&u.length<i.numberPlaces;if(d&&!f)return null;let p=+!!o+i.prefix.length;if(a<p||a>p+s.length)return null;let m=o?`-`:``,h=(d?u:``)+n;return A(m+h+(l?i.decimalSeparator+c:``),m.length+h.length,i)}function F(e,t){let n=w(t);if(!Number.isFinite(e))return``;let r=n.allowNegative&&e<0,i=Math.abs(e),a=n.decimalPlaces==null?String(i):i.toFixed(n.decimalPlaces),o=a.indexOf(`.`),s=o===-1?a:a.slice(0,o),c=o===-1?``:a.slice(o+1),l=T(s||`0`),u=n.numberPlaces==null?l:l.padStart(n.numberPlaces,`0`),d=(n.segmented?E(u,n.separator):u)+(n.decimalPlaces!==0&&c!==``?n.decimalSeparator+c:``);return(r?`-`:``)+n.prefix+d+n.suffix}const I=`data-masked`;function L(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function R(e){return e.length===1&&e>=`0`&&e<=`9`}function z(e,t){if(e.getAttribute(I)!==null)return()=>{};let{onChange:n,...r}=L(t),i=r,{decimalSeparator:a,decimalPlaces:o}=w(i),s=[],c=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),s.push(t))};e.setAttribute(I,`decimal`),c(`autocomplete`,`off`),c(`autocorrect`,`off`),c(`autocapitalize`,`off`),c(`spellcheck`,`false`);let l=!1,u=y()?`keyup`:`keydown`,d=new Set,f=e=>{let t=requestAnimationFrame(()=>{d.delete(t),e()});d.add(t)},p=(e,t)=>{e.value=t.value,e.setSelectionRange(t.caret,t.caret),n?.(t.value,M(t.value,i))},m=e=>{let t=e.target;f(()=>{p(t,A(t.value,t.value.length,i))})},h=e=>{let t=e,n=t.target;if(!t.key){l=!0,f(()=>{let e=n.selectionStart??n.value.length;p(n,A(n.value,e,i)),f(()=>{l=!1})});return}if(t.key===`Meta`)return;if(l){t.preventDefault();return}let r=t.key===`Backspace`,s=t.key===`Delete`,c=t.key.length===1&&!t.ctrlKey&&!t.altKey&&!t.metaKey;!r&&!s&&!c||f(()=>{let e=n.selectionStart??n.value.length;if(r){let e=N(n.value,i);if(e){p(n,e);return}}o!==0&&(t.key===`.`||t.key===`,`)&&t.key!==a&&e>0&&n.value[e-1]===t.key&&(n.value=n.value.slice(0,e-1)+a+n.value.slice(e));let s=R(t.key)?P(n.value,e,t.key,i):null;p(n,s??A(n.value,e,i))})};return e.addEventListener(`paste`,m),e.addEventListener(u,h),()=>{e.removeEventListener(`paste`,m),e.removeEventListener(u,h),e.removeAttribute(I);for(let t of s)e.removeAttribute(t);for(let e of d)cancelAnimationFrame(e);d.clear()}}exports.Mask=h,exports.applyDecimalMask=A,exports.applyMask=u,exports.bind=S,exports.bindDecimal=z,exports.buildMask=g,exports.formatDecimalValue=F,exports.getMaxLength=m,exports.process=_,exports.processDecimal=j,exports.unmaskDecimal=M;
|
|
2
2
|
//# sourceMappingURL=mother-mask.cjs.map
|
package/dist/mother-mask.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mother-mask.cjs","names":["isDigitChar","MASKED_ATTR"],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts","../src/decimal-mask.ts","../src/bind-decimal.ts"],"sourcesContent":["import type { ApplyMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction isSlotChar(ch: string): boolean {\n return ch === '9' || ch === 'Z' || ch === 'A'\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Flat masking (default) — treats the mask as one continuous character\n// stream. Best for continuous identifiers (phone numbers, CPF/CNPJ, credit\n// cards) where deleting/inserting a digit anywhere is expected to reflow\n// every digit after it — this is the classic mother-mask behavior and is\n// relied on by the majority of the test suite (paste, backspace, mid-string\n// insert, etc).\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nfunction applyFlatMask(value: string, mask: string, inputCaret: number): MaskResult {\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (opt-in) — treats literal separators as hard boundaries\n// between independent fields (e.g. day/month/year in \"99/99/9999\"). Editing\n// one segment never bleeds characters into a neighboring one, so replacing\n// the \"12\" in \"25/12/2025\" with a shorter or longer value keeps the year\n// exactly where it is instead of shifting digits across the \"/\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n// A bound input re-applies the same (static) mask string on every keystroke,\n// so tokenizing it is pure, repeated work — cache by mask string instead of\n// re-walking and re-allocating tokens on every keystroke.\nconst tokenCache = new Map<string, MaskToken[]>()\n\n/**\n * Split a mask into alternating literal and slot-run tokens (e.g. \"99/99/9999\"\n * → slots\"99\", literal\"/\", slots\"99\", literal\"/\", slots\"9999\"), so the masking\n * pass can reason about segment boundaries instead of a flat character stream.\n */\nfunction tokenizeMask(mask: string): MaskToken[] {\n const cached = tokenCache.get(mask)\n if (cached) return cached\n\n const tokens: MaskToken[] = []\n let i = 0\n while (i < mask.length) {\n const start = i\n const wantSlots = isSlotChar(mask[i])\n while (i < mask.length && isSlotChar(mask[i]) === wantSlots) i++\n const text = mask.slice(start, i)\n tokens.push(wantSlots ? { kind: 'slots', chars: text } : { kind: 'literal', text })\n }\n tokenCache.set(mask, tokens)\n return tokens\n}\n\n/** Total slot capacity from token index `from` (inclusive) to the end of `tokens`. */\nfunction slotCapacityFrom(tokens: MaskToken[], from: number): number {\n let capacity = 0\n for (let i = from; i < tokens.length; i++) {\n const token = tokens[i]\n if (token.kind === 'slots') capacity += token.chars.length\n }\n return capacity\n}\n\n/** Count of remaining slot-matchable (digit/letter) characters in `value` from `fromIdx` onward. */\nfunction remainingDataChars(value: string, fromIdx: number): number {\n let count = 0\n for (let i = fromIdx; i < value.length; i++) {\n const ch = value[i]\n if (isDigitChar(ch) || isLetterChar(ch)) count++\n }\n return count\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but walks the mask one *segment* at\n * a time (a run of slots, or a literal) rather than one character at a time.\n * The rule that keeps an edit inside a segment from crossing into its\n * neighbor is an **early stop**: if the value hits the literal that ends the\n * current slot run before all of that run's slots are filled (e.g. only one\n * digit typed into a two-digit month slot), the run is left partially filled\n * instead of skipping past the separator to steal a digit from the next\n * segment.\n *\n * That stop is only taken when it's actually safe — i.e. every character\n * still to come in `value` fits in the slot capacity that remains *after*\n * this segment. If stopping here would strand more data than the rest of\n * the mask can hold (e.g. pasting into a later segment while an earlier one\n * still sits under-filled from before), the \"separator\" is treated as stray\n * noise instead and skipped, letting this segment take the extra slot it\n * needs so nothing at the end gets silently dropped. This is also what lets\n * an array mask grow or shrink its pattern (moving every literal after the\n * change point) reflow correctly instead of losing a digit at the boundary.\n */\nfunction applySegmentedMask(value: string, mask: string, inputCaret: number): MaskResult {\n const tokens = tokenizeMask(mask)\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n let exhausted = false\n\n for (let t = 0; t < tokens.length && !exhausted; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n pending += token.text\n if (value.startsWith(token.text, valueIdx)) valueIdx += token.text.length\n continue\n }\n\n const nextToken = tokens[t + 1]\n const capacityAfter = slotCapacityFrom(tokens, t + 1)\n\n for (let s = 0; s < token.chars.length; s++) {\n const slotCh = token.chars[s]\n let found = false\n\n while (valueIdx < value.length) {\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, slotCh)) {\n valueIdx++\n output += pending + ch\n pending = ''\n found = true\n\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n\n // This segment's ending separator showed up before the run filled.\n // Stopping here is only safe if the rest of the value still fits in\n // whatever slot capacity remains after this segment — otherwise\n // stopping would strand data, so fall through and skip this char as\n // noise instead, letting the segment take the slot it needs.\n if (\n nextToken?.kind === 'literal' &&\n value.startsWith(nextToken.text, valueIdx) &&\n remainingDataChars(value, valueIdx) <= capacityAfter\n ) {\n break\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n if (!found) {\n if (valueIdx >= value.length) exhausted = true\n break\n }\n }\n }\n\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\nexport function applyMask(\n value: string,\n mask: string,\n inputCaret = 0,\n options?: ApplyMaskOptions,\n): MaskResult {\n if (!value) return { value: '', caret: 0 }\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret)\n : applySegmentedMask(value, mask, inputCaret)\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { ApplyMaskOptions, MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n private readonly _options: ApplyMaskOptions | undefined\n\n constructor(value: string, mask: string, caret = 0, options?: ApplyMaskOptions) {\n this._value = value\n this._mask = mask\n this.caret = caret\n this._options = options\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret, this._options)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(\n value: string,\n mask: MaskPattern,\n caret = 0,\n options?: ApplyMaskOptions,\n): Mask {\n return new Mask(value, resolveMask(value, mask), caret, options)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string {\n return buildMask(value, mask, 0, options).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, segmented } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n // Computed once here rather than inside `onKey` — the mask never changes\n // for the lifetime of this binding, and `onKey` is a synchronous, hot,\n // input-blocking path run on every keystroke.\n const maxLength = getMaxLength(mask)\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(maxLength))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n const m = buildMask(target.value, mask, 0, { segmented })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= maxLength && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n","import type { DecimalMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — mirrors apply-mask.ts)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\n// ---------------------------------------------------------------------------\n// Option resolution\n// ---------------------------------------------------------------------------\n\ninterface ResolvedDecimalOptions {\n /**\n * `undefined` means an optional, uncapped fraction (default) — the\n * decimal separator and fraction only appear once the user actually types\n * them, and there's no limit on how many digits follow. `0` means no\n * fraction at all. A positive number is a fixed, zero-padded width that's\n * always shown, even before the user types anything.\n */\n decimalPlaces: number | undefined\n /** `undefined` means unlimited (default) — the integer part grows freely. */\n numberPlaces: number | undefined\n segmented: boolean\n separator: string\n decimalSeparator: string\n prefix: string\n suffix: string\n allowNegative: boolean\n}\n\n/** @internal exported for {@link bindDecimal}'s \".\" / \",\" key normalization */\nexport function resolveDecimalOptions(options?: DecimalMaskOptions): ResolvedDecimalOptions {\n const rawPlaces = options?.decimalPlaces\n const decimalPlaces =\n rawPlaces != null && Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : undefined\n const rawNumberPlaces = options?.numberPlaces\n const numberPlaces =\n rawNumberPlaces != null && Number.isFinite(rawNumberPlaces)\n ? Math.max(1, Math.floor(rawNumberPlaces))\n : undefined\n return {\n decimalPlaces,\n numberPlaces,\n segmented: options?.segmented ?? true,\n separator: options?.separator ?? ',',\n decimalSeparator: options?.decimalSeparator ?? '.',\n prefix: options?.prefix ?? '',\n suffix: options?.suffix ?? '',\n allowNegative: options?.allowNegative ?? false,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Digit-stream helpers\n// ---------------------------------------------------------------------------\n\n/** Strip leading zeros from a digit string, always keeping at least one digit. */\nfunction stripLeadingZeros(s: string): string {\n let i = 0\n while (i < s.length - 1 && s[i] === '0') i++\n return s.slice(i)\n}\n\n/** Insert `sep` every 3 digits from the right (e.g. \"1234567\" → \"1,234,567\"). */\nfunction groupThousands(s: string, sep: string): string {\n if (!sep || s.length <= 3) return s\n const parts: string[] = []\n let i = s.length\n while (i > 3) {\n parts.unshift(s.slice(i - 3, i))\n i -= 3\n }\n parts.unshift(s.slice(0, i))\n return parts.join(sep)\n}\n\n/**\n * Find the position in `s` that leaves exactly `digitsBefore` digit\n * characters preceding it — the position immediately after that digit and\n * before any subsequent literal (grouping separator, ...), so the caret\n * stays glued to the last digit the user placed there.\n */\nfunction caretForDigitsBefore(s: string, digitsBefore: number): number {\n if (digitsBefore <= 0) return 0\n let count = 0\n for (let i = 0; i < s.length; i++) {\n if (isDigitChar(s[i])) {\n count++\n if (count === digitsBefore) return i + 1\n }\n }\n return s.length\n}\n\n// ---------------------------------------------------------------------------\n// Segmented parsing — integer digits before the decimal separator, fraction\n// digits after. Unlike a slot-pattern mask, the integer segment has no fixed\n// length. The fraction is only fixed-width when `decimalPlaces` is set to a\n// positive number — zero-padded on the right so a shorter fraction reads as\n// its low-order (trailing) digits being zero rather than reflowing/shifting\n// (e.g. editing \"423,42\" down to \"423,4\" produces \"423,40\", not \"42,34\").\n// When `decimalPlaces` is left unset, the fraction is optional and uncapped:\n// it only exists once the user types the separator, and grows to however\n// many digits they type.\n//\n// The decimal separator only has meaning as the *first* occurrence of\n// `opts.decimalSeparator`; every other non-digit character (thousands\n// separator, prefix/suffix text, a second stray separator, ...) is noise and\n// is dropped. This keeps re-parsing an already-masked value idempotent.\n// ---------------------------------------------------------------------------\n\ninterface DecimalParts {\n isNegative: boolean\n intDigits: string\n fracDigits: string\n hasSeparator: boolean\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n let intDigits = ''\n let fracDigits = ''\n let isNegative = false\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces !== 0\n\n for (const ch of raw) {\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else if (opts.numberPlaces == null || intDigits.length < opts.numberPlaces) {\n intDigits += ch\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n continue\n }\n if (ch === '-' && opts.allowNegative) isNegative = true\n // Anything else — thousands separator, prefix/suffix text, a repeated\n // separator, stray letters — is noise and is dropped.\n }\n\n return { isNegative, intDigits, fracDigits, hasSeparator: inFraction }\n}\n\n/**\n * Walk `raw[0:caret]` to find which segment the caret sits in (integer or\n * fraction) and how many digits of that segment precede it, so the same\n * position can be re-derived in the freshly formatted output.\n */\nfunction locateCaretSegment(\n raw: string,\n caret: number,\n opts: ResolvedDecimalOptions,\n): { inFraction: boolean; digitsBefore: number } {\n const canHaveFraction = opts.decimalPlaces !== 0\n let inFraction = false\n let digitsBefore = 0\n\n for (let i = 0; i < caret; i++) {\n const ch = raw[i]\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || digitsBefore < opts.decimalPlaces) digitsBefore++\n } else if (opts.numberPlaces == null || digitsBefore < opts.numberPlaces) {\n digitsBefore++\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n digitsBefore = 0\n }\n }\n\n return { inFraction, digitsBefore }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry points\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a decimal/currency mask to a value, producing the masked output and\n * a computed caret position. Digits typed before the decimal separator\n * extend the integer part; the fraction only starts once the separator is\n * typed. With a fixed `decimalPlaces` it's always displayed zero-padded to\n * that width, even before the user types it; left unset, the fraction is\n * optional — it only appears once the separator is typed, and is shown\n * exactly as typed (no padding, no cap on how many digits).\n */\nexport function applyDecimalMask(\n value: string,\n inputCaret = 0,\n options?: DecimalMaskOptions,\n): MaskResult {\n const opts = resolveDecimalOptions(options)\n if (!value) return { value: '', caret: 0 }\n\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (intDigits === '' && fracDigits === '' && !hasSeparator) return { value: '', caret: 0 }\n\n const intPart = stripLeadingZeros(intDigits || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const fracPadded =\n opts.decimalPlaces != null && opts.decimalPlaces > 0\n ? fracDigits.padEnd(opts.decimalPlaces, '0')\n : fracDigits\n const showFraction = opts.decimalPlaces === 0 ? false : opts.decimalPlaces != null || hasSeparator\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPadded : '')\n const signStr = isNegative ? '-' : ''\n const output = signStr + opts.prefix + numberStr + opts.suffix\n\n const clampedCaret = Math.max(0, Math.min(inputCaret, value.length))\n const { inFraction, digitsBefore } = locateCaretSegment(value, clampedCaret, opts)\n const prefixLen = signStr.length + opts.prefix.length\n // Left-padding zeros are synthetic — prepended ahead of every real typed\n // digit — so they shift where the caret's `digitsBefore`-th real digit\n // lands in `groupedInt` by the padding's width.\n const padLength = paddedInt.length - intPart.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore + padLength)\n\n return { value: output, caret }\n}\n\n/** Apply a decimal mask to a raw value and return just the masked string. */\nexport function processDecimal(value: string, options?: DecimalMaskOptions): string {\n return applyDecimalMask(value, value.length, options).value\n}\n\n/**\n * Parse a raw or already-masked decimal value back into a JS number.\n * Ignores prefix/suffix/thousands separator; returns `0` for an empty or\n * digit-less value.\n */\nexport function unmaskDecimal(value: string, options?: DecimalMaskOptions): number {\n const opts = resolveDecimalOptions(options)\n const { isNegative, intDigits, fracDigits } = computeDecimalParts(value, opts)\n const n = Number(fracDigits ? `${intDigits || '0'}.${fracDigits}` : intDigits || '0')\n return isNegative ? -n : n\n}\n\n/**\n * After a Backspace removes the decimal separator itself, the integer and\n * fraction digit runs collapse into one continuous stream (e.g. \"25.00\"\n * with the caret right after \".\" → Backspace deletes the \".\" → \"2500\").\n * Left alone, that reads as one big integer (\"$2,500.00\"). This restores\n * the segment boundary instead: the trailing `decimalPlaces` digits are\n * still the fraction, and the digit right before them — the one that used\n * to sit at the end of the integer part — is the one Backspace actually\n * removed, so it's dropped (not kept) — \"$25.00\" → \"$2.00\".\n *\n * Only applies when `decimalPlaces` is a fixed positive number — that's\n * what \"the trailing N digits are the fraction\" means. Every reformat\n * re-appends `decimalSeparator` in that case, so its absence from `value`\n * is an unambiguous signal that this exact keystroke just deleted it — no\n * \"value before this keystroke\" snapshot is needed. With `decimalPlaces`\n * unset (optional, uncapped fraction) there's no fixed width to reconstruct\n * from, so the merged digits are left as one continuous integer instead —\n * the same reasoning `numberPlaces` uses for an unbounded integer part.\n * Returns `null` when there's nothing to restore (the separator is still\n * present, `decimalPlaces` is `0` or unset, or too few digits remain), so\n * the caller falls through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskUnmergingSeparator(\n value: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n if (opts.decimalPlaces == null || opts.decimalPlaces <= 0) return null\n\n const { isNegative, intDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (hasSeparator || intDigits.length < opts.decimalPlaces + 1) return null\n\n const preMergeIntLength = intDigits.length - opts.decimalPlaces\n const fracDigits = intDigits.slice(preMergeIntLength)\n const remainingInt = intDigits.slice(0, preMergeIntLength - 1)\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + remainingInt + opts.decimalSeparator + fracDigits\n return applyDecimalMask(raw, signPart.length + remainingInt.length, opts)\n}\n\n/**\n * Special-cases typing a single digit into an integer segment that isn't\n * yet full of *real* digits — either because it's still the auto-inserted\n * zero placeholder (\"0\", or a wider \"00\" from a `numberPlaces`-padded field\n * that hasn't been touched), or because `numberPlaces` is left-padding a\n * partially-typed segment with synthetic zeros (e.g. \"02\" is really just\n * the one real digit \"2\", padded out to width 2 for display). Those padding\n * zeros aren't editable content, so the new digit extends the real digit\n * stream instead of combining with a padding zero at the caret:\n *\n * - \"$0.00\" + \"2\" → \"$2.00\" (not \"$20.00\")\n * - a `numberPlaces: 2` time field's untouched \"00:00\" + \"5\" → \"05:00\"\n * - that same field's \"02:00\" (one real digit, one padding zero) + \"4\" →\n * \"24:00\" — the real \"2\" is kept, the padding \"0\" is not\n *\n * A segment that's already full of real digits — e.g. \"23:00\", both digits\n * genuinely typed — doesn't match here and falls through to the default\n * {@link applyDecimalMask}, which already drops the overflow keystroke\n * (typing a 3rd real digit leaves \"23:00\" unchanged).\n *\n * `value`/`caret` must be the state *after* the browser has already\n * inserted `digit` at `caret - 1` (the same post-insertion snapshot\n * `applyDecimalMask` itself expects from `bindDecimal`). Returns `null`\n * when the pattern doesn't apply, so the caller falls through to a plain\n * {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskReplacingLoneZero(\n value: string,\n caret: number,\n digit: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n const insertIdx = caret - 1\n if (insertIdx < 0 || value[insertIdx] !== digit) return null\n\n const withoutDigit = value.slice(0, insertIdx) + value.slice(caret)\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(withoutDigit, opts)\n\n const realDigits = stripLeadingZeros(intDigits || '0')\n const hasRealDigits = realDigits !== '0'\n const hasPaddingRoom =\n hasRealDigits && opts.numberPlaces != null && realDigits.length < opts.numberPlaces\n if (hasRealDigits && !hasPaddingRoom) return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + intDigits.length) return null\n\n const signPart = isNegative ? '-' : ''\n const newIntDigits = (hasRealDigits ? realDigits : '') + digit\n const raw = signPart + newIntDigits + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + newIntDigits.length, opts)\n}\n\n/**\n * Format a plain JS number into its masked display string. With a fixed\n * `decimalPlaces` the fraction is rounded/padded to that exact width, even\n * for a whole number; left unset, the fraction is only shown when the value\n * actually has one, with as many digits as `value` naturally carries (no\n * padding, no rounding).\n */\nexport function formatDecimalValue(value: number, options?: DecimalMaskOptions): string {\n const opts = resolveDecimalOptions(options)\n if (!Number.isFinite(value)) return ''\n\n const isNegative = opts.allowNegative && value < 0\n const abs = Math.abs(value)\n const fixed = opts.decimalPlaces != null ? abs.toFixed(opts.decimalPlaces) : String(abs)\n const dotIdx = fixed.indexOf('.')\n const intRaw = dotIdx === -1 ? fixed : fixed.slice(0, dotIdx)\n const fracPart = dotIdx === -1 ? '' : fixed.slice(dotIdx + 1)\n const intPart = stripLeadingZeros(intRaw || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const showFraction = opts.decimalPlaces === 0 ? false : fracPart !== ''\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPart : '')\n\n return (isNegative ? '-' : '') + opts.prefix + numberStr + opts.suffix\n}\n","import {\n applyDecimalMask,\n applyDecimalMaskReplacingLoneZero,\n applyDecimalMaskUnmergingSeparator,\n resolveDecimalOptions,\n unmaskDecimal,\n} from './decimal-mask'\nimport { isIos } from './platform'\nimport type { BindDecimalOptions, DecimalMaskOptions, MaskResult } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindDecimalOptions(\n second:\n | BindDecimalOptions\n | ((value: string, numericValue: number) => void)\n | null\n | undefined,\n): BindDecimalOptions {\n if (second == null) return {}\n if (typeof second === 'function') return { onChange: second }\n return second\n}\n\nfunction isDigitKey(key: string): boolean {\n return key.length === 1 && key >= '0' && key <= '9'\n}\n\n/**\n * Bind a decimal/currency mask to an input element.\n *\n * Same contract as {@link bind}: idempotent (marked with `data-masked`),\n * returns a dispose function, and reformats on paste and keyboard-driven\n * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no\n * fixed pattern — the integer part grows and shrinks freely; formatting is\n * driven entirely by `options`.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.\n */\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n options?: BindDecimalOptions | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n onChange: ((value: string, numericValue: number) => void) | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n second?: BindDecimalOptions | ((value: string, numericValue: number) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, ...maskOptions } = toBindDecimalOptions(second)\n const decimalOptions: DecimalMaskOptions = maskOptions\n const { decimalSeparator, decimalPlaces } = resolveDecimalOptions(decimalOptions)\n\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, 'decimal')\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n\n let lockInput = false\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n target.setSelectionRange(m.caret, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n applyResult(target, applyDecimalMask(target.value, target.value.length, decimalOptions))\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n\n // Navigation (arrows, Home/End, Tab, ...), selection, and shortcut keys\n // (Ctrl/Cmd+A, Ctrl/Cmd+C, ...) don't change the text — leave the\n // browser's native caret/selection handling alone instead of recomputing\n // and overwriting it on every keystroke.\n if (!isBackspace && !isDelete && !isCharInsert) return\n\n // Everything below reads `target.value`/`selectionStart` inside the rAF\n // callback rather than synchronously here, since the browser's native\n // character insertion for this keystroke isn't guaranteed to have landed\n // yet at the point a keydown listener runs — only by the next frame.\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n\n // Backspace that just deleted the decimal separator merges the\n // integer and fraction digit runs into one continuous stream —\n // restore the boundary instead of treating that as one big integer.\n if (isBackspace) {\n const unmerged = applyDecimalMaskUnmergingSeparator(target.value, decimalOptions)\n if (unmerged) {\n applyResult(target, unmerged)\n return\n }\n }\n\n // A numeric keypad (or a locale mismatch) may only offer \".\" or \",\".\n // Normalize whichever one the user just typed to the configured\n // `decimalSeparator` so it reliably opens the fraction segment.\n if (\n decimalPlaces !== 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator &&\n pos > 0 &&\n target.value[pos - 1] === ke.key\n ) {\n target.value = target.value.slice(0, pos - 1) + decimalSeparator + target.value.slice(pos)\n }\n\n // Typing a digit into a field whose integer part isn't yet full of\n // real digits — the auto-inserted \"0\" placeholder, or a\n // `numberPlaces`-padded segment with fewer real digits than its width\n // — extends the real digit stream instead of combining with a\n // padding zero (e.g. \"$0.00\" + \"2\" → \"$2.00\", \"02:00\" + \"4\" →\n // \"24:00\"). A segment already full of real digits (e.g. \"23:00\")\n // falls through to the default mask, which drops the keystroke.\n const replaced = isDigitKey(ke.key)\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, ke.key, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mother-mask.cjs","names":["isDigitChar","MASKED_ATTR"],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts","../src/decimal-mask.ts","../src/bind-decimal.ts"],"sourcesContent":["import type { ApplyMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction isSlotChar(ch: string): boolean {\n return ch === '9' || ch === 'Z' || ch === 'A'\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Flat masking (default) — treats the mask as one continuous character\n// stream. Best for continuous identifiers (phone numbers, CPF/CNPJ, credit\n// cards) where deleting/inserting a digit anywhere is expected to reflow\n// every digit after it — this is the classic mother-mask behavior and is\n// relied on by the majority of the test suite (paste, backspace, mid-string\n// insert, etc).\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nfunction applyFlatMask(value: string, mask: string, inputCaret: number): MaskResult {\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (opt-in) — treats literal separators as hard boundaries\n// between independent fields (e.g. day/month/year in \"99/99/9999\"). Editing\n// one segment never bleeds characters into a neighboring one, so replacing\n// the \"12\" in \"25/12/2025\" with a shorter or longer value keeps the year\n// exactly where it is instead of shifting digits across the \"/\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n// A bound input re-applies the same (static) mask string on every keystroke,\n// so tokenizing it is pure, repeated work — cache by mask string instead of\n// re-walking and re-allocating tokens on every keystroke.\nconst tokenCache = new Map<string, MaskToken[]>()\n\n/**\n * Split a mask into alternating literal and slot-run tokens (e.g. \"99/99/9999\"\n * → slots\"99\", literal\"/\", slots\"99\", literal\"/\", slots\"9999\"), so the masking\n * pass can reason about segment boundaries instead of a flat character stream.\n */\nfunction tokenizeMask(mask: string): MaskToken[] {\n const cached = tokenCache.get(mask)\n if (cached) return cached\n\n const tokens: MaskToken[] = []\n let i = 0\n while (i < mask.length) {\n const start = i\n const wantSlots = isSlotChar(mask[i])\n while (i < mask.length && isSlotChar(mask[i]) === wantSlots) i++\n const text = mask.slice(start, i)\n tokens.push(wantSlots ? { kind: 'slots', chars: text } : { kind: 'literal', text })\n }\n tokenCache.set(mask, tokens)\n return tokens\n}\n\n/** Total slot capacity from token index `from` (inclusive) to the end of `tokens`. */\nfunction slotCapacityFrom(tokens: MaskToken[], from: number): number {\n let capacity = 0\n for (let i = from; i < tokens.length; i++) {\n const token = tokens[i]\n if (token.kind === 'slots') capacity += token.chars.length\n }\n return capacity\n}\n\n/** Count of remaining slot-matchable (digit/letter) characters in `value` from `fromIdx` onward. */\nfunction remainingDataChars(value: string, fromIdx: number): number {\n let count = 0\n for (let i = fromIdx; i < value.length; i++) {\n const ch = value[i]\n if (isDigitChar(ch) || isLetterChar(ch)) count++\n }\n return count\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but walks the mask one *segment* at\n * a time (a run of slots, or a literal) rather than one character at a time.\n * The rule that keeps an edit inside a segment from crossing into its\n * neighbor is an **early stop**: if the value hits the literal that ends the\n * current slot run before all of that run's slots are filled (e.g. only one\n * digit typed into a two-digit month slot), the run is left partially filled\n * instead of skipping past the separator to steal a digit from the next\n * segment.\n *\n * That stop is only taken when it's actually safe — i.e. every character\n * still to come in `value` fits in the slot capacity that remains *after*\n * this segment. If stopping here would strand more data than the rest of\n * the mask can hold (e.g. pasting into a later segment while an earlier one\n * still sits under-filled from before), the \"separator\" is treated as stray\n * noise instead and skipped, letting this segment take the extra slot it\n * needs so nothing at the end gets silently dropped. This is also what lets\n * an array mask grow or shrink its pattern (moving every literal after the\n * change point) reflow correctly instead of losing a digit at the boundary.\n */\nfunction applySegmentedMask(value: string, mask: string, inputCaret: number): MaskResult {\n const tokens = tokenizeMask(mask)\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n let exhausted = false\n\n for (let t = 0; t < tokens.length && !exhausted; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n pending += token.text\n if (value.startsWith(token.text, valueIdx)) valueIdx += token.text.length\n continue\n }\n\n const nextToken = tokens[t + 1]\n const capacityAfter = slotCapacityFrom(tokens, t + 1)\n\n for (let s = 0; s < token.chars.length; s++) {\n const slotCh = token.chars[s]\n let found = false\n\n while (valueIdx < value.length) {\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, slotCh)) {\n valueIdx++\n output += pending + ch\n pending = ''\n found = true\n\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n\n // This segment's ending separator showed up before the run filled.\n // Stopping here is only safe if the rest of the value still fits in\n // whatever slot capacity remains after this segment — otherwise\n // stopping would strand data, so fall through and skip this char as\n // noise instead, letting the segment take the slot it needs.\n if (\n nextToken?.kind === 'literal' &&\n value.startsWith(nextToken.text, valueIdx) &&\n remainingDataChars(value, valueIdx) <= capacityAfter\n ) {\n break\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n if (!found) {\n if (valueIdx >= value.length) exhausted = true\n break\n }\n }\n }\n\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\nexport function applyMask(\n value: string,\n mask: string,\n inputCaret = 0,\n options?: ApplyMaskOptions,\n): MaskResult {\n if (!value) return { value: '', caret: 0 }\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret)\n : applySegmentedMask(value, mask, inputCaret)\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { ApplyMaskOptions, MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n private readonly _options: ApplyMaskOptions | undefined\n\n constructor(value: string, mask: string, caret = 0, options?: ApplyMaskOptions) {\n this._value = value\n this._mask = mask\n this.caret = caret\n this._options = options\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret, this._options)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(\n value: string,\n mask: MaskPattern,\n caret = 0,\n options?: ApplyMaskOptions,\n): Mask {\n return new Mask(value, resolveMask(value, mask), caret, options)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string {\n return buildMask(value, mask, 0, options).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, segmented } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n // Computed once here rather than inside `onKey` — the mask never changes\n // for the lifetime of this binding, and `onKey` is a synchronous, hot,\n // input-blocking path run on every keystroke.\n const maxLength = getMaxLength(mask)\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(maxLength))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n const m = buildMask(target.value, mask, 0, { segmented })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= maxLength && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n","import type { DecimalMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — mirrors apply-mask.ts)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\n// ---------------------------------------------------------------------------\n// Option resolution\n// ---------------------------------------------------------------------------\n\ninterface ResolvedDecimalOptions {\n /**\n * `undefined` means an optional, uncapped fraction (default) — the\n * decimal separator and fraction only appear once the user actually types\n * them, and there's no limit on how many digits follow. `0` means no\n * fraction at all. A positive number is a fixed, zero-padded width that's\n * always shown, even before the user types anything.\n */\n decimalPlaces: number | undefined\n /** `undefined` means unlimited (default) — the integer part grows freely. */\n numberPlaces: number | undefined\n segmented: boolean\n separator: string\n decimalSeparator: string\n prefix: string\n suffix: string\n allowNegative: boolean\n}\n\n/** @internal exported for {@link bindDecimal}'s \".\" / \",\" key normalization */\nexport function resolveDecimalOptions(options?: DecimalMaskOptions): ResolvedDecimalOptions {\n const rawPlaces = options?.decimalPlaces\n const decimalPlaces =\n rawPlaces != null && Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : undefined\n const rawNumberPlaces = options?.numberPlaces\n const numberPlaces =\n rawNumberPlaces != null && Number.isFinite(rawNumberPlaces)\n ? Math.max(1, Math.floor(rawNumberPlaces))\n : undefined\n return {\n decimalPlaces,\n numberPlaces,\n segmented: options?.segmented ?? true,\n separator: options?.separator ?? ',',\n decimalSeparator: options?.decimalSeparator ?? '.',\n prefix: options?.prefix ?? '',\n suffix: options?.suffix ?? '',\n allowNegative: options?.allowNegative ?? false,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Digit-stream helpers\n// ---------------------------------------------------------------------------\n\n/** Strip leading zeros from a digit string, always keeping at least one digit. */\nfunction stripLeadingZeros(s: string): string {\n let i = 0\n while (i < s.length - 1 && s[i] === '0') i++\n return s.slice(i)\n}\n\n/** Insert `sep` every 3 digits from the right (e.g. \"1234567\" → \"1,234,567\"). */\nfunction groupThousands(s: string, sep: string): string {\n if (!sep || s.length <= 3) return s\n const parts: string[] = []\n let i = s.length\n while (i > 3) {\n parts.unshift(s.slice(i - 3, i))\n i -= 3\n }\n parts.unshift(s.slice(0, i))\n return parts.join(sep)\n}\n\n/**\n * Find the position in `s` that leaves exactly `digitsBefore` digit\n * characters preceding it — the position immediately after that digit and\n * before any subsequent literal (grouping separator, ...), so the caret\n * stays glued to the last digit the user placed there.\n */\nfunction caretForDigitsBefore(s: string, digitsBefore: number): number {\n if (digitsBefore <= 0) return 0\n let count = 0\n for (let i = 0; i < s.length; i++) {\n if (isDigitChar(s[i])) {\n count++\n if (count === digitsBefore) return i + 1\n }\n }\n return s.length\n}\n\n// ---------------------------------------------------------------------------\n// Segmented parsing — integer digits before the decimal separator, fraction\n// digits after. Unlike a slot-pattern mask, the integer segment has no fixed\n// length. The fraction is only fixed-width when `decimalPlaces` is set to a\n// positive number — zero-padded on the right so a shorter fraction reads as\n// its low-order (trailing) digits being zero rather than reflowing/shifting\n// (e.g. editing \"423,42\" down to \"423,4\" produces \"423,40\", not \"42,34\").\n// When `decimalPlaces` is left unset, the fraction is optional and uncapped:\n// it only exists once the user types the separator, and grows to however\n// many digits they type.\n//\n// The decimal separator only has meaning as the *first* occurrence of\n// `opts.decimalSeparator`; every other non-digit character (thousands\n// separator, prefix/suffix text, a second stray separator, ...) is noise and\n// is dropped. This keeps re-parsing an already-masked value idempotent.\n// ---------------------------------------------------------------------------\n\ninterface DecimalParts {\n isNegative: boolean\n intDigits: string\n fracDigits: string\n hasSeparator: boolean\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n let intDigits = ''\n let fracDigits = ''\n let isNegative = false\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces !== 0\n\n for (const ch of raw) {\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else if (opts.numberPlaces == null || intDigits.length < opts.numberPlaces) {\n intDigits += ch\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n continue\n }\n if (ch === '-' && opts.allowNegative) isNegative = true\n // Anything else — thousands separator, prefix/suffix text, a repeated\n // separator, stray letters — is noise and is dropped.\n }\n\n return { isNegative, intDigits, fracDigits, hasSeparator: inFraction }\n}\n\n/**\n * Walk `raw[0:caret]` to find which segment the caret sits in (integer or\n * fraction) and how many digits of that segment precede it, so the same\n * position can be re-derived in the freshly formatted output.\n */\nfunction locateCaretSegment(\n raw: string,\n caret: number,\n opts: ResolvedDecimalOptions,\n): { inFraction: boolean; digitsBefore: number } {\n const canHaveFraction = opts.decimalPlaces !== 0\n let inFraction = false\n let digitsBefore = 0\n\n for (let i = 0; i < caret; i++) {\n const ch = raw[i]\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || digitsBefore < opts.decimalPlaces) digitsBefore++\n } else if (opts.numberPlaces == null || digitsBefore < opts.numberPlaces) {\n digitsBefore++\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n digitsBefore = 0\n }\n }\n\n return { inFraction, digitsBefore }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry points\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a decimal/currency mask to a value, producing the masked output and\n * a computed caret position. Digits typed before the decimal separator\n * extend the integer part; the fraction only starts once the separator is\n * typed. With a fixed `decimalPlaces` it's always displayed zero-padded to\n * that width, even before the user types it; left unset, the fraction is\n * optional — it only appears once the separator is typed, and is shown\n * exactly as typed (no padding, no cap on how many digits).\n */\nexport function applyDecimalMask(\n value: string,\n inputCaret = 0,\n options?: DecimalMaskOptions,\n): MaskResult {\n const opts = resolveDecimalOptions(options)\n if (!value) return { value: '', caret: 0 }\n\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (intDigits === '' && fracDigits === '' && !hasSeparator) return { value: '', caret: 0 }\n\n const intPart = stripLeadingZeros(intDigits || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const fracPadded =\n opts.decimalPlaces != null && opts.decimalPlaces > 0\n ? fracDigits.padEnd(opts.decimalPlaces, '0')\n : fracDigits\n const showFraction = opts.decimalPlaces === 0 ? false : opts.decimalPlaces != null || hasSeparator\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPadded : '')\n const signStr = isNegative ? '-' : ''\n const output = signStr + opts.prefix + numberStr + opts.suffix\n\n const clampedCaret = Math.max(0, Math.min(inputCaret, value.length))\n const { inFraction, digitsBefore } = locateCaretSegment(value, clampedCaret, opts)\n const prefixLen = signStr.length + opts.prefix.length\n // Left-padding zeros are synthetic — prepended ahead of every real typed\n // digit — so they shift where the caret's `digitsBefore`-th real digit\n // lands in `groupedInt` by the padding's width.\n const padLength = paddedInt.length - intPart.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore + padLength)\n\n return { value: output, caret }\n}\n\n/** Apply a decimal mask to a raw value and return just the masked string. */\nexport function processDecimal(value: string, options?: DecimalMaskOptions): string {\n return applyDecimalMask(value, value.length, options).value\n}\n\n/**\n * Parse a raw or already-masked decimal value back into a JS number.\n * Ignores prefix/suffix/thousands separator; returns `0` for an empty or\n * digit-less value.\n */\nexport function unmaskDecimal(value: string, options?: DecimalMaskOptions): number {\n const opts = resolveDecimalOptions(options)\n const { isNegative, intDigits, fracDigits } = computeDecimalParts(value, opts)\n const n = Number(fracDigits ? `${intDigits || '0'}.${fracDigits}` : intDigits || '0')\n return isNegative ? -n : n\n}\n\n/**\n * After a Backspace removes the decimal separator itself, the integer and\n * fraction digit runs collapse into one continuous stream (e.g. \"25.00\"\n * with the caret right after \".\" → Backspace deletes the \".\" → \"2500\").\n * Left alone, that reads as one big integer (\"$2,500.00\"). This restores\n * the segment boundary instead: the trailing `decimalPlaces` digits are\n * still the fraction, and the digit right before them — the one that used\n * to sit at the end of the integer part — is the one Backspace actually\n * removed, so it's dropped (not kept) — \"$25.00\" → \"$2.00\".\n *\n * Only applies when `decimalPlaces` is a fixed positive number — that's\n * what \"the trailing N digits are the fraction\" means. Every reformat\n * re-appends `decimalSeparator` in that case, so its absence from `value`\n * is an unambiguous signal that this exact keystroke just deleted it — no\n * \"value before this keystroke\" snapshot is needed. With `decimalPlaces`\n * unset (optional, uncapped fraction) there's no fixed width to reconstruct\n * from, so the merged digits are left as one continuous integer instead —\n * the same reasoning `numberPlaces` uses for an unbounded integer part.\n * Returns `null` when there's nothing to restore (the separator is still\n * present, `decimalPlaces` is `0` or unset, or too few digits remain), so\n * the caller falls through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskUnmergingSeparator(\n value: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n if (opts.decimalPlaces == null || opts.decimalPlaces <= 0) return null\n\n const { isNegative, intDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (hasSeparator || intDigits.length < opts.decimalPlaces + 1) return null\n\n const preMergeIntLength = intDigits.length - opts.decimalPlaces\n const fracDigits = intDigits.slice(preMergeIntLength)\n const remainingInt = intDigits.slice(0, preMergeIntLength - 1)\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + remainingInt + opts.decimalSeparator + fracDigits\n return applyDecimalMask(raw, signPart.length + remainingInt.length, opts)\n}\n\n/**\n * Special-cases typing a single digit into an integer segment that isn't\n * yet full of *real* digits — either because it's still the auto-inserted\n * zero placeholder (\"0\", or a wider \"00\" from a `numberPlaces`-padded field\n * that hasn't been touched), or because `numberPlaces` is left-padding a\n * partially-typed segment with synthetic zeros (e.g. \"02\" is really just\n * the one real digit \"2\", padded out to width 2 for display). Those padding\n * zeros aren't editable content, so the new digit extends the real digit\n * stream instead of combining with a padding zero at the caret:\n *\n * - \"$0.00\" + \"2\" → \"$2.00\" (not \"$20.00\")\n * - a `numberPlaces: 2` time field's untouched \"00:00\" + \"5\" → \"05:00\"\n * - that same field's \"02:00\" (one real digit, one padding zero) + \"4\" →\n * \"24:00\" — the real \"2\" is kept, the padding \"0\" is not\n *\n * A segment that's already full of real digits — e.g. \"23:00\", both digits\n * genuinely typed — doesn't match here and falls through to the default\n * {@link applyDecimalMask}, which already drops the overflow keystroke\n * (typing a 3rd real digit leaves \"23:00\" unchanged).\n *\n * `value`/`caret` must be the state *after* the browser has already\n * inserted `digit` at `caret - 1` (the same post-insertion snapshot\n * `applyDecimalMask` itself expects from `bindDecimal`). Returns `null`\n * when the pattern doesn't apply, so the caller falls through to a plain\n * {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskReplacingLoneZero(\n value: string,\n caret: number,\n digit: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n const insertIdx = caret - 1\n if (insertIdx < 0 || value[insertIdx] !== digit) return null\n\n const withoutDigit = value.slice(0, insertIdx) + value.slice(caret)\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(withoutDigit, opts)\n\n const realDigits = stripLeadingZeros(intDigits || '0')\n const hasRealDigits = realDigits !== '0'\n const hasPaddingRoom =\n hasRealDigits && opts.numberPlaces != null && realDigits.length < opts.numberPlaces\n if (hasRealDigits && !hasPaddingRoom) return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + intDigits.length) return null\n\n const signPart = isNegative ? '-' : ''\n const newIntDigits = (hasRealDigits ? realDigits : '') + digit\n const raw = signPart + newIntDigits + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + newIntDigits.length, opts)\n}\n\n/**\n * Format a plain JS number into its masked display string. With a fixed\n * `decimalPlaces` the fraction is rounded/padded to that exact width, even\n * for a whole number; left unset, the fraction is only shown when the value\n * actually has one, with as many digits as `value` naturally carries (no\n * padding, no rounding).\n */\nexport function formatDecimalValue(value: number, options?: DecimalMaskOptions): string {\n const opts = resolveDecimalOptions(options)\n if (!Number.isFinite(value)) return ''\n\n const isNegative = opts.allowNegative && value < 0\n const abs = Math.abs(value)\n const fixed = opts.decimalPlaces != null ? abs.toFixed(opts.decimalPlaces) : String(abs)\n const dotIdx = fixed.indexOf('.')\n const intRaw = dotIdx === -1 ? fixed : fixed.slice(0, dotIdx)\n const fracPart = dotIdx === -1 ? '' : fixed.slice(dotIdx + 1)\n const intPart = stripLeadingZeros(intRaw || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const showFraction = opts.decimalPlaces === 0 ? false : fracPart !== ''\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPart : '')\n\n return (isNegative ? '-' : '') + opts.prefix + numberStr + opts.suffix\n}\n","import {\n applyDecimalMask,\n applyDecimalMaskReplacingLoneZero,\n applyDecimalMaskUnmergingSeparator,\n resolveDecimalOptions,\n unmaskDecimal,\n} from './decimal-mask'\nimport { isIos } from './platform'\nimport type { BindDecimalOptions, DecimalMaskOptions, MaskResult } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindDecimalOptions(\n second:\n | BindDecimalOptions\n | ((value: string, numericValue: number) => void)\n | null\n | undefined,\n): BindDecimalOptions {\n if (second == null) return {}\n if (typeof second === 'function') return { onChange: second }\n return second\n}\n\nfunction isDigitKey(key: string): boolean {\n return key.length === 1 && key >= '0' && key <= '9'\n}\n\n/**\n * Bind a decimal/currency mask to an input element.\n *\n * Same contract as {@link bind}: idempotent (marked with `data-masked`),\n * returns a dispose function, and reformats on paste and keyboard-driven\n * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no\n * fixed pattern — the integer part grows and shrinks freely; formatting is\n * driven entirely by `options`.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.\n */\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n options?: BindDecimalOptions | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n onChange: ((value: string, numericValue: number) => void) | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n second?: BindDecimalOptions | ((value: string, numericValue: number) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, ...maskOptions } = toBindDecimalOptions(second)\n const decimalOptions: DecimalMaskOptions = maskOptions\n const { decimalSeparator, decimalPlaces } = resolveDecimalOptions(decimalOptions)\n\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, 'decimal')\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n\n let lockInput = false\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n target.setSelectionRange(m.caret, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n applyResult(target, applyDecimalMask(target.value, target.value.length, decimalOptions))\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n\n // Navigation (arrows, Home/End, Tab, ...), selection, and shortcut keys\n // (Ctrl/Cmd+A, Ctrl/Cmd+C, ...) don't change the text — leave the\n // browser's native caret/selection handling alone instead of recomputing\n // and overwriting it on every keystroke.\n if (!isBackspace && !isDelete && !isCharInsert) return\n\n // Everything below reads `target.value`/`selectionStart` inside the rAF\n // callback rather than synchronously here, since the browser's native\n // character insertion for this keystroke isn't guaranteed to have landed\n // yet at the point a keydown listener runs — only by the next frame.\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n\n // Backspace that just deleted the decimal separator merges the\n // integer and fraction digit runs into one continuous stream —\n // restore the boundary instead of treating that as one big integer.\n if (isBackspace) {\n const unmerged = applyDecimalMaskUnmergingSeparator(target.value, decimalOptions)\n if (unmerged) {\n applyResult(target, unmerged)\n return\n }\n }\n\n // A numeric keypad (or a locale mismatch) may only offer \".\" or \",\".\n // Normalize whichever one the user just typed to the configured\n // `decimalSeparator` so it reliably opens the fraction segment.\n if (\n decimalPlaces !== 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator &&\n pos > 0 &&\n target.value[pos - 1] === ke.key\n ) {\n target.value = target.value.slice(0, pos - 1) + decimalSeparator + target.value.slice(pos)\n }\n\n // Typing a digit into a field whose integer part isn't yet full of\n // real digits — the auto-inserted \"0\" placeholder, or a\n // `numberPlaces`-padded segment with fewer real digits than its width\n // — extends the real digit stream instead of combining with a\n // padding zero (e.g. \"$0.00\" + \"2\" → \"$2.00\", \"02:00\" + \"4\" →\n // \"24:00\"). A segment already full of real digits (e.g. \"23:00\")\n // falls through to the default mask, which drops the keystroke.\n const replaced = isDigitKey(ke.key)\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, ke.key, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n"],"mappings":"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"}
|
package/dist/mother-mask.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(e){return e===`9`||e===`Z`||e===`A`}function r(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function i(e,t,n){let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(r(t,u)){i+=a+t,a=``,d=!0,c||(o<=n?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}const a=new Map;function o(e){let t=a.get(e);if(t)return t;let r=[],i=0;for(;i<e.length;){let t=i,a=n(e[i]);for(;i<e.length&&n(e[i])===a;)i++;let o=e.slice(t,i);r.push(a?{kind:`slots`,chars:o}:{kind:`literal`,text:o})}return a.set(e,r),r}function s(e,t){let n=0;for(let r=t;r<e.length;r++){let t=e[r];t.kind===`slots`&&(n+=t.chars.length)}return n}function c(n,r){let i=0;for(let a=r;a<n.length;a++){let r=n[a];(e(r)||t(r))&&i++}return i}function l(e,t,n){let i=o(t),a=``,l=``,u=0,d=0,f=!1,p=!1;for(let t=0;t<i.length&&!p;t++){let o=i[t];if(o.kind===`literal`){l+=o.text,e.startsWith(o.text,u)&&(u+=o.text.length);continue}let m=i[t+1],h=s(i,t+1);for(let t=0;t<o.chars.length;t++){let i=o.chars[t],s=!1;for(;u<e.length;){let t=e[u];if(r(t,i)){u++,a+=l+t,l=``,s=!0,f||(u<=n?d=a.length:f=!0);break}if(m?.kind===`literal`&&e.startsWith(m.text,u)&&c(e,u)<=h)break;u++}if(!s){u>=e.length&&(p=!0);break}}}return f||(d=a.length),{value:a,caret:d}}function u(e,t,n=0,r){return e?r?.segmented===!1?i(e,t,n):l(e,t,n):{value:``,caret:0}}function d(e){let t=0;for(let n of e)(n>=`0`&&n<=`9`||n>=`a`&&n<=`z`||n>=`A`&&n<=`Z`)&&t++;return t}function f(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function p(e,t){if(!Array.isArray(t))return t;let n=d(e),r=0;for(;r<t.length-1&&n>f(t[r]);)r++;return t[r]}function m(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var h=class{caret;_value;_mask;_options;constructor(e,t,n=0,r){this._value=e,this._mask=t,this.caret=n,this._options=r}process(){let e=u(this._value,this._mask,this.caret,this._options);return this.caret=e.caret,e.value}};function g(e,t,n=0,r){return new h(e,p(e,t),n,r)}function _(e,t,n){return g(e,t,0,n).process()}let v;function y(){return v===void 0&&(v=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),v}const b=`data-masked`;function x(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function S(e,t,n){if(e.getAttribute(b)!==null)return()=>{};let{onChange:r,segmented:i}=x(n),a=[],o=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),a.push(t))},s=m(t);e.setAttribute(b,Array.isArray(t)?t.join(`|`):t),o(`autocomplete`,`off`),o(`autocorrect`,`off`),o(`autocapitalize`,`off`),o(`spellcheck`,`false`),o(`maxlength`,String(s));let c=!1,l=y()?`keyup`:`keydown`,u=new Set,d=e=>{let t=requestAnimationFrame(()=>{u.delete(t),e()});u.add(t)},f=e=>{let n=e.target;d(()=>{
|
|
1
|
+
function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(e){return e===`9`||e===`Z`||e===`A`}function r(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function i(e,t,n){let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(r(t,u)){i+=a+t,a=``,d=!0,c||(o<=n?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}const a=new Map;function o(e){let t=a.get(e);if(t)return t;let r=[],i=0;for(;i<e.length;){let t=i,a=n(e[i]);for(;i<e.length&&n(e[i])===a;)i++;let o=e.slice(t,i);r.push(a?{kind:`slots`,chars:o}:{kind:`literal`,text:o})}return a.set(e,r),r}function s(e,t){let n=0;for(let r=t;r<e.length;r++){let t=e[r];t.kind===`slots`&&(n+=t.chars.length)}return n}function c(n,r){let i=0;for(let a=r;a<n.length;a++){let r=n[a];(e(r)||t(r))&&i++}return i}function l(e,t,n){let i=o(t),a=``,l=``,u=0,d=0,f=!1,p=!1;for(let t=0;t<i.length&&!p;t++){let o=i[t];if(o.kind===`literal`){l+=o.text,e.startsWith(o.text,u)&&(u+=o.text.length);continue}let m=i[t+1],h=s(i,t+1);for(let t=0;t<o.chars.length;t++){let i=o.chars[t],s=!1;for(;u<e.length;){let t=e[u];if(r(t,i)){u++,a+=l+t,l=``,s=!0,f||(u<=n?d=a.length:f=!0);break}if(m?.kind===`literal`&&e.startsWith(m.text,u)&&c(e,u)<=h)break;u++}if(!s){u>=e.length&&(p=!0);break}}}return f||(d=a.length),{value:a,caret:d}}function u(e,t,n=0,r){return e?r?.segmented===!1?i(e,t,n):l(e,t,n):{value:``,caret:0}}function d(e){let t=0;for(let n of e)(n>=`0`&&n<=`9`||n>=`a`&&n<=`z`||n>=`A`&&n<=`Z`)&&t++;return t}function f(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function p(e,t){if(!Array.isArray(t))return t;let n=d(e),r=0;for(;r<t.length-1&&n>f(t[r]);)r++;return t[r]}function m(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var h=class{caret;_value;_mask;_options;constructor(e,t,n=0,r){this._value=e,this._mask=t,this.caret=n,this._options=r}process(){let e=u(this._value,this._mask,this.caret,this._options);return this.caret=e.caret,e.value}};function g(e,t,n=0,r){return new h(e,p(e,t),n,r)}function _(e,t,n){return g(e,t,0,n).process()}let v;function y(){return v===void 0&&(v=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),v}const b=`data-masked`;function x(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function S(e,t,n){if(e.getAttribute(b)!==null)return()=>{};let{onChange:r,segmented:i}=x(n),a=[],o=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),a.push(t))},s=m(t);e.setAttribute(b,Array.isArray(t)?t.join(`|`):t),o(`autocomplete`,`off`),o(`autocorrect`,`off`),o(`autocapitalize`,`off`),o(`spellcheck`,`false`),o(`maxlength`,String(s));let c=!1,l=y()?`keyup`:`keydown`,u=new Set,d=e=>{let t=requestAnimationFrame(()=>{u.delete(t),e()});u.add(t)},f=e=>{let n=e.target;d(()=>{let e=g(n.value,t,0,{segmented:i});n.value=e.process(),r?.(n.value)})},p=e=>{let n=e,a=n.target,o=a.value;if(!n.key){c=!0,d(()=>{let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});a.value=n.process(),a.setSelectionRange(n.caret,n.caret),d(()=>{c=!1})});return}if(n.key===`Meta`)return;let l=n.key===`Backspace`,u=n.key===`Delete`,f=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,p=n.key===`Unidentified`;if(f&&a.selectionStart===a.selectionEnd&&o.length>=s&&!y()){n.preventDefault();return}if(c){n.preventDefault();return}d(()=>{let e=a.selectionStart??999,n=g(a.value,t,e,{segmented:i});if(a.value=n.process(),p){let t=a.value.length>o.length?n.caret:e;a.setSelectionRange(t,t)}else if(u){let t=o.length===a.value.length?e+1:e;a.setSelectionRange(t,t)}else l?a.setSelectionRange(e,e):f&&a.setSelectionRange(n.caret,n.caret);r?.(a.value)})};return e.addEventListener(`paste`,f),e.addEventListener(l,p),()=>{e.removeEventListener(`paste`,f),e.removeEventListener(l,p),e.removeAttribute(b);for(let t of a)e.removeAttribute(t);for(let e of u)cancelAnimationFrame(e);u.clear()}}function C(e){return e>=`0`&&e<=`9`}function w(e){let t=e?.decimalPlaces,n=t!=null&&Number.isFinite(t)?Math.max(0,Math.floor(t)):void 0,r=e?.numberPlaces;return{decimalPlaces:n,numberPlaces:r!=null&&Number.isFinite(r)?Math.max(1,Math.floor(r)):void 0,segmented:e?.segmented??!0,separator:e?.separator??`,`,decimalSeparator:e?.decimalSeparator??`.`,prefix:e?.prefix??``,suffix:e?.suffix??``,allowNegative:e?.allowNegative??!1}}function T(e){let t=0;for(;t<e.length-1&&e[t]===`0`;)t++;return e.slice(t)}function E(e,t){if(!t||e.length<=3)return e;let n=[],r=e.length;for(;r>3;)n.unshift(e.slice(r-3,r)),r-=3;return n.unshift(e.slice(0,r)),n.join(t)}function D(e,t){if(t<=0)return 0;let n=0;for(let r=0;r<e.length;r++)if(C(e[r])&&(n++,n===t))return r+1;return e.length}function O(e,t){let n=``,r=``,i=!1,a=!1,o=t.decimalPlaces!==0;for(let s of e){if(C(s)){a?(t.decimalPlaces==null||r.length<t.decimalPlaces)&&(r+=s):(t.numberPlaces==null||n.length<t.numberPlaces)&&(n+=s);continue}if(o&&!a&&s===t.decimalSeparator){a=!0;continue}s===`-`&&t.allowNegative&&(i=!0)}return{isNegative:i,intDigits:n,fracDigits:r,hasSeparator:a}}function k(e,t,n){let r=n.decimalPlaces!==0,i=!1,a=0;for(let o=0;o<t;o++){let t=e[o];if(C(t)){i?(n.decimalPlaces==null||a<n.decimalPlaces)&&a++:(n.numberPlaces==null||a<n.numberPlaces)&&a++;continue}r&&!i&&t===n.decimalSeparator&&(i=!0,a=0)}return{inFraction:i,digitsBefore:a}}function A(e,t=0,n){let r=w(n);if(!e)return{value:``,caret:0};let{isNegative:i,intDigits:a,fracDigits:o,hasSeparator:s}=O(e,r);if(a===``&&o===``&&!s)return{value:``,caret:0};let c=T(a||`0`),l=r.numberPlaces==null?c:c.padStart(r.numberPlaces,`0`),u=r.segmented?E(l,r.separator):l,d=r.decimalPlaces!=null&&r.decimalPlaces>0?o.padEnd(r.decimalPlaces,`0`):o,f=u+(r.decimalPlaces!==0&&(r.decimalPlaces!=null||s)?r.decimalSeparator+d:``),p=i?`-`:``,m=p+r.prefix+f+r.suffix,{inFraction:h,digitsBefore:g}=k(e,Math.max(0,Math.min(t,e.length)),r),_=p.length+r.prefix.length,v=l.length-c.length;return{value:m,caret:h?_+u.length+r.decimalSeparator.length+g:_+D(u,g+v)}}function j(e,t){return A(e,e.length,t).value}function M(e,t){let{isNegative:n,intDigits:r,fracDigits:i}=O(e,w(t)),a=Number(i?`${r||`0`}.${i}`:r||`0`);return n?-a:a}function N(e,t){let n=w(t);if(n.decimalPlaces==null||n.decimalPlaces<=0)return null;let{isNegative:r,intDigits:i,hasSeparator:a}=O(e,n);if(a||i.length<n.decimalPlaces+1)return null;let o=i.length-n.decimalPlaces,s=i.slice(o),c=i.slice(0,o-1),l=r?`-`:``;return A(l+c+n.decimalSeparator+s,l.length+c.length,n)}function P(e,t,n,r){let i=w(r),a=t-1;if(a<0||e[a]!==n)return null;let{isNegative:o,intDigits:s,fracDigits:c,hasSeparator:l}=O(e.slice(0,a)+e.slice(t),i),u=T(s||`0`),d=u!==`0`,f=d&&i.numberPlaces!=null&&u.length<i.numberPlaces;if(d&&!f)return null;let p=+!!o+i.prefix.length;if(a<p||a>p+s.length)return null;let m=o?`-`:``,h=(d?u:``)+n;return A(m+h+(l?i.decimalSeparator+c:``),m.length+h.length,i)}function F(e,t){let n=w(t);if(!Number.isFinite(e))return``;let r=n.allowNegative&&e<0,i=Math.abs(e),a=n.decimalPlaces==null?String(i):i.toFixed(n.decimalPlaces),o=a.indexOf(`.`),s=o===-1?a:a.slice(0,o),c=o===-1?``:a.slice(o+1),l=T(s||`0`),u=n.numberPlaces==null?l:l.padStart(n.numberPlaces,`0`),d=(n.segmented?E(u,n.separator):u)+(n.decimalPlaces!==0&&c!==``?n.decimalSeparator+c:``);return(r?`-`:``)+n.prefix+d+n.suffix}const I=`data-masked`;function L(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function R(e){return e.length===1&&e>=`0`&&e<=`9`}function z(e,t){if(e.getAttribute(I)!==null)return()=>{};let{onChange:n,...r}=L(t),i=r,{decimalSeparator:a,decimalPlaces:o}=w(i),s=[],c=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),s.push(t))};e.setAttribute(I,`decimal`),c(`autocomplete`,`off`),c(`autocorrect`,`off`),c(`autocapitalize`,`off`),c(`spellcheck`,`false`);let l=!1,u=y()?`keyup`:`keydown`,d=new Set,f=e=>{let t=requestAnimationFrame(()=>{d.delete(t),e()});d.add(t)},p=(e,t)=>{e.value=t.value,e.setSelectionRange(t.caret,t.caret),n?.(t.value,M(t.value,i))},m=e=>{let t=e.target;f(()=>{p(t,A(t.value,t.value.length,i))})},h=e=>{let t=e,n=t.target;if(!t.key){l=!0,f(()=>{let e=n.selectionStart??n.value.length;p(n,A(n.value,e,i)),f(()=>{l=!1})});return}if(t.key===`Meta`)return;if(l){t.preventDefault();return}let r=t.key===`Backspace`,s=t.key===`Delete`,c=t.key.length===1&&!t.ctrlKey&&!t.altKey&&!t.metaKey;!r&&!s&&!c||f(()=>{let e=n.selectionStart??n.value.length;if(r){let e=N(n.value,i);if(e){p(n,e);return}}o!==0&&(t.key===`.`||t.key===`,`)&&t.key!==a&&e>0&&n.value[e-1]===t.key&&(n.value=n.value.slice(0,e-1)+a+n.value.slice(e));let s=R(t.key)?P(n.value,e,t.key,i):null;p(n,s??A(n.value,e,i))})};return e.addEventListener(`paste`,m),e.addEventListener(u,h),()=>{e.removeEventListener(`paste`,m),e.removeEventListener(u,h),e.removeAttribute(I);for(let t of s)e.removeAttribute(t);for(let e of d)cancelAnimationFrame(e);d.clear()}}export{h as Mask,A as applyDecimalMask,u as applyMask,S as bind,z as bindDecimal,g as buildMask,F as formatDecimalValue,m as getMaxLength,_ as process,j as processDecimal,M as unmaskDecimal};
|
|
2
2
|
//# sourceMappingURL=mother-mask.mjs.map
|
package/dist/mother-mask.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mother-mask.mjs","names":["isDigitChar","MASKED_ATTR"],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts","../src/decimal-mask.ts","../src/bind-decimal.ts"],"sourcesContent":["import type { ApplyMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction isSlotChar(ch: string): boolean {\n return ch === '9' || ch === 'Z' || ch === 'A'\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Flat masking (default) — treats the mask as one continuous character\n// stream. Best for continuous identifiers (phone numbers, CPF/CNPJ, credit\n// cards) where deleting/inserting a digit anywhere is expected to reflow\n// every digit after it — this is the classic mother-mask behavior and is\n// relied on by the majority of the test suite (paste, backspace, mid-string\n// insert, etc).\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nfunction applyFlatMask(value: string, mask: string, inputCaret: number): MaskResult {\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (opt-in) — treats literal separators as hard boundaries\n// between independent fields (e.g. day/month/year in \"99/99/9999\"). Editing\n// one segment never bleeds characters into a neighboring one, so replacing\n// the \"12\" in \"25/12/2025\" with a shorter or longer value keeps the year\n// exactly where it is instead of shifting digits across the \"/\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n// A bound input re-applies the same (static) mask string on every keystroke,\n// so tokenizing it is pure, repeated work — cache by mask string instead of\n// re-walking and re-allocating tokens on every keystroke.\nconst tokenCache = new Map<string, MaskToken[]>()\n\n/**\n * Split a mask into alternating literal and slot-run tokens (e.g. \"99/99/9999\"\n * → slots\"99\", literal\"/\", slots\"99\", literal\"/\", slots\"9999\"), so the masking\n * pass can reason about segment boundaries instead of a flat character stream.\n */\nfunction tokenizeMask(mask: string): MaskToken[] {\n const cached = tokenCache.get(mask)\n if (cached) return cached\n\n const tokens: MaskToken[] = []\n let i = 0\n while (i < mask.length) {\n const start = i\n const wantSlots = isSlotChar(mask[i])\n while (i < mask.length && isSlotChar(mask[i]) === wantSlots) i++\n const text = mask.slice(start, i)\n tokens.push(wantSlots ? { kind: 'slots', chars: text } : { kind: 'literal', text })\n }\n tokenCache.set(mask, tokens)\n return tokens\n}\n\n/** Total slot capacity from token index `from` (inclusive) to the end of `tokens`. */\nfunction slotCapacityFrom(tokens: MaskToken[], from: number): number {\n let capacity = 0\n for (let i = from; i < tokens.length; i++) {\n const token = tokens[i]\n if (token.kind === 'slots') capacity += token.chars.length\n }\n return capacity\n}\n\n/** Count of remaining slot-matchable (digit/letter) characters in `value` from `fromIdx` onward. */\nfunction remainingDataChars(value: string, fromIdx: number): number {\n let count = 0\n for (let i = fromIdx; i < value.length; i++) {\n const ch = value[i]\n if (isDigitChar(ch) || isLetterChar(ch)) count++\n }\n return count\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but walks the mask one *segment* at\n * a time (a run of slots, or a literal) rather than one character at a time.\n * The rule that keeps an edit inside a segment from crossing into its\n * neighbor is an **early stop**: if the value hits the literal that ends the\n * current slot run before all of that run's slots are filled (e.g. only one\n * digit typed into a two-digit month slot), the run is left partially filled\n * instead of skipping past the separator to steal a digit from the next\n * segment.\n *\n * That stop is only taken when it's actually safe — i.e. every character\n * still to come in `value` fits in the slot capacity that remains *after*\n * this segment. If stopping here would strand more data than the rest of\n * the mask can hold (e.g. pasting into a later segment while an earlier one\n * still sits under-filled from before), the \"separator\" is treated as stray\n * noise instead and skipped, letting this segment take the extra slot it\n * needs so nothing at the end gets silently dropped. This is also what lets\n * an array mask grow or shrink its pattern (moving every literal after the\n * change point) reflow correctly instead of losing a digit at the boundary.\n */\nfunction applySegmentedMask(value: string, mask: string, inputCaret: number): MaskResult {\n const tokens = tokenizeMask(mask)\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n let exhausted = false\n\n for (let t = 0; t < tokens.length && !exhausted; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n pending += token.text\n if (value.startsWith(token.text, valueIdx)) valueIdx += token.text.length\n continue\n }\n\n const nextToken = tokens[t + 1]\n const capacityAfter = slotCapacityFrom(tokens, t + 1)\n\n for (let s = 0; s < token.chars.length; s++) {\n const slotCh = token.chars[s]\n let found = false\n\n while (valueIdx < value.length) {\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, slotCh)) {\n valueIdx++\n output += pending + ch\n pending = ''\n found = true\n\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n\n // This segment's ending separator showed up before the run filled.\n // Stopping here is only safe if the rest of the value still fits in\n // whatever slot capacity remains after this segment — otherwise\n // stopping would strand data, so fall through and skip this char as\n // noise instead, letting the segment take the slot it needs.\n if (\n nextToken?.kind === 'literal' &&\n value.startsWith(nextToken.text, valueIdx) &&\n remainingDataChars(value, valueIdx) <= capacityAfter\n ) {\n break\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n if (!found) {\n if (valueIdx >= value.length) exhausted = true\n break\n }\n }\n }\n\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\nexport function applyMask(\n value: string,\n mask: string,\n inputCaret = 0,\n options?: ApplyMaskOptions,\n): MaskResult {\n if (!value) return { value: '', caret: 0 }\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret)\n : applySegmentedMask(value, mask, inputCaret)\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { ApplyMaskOptions, MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n private readonly _options: ApplyMaskOptions | undefined\n\n constructor(value: string, mask: string, caret = 0, options?: ApplyMaskOptions) {\n this._value = value\n this._mask = mask\n this.caret = caret\n this._options = options\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret, this._options)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(\n value: string,\n mask: MaskPattern,\n caret = 0,\n options?: ApplyMaskOptions,\n): Mask {\n return new Mask(value, resolveMask(value, mask), caret, options)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string {\n return buildMask(value, mask, 0, options).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, segmented } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n // Computed once here rather than inside `onKey` — the mask never changes\n // for the lifetime of this binding, and `onKey` is a synchronous, hot,\n // input-blocking path run on every keystroke.\n const maxLength = getMaxLength(mask)\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(maxLength))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n const m = buildMask(target.value, mask, 0, { segmented })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= maxLength && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n","import type { DecimalMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — mirrors apply-mask.ts)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\n// ---------------------------------------------------------------------------\n// Option resolution\n// ---------------------------------------------------------------------------\n\ninterface ResolvedDecimalOptions {\n /**\n * `undefined` means an optional, uncapped fraction (default) — the\n * decimal separator and fraction only appear once the user actually types\n * them, and there's no limit on how many digits follow. `0` means no\n * fraction at all. A positive number is a fixed, zero-padded width that's\n * always shown, even before the user types anything.\n */\n decimalPlaces: number | undefined\n /** `undefined` means unlimited (default) — the integer part grows freely. */\n numberPlaces: number | undefined\n segmented: boolean\n separator: string\n decimalSeparator: string\n prefix: string\n suffix: string\n allowNegative: boolean\n}\n\n/** @internal exported for {@link bindDecimal}'s \".\" / \",\" key normalization */\nexport function resolveDecimalOptions(options?: DecimalMaskOptions): ResolvedDecimalOptions {\n const rawPlaces = options?.decimalPlaces\n const decimalPlaces =\n rawPlaces != null && Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : undefined\n const rawNumberPlaces = options?.numberPlaces\n const numberPlaces =\n rawNumberPlaces != null && Number.isFinite(rawNumberPlaces)\n ? Math.max(1, Math.floor(rawNumberPlaces))\n : undefined\n return {\n decimalPlaces,\n numberPlaces,\n segmented: options?.segmented ?? true,\n separator: options?.separator ?? ',',\n decimalSeparator: options?.decimalSeparator ?? '.',\n prefix: options?.prefix ?? '',\n suffix: options?.suffix ?? '',\n allowNegative: options?.allowNegative ?? false,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Digit-stream helpers\n// ---------------------------------------------------------------------------\n\n/** Strip leading zeros from a digit string, always keeping at least one digit. */\nfunction stripLeadingZeros(s: string): string {\n let i = 0\n while (i < s.length - 1 && s[i] === '0') i++\n return s.slice(i)\n}\n\n/** Insert `sep` every 3 digits from the right (e.g. \"1234567\" → \"1,234,567\"). */\nfunction groupThousands(s: string, sep: string): string {\n if (!sep || s.length <= 3) return s\n const parts: string[] = []\n let i = s.length\n while (i > 3) {\n parts.unshift(s.slice(i - 3, i))\n i -= 3\n }\n parts.unshift(s.slice(0, i))\n return parts.join(sep)\n}\n\n/**\n * Find the position in `s` that leaves exactly `digitsBefore` digit\n * characters preceding it — the position immediately after that digit and\n * before any subsequent literal (grouping separator, ...), so the caret\n * stays glued to the last digit the user placed there.\n */\nfunction caretForDigitsBefore(s: string, digitsBefore: number): number {\n if (digitsBefore <= 0) return 0\n let count = 0\n for (let i = 0; i < s.length; i++) {\n if (isDigitChar(s[i])) {\n count++\n if (count === digitsBefore) return i + 1\n }\n }\n return s.length\n}\n\n// ---------------------------------------------------------------------------\n// Segmented parsing — integer digits before the decimal separator, fraction\n// digits after. Unlike a slot-pattern mask, the integer segment has no fixed\n// length. The fraction is only fixed-width when `decimalPlaces` is set to a\n// positive number — zero-padded on the right so a shorter fraction reads as\n// its low-order (trailing) digits being zero rather than reflowing/shifting\n// (e.g. editing \"423,42\" down to \"423,4\" produces \"423,40\", not \"42,34\").\n// When `decimalPlaces` is left unset, the fraction is optional and uncapped:\n// it only exists once the user types the separator, and grows to however\n// many digits they type.\n//\n// The decimal separator only has meaning as the *first* occurrence of\n// `opts.decimalSeparator`; every other non-digit character (thousands\n// separator, prefix/suffix text, a second stray separator, ...) is noise and\n// is dropped. This keeps re-parsing an already-masked value idempotent.\n// ---------------------------------------------------------------------------\n\ninterface DecimalParts {\n isNegative: boolean\n intDigits: string\n fracDigits: string\n hasSeparator: boolean\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n let intDigits = ''\n let fracDigits = ''\n let isNegative = false\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces !== 0\n\n for (const ch of raw) {\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else if (opts.numberPlaces == null || intDigits.length < opts.numberPlaces) {\n intDigits += ch\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n continue\n }\n if (ch === '-' && opts.allowNegative) isNegative = true\n // Anything else — thousands separator, prefix/suffix text, a repeated\n // separator, stray letters — is noise and is dropped.\n }\n\n return { isNegative, intDigits, fracDigits, hasSeparator: inFraction }\n}\n\n/**\n * Walk `raw[0:caret]` to find which segment the caret sits in (integer or\n * fraction) and how many digits of that segment precede it, so the same\n * position can be re-derived in the freshly formatted output.\n */\nfunction locateCaretSegment(\n raw: string,\n caret: number,\n opts: ResolvedDecimalOptions,\n): { inFraction: boolean; digitsBefore: number } {\n const canHaveFraction = opts.decimalPlaces !== 0\n let inFraction = false\n let digitsBefore = 0\n\n for (let i = 0; i < caret; i++) {\n const ch = raw[i]\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || digitsBefore < opts.decimalPlaces) digitsBefore++\n } else if (opts.numberPlaces == null || digitsBefore < opts.numberPlaces) {\n digitsBefore++\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n digitsBefore = 0\n }\n }\n\n return { inFraction, digitsBefore }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry points\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a decimal/currency mask to a value, producing the masked output and\n * a computed caret position. Digits typed before the decimal separator\n * extend the integer part; the fraction only starts once the separator is\n * typed. With a fixed `decimalPlaces` it's always displayed zero-padded to\n * that width, even before the user types it; left unset, the fraction is\n * optional — it only appears once the separator is typed, and is shown\n * exactly as typed (no padding, no cap on how many digits).\n */\nexport function applyDecimalMask(\n value: string,\n inputCaret = 0,\n options?: DecimalMaskOptions,\n): MaskResult {\n const opts = resolveDecimalOptions(options)\n if (!value) return { value: '', caret: 0 }\n\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (intDigits === '' && fracDigits === '' && !hasSeparator) return { value: '', caret: 0 }\n\n const intPart = stripLeadingZeros(intDigits || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const fracPadded =\n opts.decimalPlaces != null && opts.decimalPlaces > 0\n ? fracDigits.padEnd(opts.decimalPlaces, '0')\n : fracDigits\n const showFraction = opts.decimalPlaces === 0 ? false : opts.decimalPlaces != null || hasSeparator\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPadded : '')\n const signStr = isNegative ? '-' : ''\n const output = signStr + opts.prefix + numberStr + opts.suffix\n\n const clampedCaret = Math.max(0, Math.min(inputCaret, value.length))\n const { inFraction, digitsBefore } = locateCaretSegment(value, clampedCaret, opts)\n const prefixLen = signStr.length + opts.prefix.length\n // Left-padding zeros are synthetic — prepended ahead of every real typed\n // digit — so they shift where the caret's `digitsBefore`-th real digit\n // lands in `groupedInt` by the padding's width.\n const padLength = paddedInt.length - intPart.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore + padLength)\n\n return { value: output, caret }\n}\n\n/** Apply a decimal mask to a raw value and return just the masked string. */\nexport function processDecimal(value: string, options?: DecimalMaskOptions): string {\n return applyDecimalMask(value, value.length, options).value\n}\n\n/**\n * Parse a raw or already-masked decimal value back into a JS number.\n * Ignores prefix/suffix/thousands separator; returns `0` for an empty or\n * digit-less value.\n */\nexport function unmaskDecimal(value: string, options?: DecimalMaskOptions): number {\n const opts = resolveDecimalOptions(options)\n const { isNegative, intDigits, fracDigits } = computeDecimalParts(value, opts)\n const n = Number(fracDigits ? `${intDigits || '0'}.${fracDigits}` : intDigits || '0')\n return isNegative ? -n : n\n}\n\n/**\n * After a Backspace removes the decimal separator itself, the integer and\n * fraction digit runs collapse into one continuous stream (e.g. \"25.00\"\n * with the caret right after \".\" → Backspace deletes the \".\" → \"2500\").\n * Left alone, that reads as one big integer (\"$2,500.00\"). This restores\n * the segment boundary instead: the trailing `decimalPlaces` digits are\n * still the fraction, and the digit right before them — the one that used\n * to sit at the end of the integer part — is the one Backspace actually\n * removed, so it's dropped (not kept) — \"$25.00\" → \"$2.00\".\n *\n * Only applies when `decimalPlaces` is a fixed positive number — that's\n * what \"the trailing N digits are the fraction\" means. Every reformat\n * re-appends `decimalSeparator` in that case, so its absence from `value`\n * is an unambiguous signal that this exact keystroke just deleted it — no\n * \"value before this keystroke\" snapshot is needed. With `decimalPlaces`\n * unset (optional, uncapped fraction) there's no fixed width to reconstruct\n * from, so the merged digits are left as one continuous integer instead —\n * the same reasoning `numberPlaces` uses for an unbounded integer part.\n * Returns `null` when there's nothing to restore (the separator is still\n * present, `decimalPlaces` is `0` or unset, or too few digits remain), so\n * the caller falls through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskUnmergingSeparator(\n value: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n if (opts.decimalPlaces == null || opts.decimalPlaces <= 0) return null\n\n const { isNegative, intDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (hasSeparator || intDigits.length < opts.decimalPlaces + 1) return null\n\n const preMergeIntLength = intDigits.length - opts.decimalPlaces\n const fracDigits = intDigits.slice(preMergeIntLength)\n const remainingInt = intDigits.slice(0, preMergeIntLength - 1)\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + remainingInt + opts.decimalSeparator + fracDigits\n return applyDecimalMask(raw, signPart.length + remainingInt.length, opts)\n}\n\n/**\n * Special-cases typing a single digit into an integer segment that isn't\n * yet full of *real* digits — either because it's still the auto-inserted\n * zero placeholder (\"0\", or a wider \"00\" from a `numberPlaces`-padded field\n * that hasn't been touched), or because `numberPlaces` is left-padding a\n * partially-typed segment with synthetic zeros (e.g. \"02\" is really just\n * the one real digit \"2\", padded out to width 2 for display). Those padding\n * zeros aren't editable content, so the new digit extends the real digit\n * stream instead of combining with a padding zero at the caret:\n *\n * - \"$0.00\" + \"2\" → \"$2.00\" (not \"$20.00\")\n * - a `numberPlaces: 2` time field's untouched \"00:00\" + \"5\" → \"05:00\"\n * - that same field's \"02:00\" (one real digit, one padding zero) + \"4\" →\n * \"24:00\" — the real \"2\" is kept, the padding \"0\" is not\n *\n * A segment that's already full of real digits — e.g. \"23:00\", both digits\n * genuinely typed — doesn't match here and falls through to the default\n * {@link applyDecimalMask}, which already drops the overflow keystroke\n * (typing a 3rd real digit leaves \"23:00\" unchanged).\n *\n * `value`/`caret` must be the state *after* the browser has already\n * inserted `digit` at `caret - 1` (the same post-insertion snapshot\n * `applyDecimalMask` itself expects from `bindDecimal`). Returns `null`\n * when the pattern doesn't apply, so the caller falls through to a plain\n * {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskReplacingLoneZero(\n value: string,\n caret: number,\n digit: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n const insertIdx = caret - 1\n if (insertIdx < 0 || value[insertIdx] !== digit) return null\n\n const withoutDigit = value.slice(0, insertIdx) + value.slice(caret)\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(withoutDigit, opts)\n\n const realDigits = stripLeadingZeros(intDigits || '0')\n const hasRealDigits = realDigits !== '0'\n const hasPaddingRoom =\n hasRealDigits && opts.numberPlaces != null && realDigits.length < opts.numberPlaces\n if (hasRealDigits && !hasPaddingRoom) return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + intDigits.length) return null\n\n const signPart = isNegative ? '-' : ''\n const newIntDigits = (hasRealDigits ? realDigits : '') + digit\n const raw = signPart + newIntDigits + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + newIntDigits.length, opts)\n}\n\n/**\n * Format a plain JS number into its masked display string. With a fixed\n * `decimalPlaces` the fraction is rounded/padded to that exact width, even\n * for a whole number; left unset, the fraction is only shown when the value\n * actually has one, with as many digits as `value` naturally carries (no\n * padding, no rounding).\n */\nexport function formatDecimalValue(value: number, options?: DecimalMaskOptions): string {\n const opts = resolveDecimalOptions(options)\n if (!Number.isFinite(value)) return ''\n\n const isNegative = opts.allowNegative && value < 0\n const abs = Math.abs(value)\n const fixed = opts.decimalPlaces != null ? abs.toFixed(opts.decimalPlaces) : String(abs)\n const dotIdx = fixed.indexOf('.')\n const intRaw = dotIdx === -1 ? fixed : fixed.slice(0, dotIdx)\n const fracPart = dotIdx === -1 ? '' : fixed.slice(dotIdx + 1)\n const intPart = stripLeadingZeros(intRaw || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const showFraction = opts.decimalPlaces === 0 ? false : fracPart !== ''\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPart : '')\n\n return (isNegative ? '-' : '') + opts.prefix + numberStr + opts.suffix\n}\n","import {\n applyDecimalMask,\n applyDecimalMaskReplacingLoneZero,\n applyDecimalMaskUnmergingSeparator,\n resolveDecimalOptions,\n unmaskDecimal,\n} from './decimal-mask'\nimport { isIos } from './platform'\nimport type { BindDecimalOptions, DecimalMaskOptions, MaskResult } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindDecimalOptions(\n second:\n | BindDecimalOptions\n | ((value: string, numericValue: number) => void)\n | null\n | undefined,\n): BindDecimalOptions {\n if (second == null) return {}\n if (typeof second === 'function') return { onChange: second }\n return second\n}\n\nfunction isDigitKey(key: string): boolean {\n return key.length === 1 && key >= '0' && key <= '9'\n}\n\n/**\n * Bind a decimal/currency mask to an input element.\n *\n * Same contract as {@link bind}: idempotent (marked with `data-masked`),\n * returns a dispose function, and reformats on paste and keyboard-driven\n * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no\n * fixed pattern — the integer part grows and shrinks freely; formatting is\n * driven entirely by `options`.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.\n */\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n options?: BindDecimalOptions | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n onChange: ((value: string, numericValue: number) => void) | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n second?: BindDecimalOptions | ((value: string, numericValue: number) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, ...maskOptions } = toBindDecimalOptions(second)\n const decimalOptions: DecimalMaskOptions = maskOptions\n const { decimalSeparator, decimalPlaces } = resolveDecimalOptions(decimalOptions)\n\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, 'decimal')\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n\n let lockInput = false\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n target.setSelectionRange(m.caret, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n applyResult(target, applyDecimalMask(target.value, target.value.length, decimalOptions))\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n\n // Navigation (arrows, Home/End, Tab, ...), selection, and shortcut keys\n // (Ctrl/Cmd+A, Ctrl/Cmd+C, ...) don't change the text — leave the\n // browser's native caret/selection handling alone instead of recomputing\n // and overwriting it on every keystroke.\n if (!isBackspace && !isDelete && !isCharInsert) return\n\n // Everything below reads `target.value`/`selectionStart` inside the rAF\n // callback rather than synchronously here, since the browser's native\n // character insertion for this keystroke isn't guaranteed to have landed\n // yet at the point a keydown listener runs — only by the next frame.\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n\n // Backspace that just deleted the decimal separator merges the\n // integer and fraction digit runs into one continuous stream —\n // restore the boundary instead of treating that as one big integer.\n if (isBackspace) {\n const unmerged = applyDecimalMaskUnmergingSeparator(target.value, decimalOptions)\n if (unmerged) {\n applyResult(target, unmerged)\n return\n }\n }\n\n // A numeric keypad (or a locale mismatch) may only offer \".\" or \",\".\n // Normalize whichever one the user just typed to the configured\n // `decimalSeparator` so it reliably opens the fraction segment.\n if (\n decimalPlaces !== 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator &&\n pos > 0 &&\n target.value[pos - 1] === ke.key\n ) {\n target.value = target.value.slice(0, pos - 1) + decimalSeparator + target.value.slice(pos)\n }\n\n // Typing a digit into a field whose integer part isn't yet full of\n // real digits — the auto-inserted \"0\" placeholder, or a\n // `numberPlaces`-padded segment with fewer real digits than its width\n // — extends the real digit stream instead of combining with a\n // padding zero (e.g. \"$0.00\" + \"2\" → \"$2.00\", \"02:00\" + \"4\" →\n // \"24:00\"). A segment already full of real digits (e.g. \"23:00\")\n // falls through to the default mask, which drops the keystroke.\n const replaced = isDigitKey(ke.key)\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, ke.key, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mother-mask.mjs","names":["isDigitChar","MASKED_ATTR"],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts","../src/decimal-mask.ts","../src/bind-decimal.ts"],"sourcesContent":["import type { ApplyMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction isSlotChar(ch: string): boolean {\n return ch === '9' || ch === 'Z' || ch === 'A'\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Flat masking (default) — treats the mask as one continuous character\n// stream. Best for continuous identifiers (phone numbers, CPF/CNPJ, credit\n// cards) where deleting/inserting a digit anywhere is expected to reflow\n// every digit after it — this is the classic mother-mask behavior and is\n// relied on by the majority of the test suite (paste, backspace, mid-string\n// insert, etc).\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nfunction applyFlatMask(value: string, mask: string, inputCaret: number): MaskResult {\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (opt-in) — treats literal separators as hard boundaries\n// between independent fields (e.g. day/month/year in \"99/99/9999\"). Editing\n// one segment never bleeds characters into a neighboring one, so replacing\n// the \"12\" in \"25/12/2025\" with a shorter or longer value keeps the year\n// exactly where it is instead of shifting digits across the \"/\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n// A bound input re-applies the same (static) mask string on every keystroke,\n// so tokenizing it is pure, repeated work — cache by mask string instead of\n// re-walking and re-allocating tokens on every keystroke.\nconst tokenCache = new Map<string, MaskToken[]>()\n\n/**\n * Split a mask into alternating literal and slot-run tokens (e.g. \"99/99/9999\"\n * → slots\"99\", literal\"/\", slots\"99\", literal\"/\", slots\"9999\"), so the masking\n * pass can reason about segment boundaries instead of a flat character stream.\n */\nfunction tokenizeMask(mask: string): MaskToken[] {\n const cached = tokenCache.get(mask)\n if (cached) return cached\n\n const tokens: MaskToken[] = []\n let i = 0\n while (i < mask.length) {\n const start = i\n const wantSlots = isSlotChar(mask[i])\n while (i < mask.length && isSlotChar(mask[i]) === wantSlots) i++\n const text = mask.slice(start, i)\n tokens.push(wantSlots ? { kind: 'slots', chars: text } : { kind: 'literal', text })\n }\n tokenCache.set(mask, tokens)\n return tokens\n}\n\n/** Total slot capacity from token index `from` (inclusive) to the end of `tokens`. */\nfunction slotCapacityFrom(tokens: MaskToken[], from: number): number {\n let capacity = 0\n for (let i = from; i < tokens.length; i++) {\n const token = tokens[i]\n if (token.kind === 'slots') capacity += token.chars.length\n }\n return capacity\n}\n\n/** Count of remaining slot-matchable (digit/letter) characters in `value` from `fromIdx` onward. */\nfunction remainingDataChars(value: string, fromIdx: number): number {\n let count = 0\n for (let i = fromIdx; i < value.length; i++) {\n const ch = value[i]\n if (isDigitChar(ch) || isLetterChar(ch)) count++\n }\n return count\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but walks the mask one *segment* at\n * a time (a run of slots, or a literal) rather than one character at a time.\n * The rule that keeps an edit inside a segment from crossing into its\n * neighbor is an **early stop**: if the value hits the literal that ends the\n * current slot run before all of that run's slots are filled (e.g. only one\n * digit typed into a two-digit month slot), the run is left partially filled\n * instead of skipping past the separator to steal a digit from the next\n * segment.\n *\n * That stop is only taken when it's actually safe — i.e. every character\n * still to come in `value` fits in the slot capacity that remains *after*\n * this segment. If stopping here would strand more data than the rest of\n * the mask can hold (e.g. pasting into a later segment while an earlier one\n * still sits under-filled from before), the \"separator\" is treated as stray\n * noise instead and skipped, letting this segment take the extra slot it\n * needs so nothing at the end gets silently dropped. This is also what lets\n * an array mask grow or shrink its pattern (moving every literal after the\n * change point) reflow correctly instead of losing a digit at the boundary.\n */\nfunction applySegmentedMask(value: string, mask: string, inputCaret: number): MaskResult {\n const tokens = tokenizeMask(mask)\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n let exhausted = false\n\n for (let t = 0; t < tokens.length && !exhausted; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n pending += token.text\n if (value.startsWith(token.text, valueIdx)) valueIdx += token.text.length\n continue\n }\n\n const nextToken = tokens[t + 1]\n const capacityAfter = slotCapacityFrom(tokens, t + 1)\n\n for (let s = 0; s < token.chars.length; s++) {\n const slotCh = token.chars[s]\n let found = false\n\n while (valueIdx < value.length) {\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, slotCh)) {\n valueIdx++\n output += pending + ch\n pending = ''\n found = true\n\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n\n // This segment's ending separator showed up before the run filled.\n // Stopping here is only safe if the rest of the value still fits in\n // whatever slot capacity remains after this segment — otherwise\n // stopping would strand data, so fall through and skip this char as\n // noise instead, letting the segment take the slot it needs.\n if (\n nextToken?.kind === 'literal' &&\n value.startsWith(nextToken.text, valueIdx) &&\n remainingDataChars(value, valueIdx) <= capacityAfter\n ) {\n break\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n if (!found) {\n if (valueIdx >= value.length) exhausted = true\n break\n }\n }\n }\n\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\nexport function applyMask(\n value: string,\n mask: string,\n inputCaret = 0,\n options?: ApplyMaskOptions,\n): MaskResult {\n if (!value) return { value: '', caret: 0 }\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret)\n : applySegmentedMask(value, mask, inputCaret)\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { ApplyMaskOptions, MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n private readonly _options: ApplyMaskOptions | undefined\n\n constructor(value: string, mask: string, caret = 0, options?: ApplyMaskOptions) {\n this._value = value\n this._mask = mask\n this.caret = caret\n this._options = options\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret, this._options)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(\n value: string,\n mask: MaskPattern,\n caret = 0,\n options?: ApplyMaskOptions,\n): Mask {\n return new Mask(value, resolveMask(value, mask), caret, options)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string {\n return buildMask(value, mask, 0, options).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, segmented } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n // Computed once here rather than inside `onKey` — the mask never changes\n // for the lifetime of this binding, and `onKey` is a synchronous, hot,\n // input-blocking path run on every keystroke.\n const maxLength = getMaxLength(mask)\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(maxLength))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n const m = buildMask(target.value, mask, 0, { segmented })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= maxLength && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n","import type { DecimalMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — mirrors apply-mask.ts)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\n// ---------------------------------------------------------------------------\n// Option resolution\n// ---------------------------------------------------------------------------\n\ninterface ResolvedDecimalOptions {\n /**\n * `undefined` means an optional, uncapped fraction (default) — the\n * decimal separator and fraction only appear once the user actually types\n * them, and there's no limit on how many digits follow. `0` means no\n * fraction at all. A positive number is a fixed, zero-padded width that's\n * always shown, even before the user types anything.\n */\n decimalPlaces: number | undefined\n /** `undefined` means unlimited (default) — the integer part grows freely. */\n numberPlaces: number | undefined\n segmented: boolean\n separator: string\n decimalSeparator: string\n prefix: string\n suffix: string\n allowNegative: boolean\n}\n\n/** @internal exported for {@link bindDecimal}'s \".\" / \",\" key normalization */\nexport function resolveDecimalOptions(options?: DecimalMaskOptions): ResolvedDecimalOptions {\n const rawPlaces = options?.decimalPlaces\n const decimalPlaces =\n rawPlaces != null && Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : undefined\n const rawNumberPlaces = options?.numberPlaces\n const numberPlaces =\n rawNumberPlaces != null && Number.isFinite(rawNumberPlaces)\n ? Math.max(1, Math.floor(rawNumberPlaces))\n : undefined\n return {\n decimalPlaces,\n numberPlaces,\n segmented: options?.segmented ?? true,\n separator: options?.separator ?? ',',\n decimalSeparator: options?.decimalSeparator ?? '.',\n prefix: options?.prefix ?? '',\n suffix: options?.suffix ?? '',\n allowNegative: options?.allowNegative ?? false,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Digit-stream helpers\n// ---------------------------------------------------------------------------\n\n/** Strip leading zeros from a digit string, always keeping at least one digit. */\nfunction stripLeadingZeros(s: string): string {\n let i = 0\n while (i < s.length - 1 && s[i] === '0') i++\n return s.slice(i)\n}\n\n/** Insert `sep` every 3 digits from the right (e.g. \"1234567\" → \"1,234,567\"). */\nfunction groupThousands(s: string, sep: string): string {\n if (!sep || s.length <= 3) return s\n const parts: string[] = []\n let i = s.length\n while (i > 3) {\n parts.unshift(s.slice(i - 3, i))\n i -= 3\n }\n parts.unshift(s.slice(0, i))\n return parts.join(sep)\n}\n\n/**\n * Find the position in `s` that leaves exactly `digitsBefore` digit\n * characters preceding it — the position immediately after that digit and\n * before any subsequent literal (grouping separator, ...), so the caret\n * stays glued to the last digit the user placed there.\n */\nfunction caretForDigitsBefore(s: string, digitsBefore: number): number {\n if (digitsBefore <= 0) return 0\n let count = 0\n for (let i = 0; i < s.length; i++) {\n if (isDigitChar(s[i])) {\n count++\n if (count === digitsBefore) return i + 1\n }\n }\n return s.length\n}\n\n// ---------------------------------------------------------------------------\n// Segmented parsing — integer digits before the decimal separator, fraction\n// digits after. Unlike a slot-pattern mask, the integer segment has no fixed\n// length. The fraction is only fixed-width when `decimalPlaces` is set to a\n// positive number — zero-padded on the right so a shorter fraction reads as\n// its low-order (trailing) digits being zero rather than reflowing/shifting\n// (e.g. editing \"423,42\" down to \"423,4\" produces \"423,40\", not \"42,34\").\n// When `decimalPlaces` is left unset, the fraction is optional and uncapped:\n// it only exists once the user types the separator, and grows to however\n// many digits they type.\n//\n// The decimal separator only has meaning as the *first* occurrence of\n// `opts.decimalSeparator`; every other non-digit character (thousands\n// separator, prefix/suffix text, a second stray separator, ...) is noise and\n// is dropped. This keeps re-parsing an already-masked value idempotent.\n// ---------------------------------------------------------------------------\n\ninterface DecimalParts {\n isNegative: boolean\n intDigits: string\n fracDigits: string\n hasSeparator: boolean\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n let intDigits = ''\n let fracDigits = ''\n let isNegative = false\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces !== 0\n\n for (const ch of raw) {\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else if (opts.numberPlaces == null || intDigits.length < opts.numberPlaces) {\n intDigits += ch\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n continue\n }\n if (ch === '-' && opts.allowNegative) isNegative = true\n // Anything else — thousands separator, prefix/suffix text, a repeated\n // separator, stray letters — is noise and is dropped.\n }\n\n return { isNegative, intDigits, fracDigits, hasSeparator: inFraction }\n}\n\n/**\n * Walk `raw[0:caret]` to find which segment the caret sits in (integer or\n * fraction) and how many digits of that segment precede it, so the same\n * position can be re-derived in the freshly formatted output.\n */\nfunction locateCaretSegment(\n raw: string,\n caret: number,\n opts: ResolvedDecimalOptions,\n): { inFraction: boolean; digitsBefore: number } {\n const canHaveFraction = opts.decimalPlaces !== 0\n let inFraction = false\n let digitsBefore = 0\n\n for (let i = 0; i < caret; i++) {\n const ch = raw[i]\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || digitsBefore < opts.decimalPlaces) digitsBefore++\n } else if (opts.numberPlaces == null || digitsBefore < opts.numberPlaces) {\n digitsBefore++\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n digitsBefore = 0\n }\n }\n\n return { inFraction, digitsBefore }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry points\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a decimal/currency mask to a value, producing the masked output and\n * a computed caret position. Digits typed before the decimal separator\n * extend the integer part; the fraction only starts once the separator is\n * typed. With a fixed `decimalPlaces` it's always displayed zero-padded to\n * that width, even before the user types it; left unset, the fraction is\n * optional — it only appears once the separator is typed, and is shown\n * exactly as typed (no padding, no cap on how many digits).\n */\nexport function applyDecimalMask(\n value: string,\n inputCaret = 0,\n options?: DecimalMaskOptions,\n): MaskResult {\n const opts = resolveDecimalOptions(options)\n if (!value) return { value: '', caret: 0 }\n\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (intDigits === '' && fracDigits === '' && !hasSeparator) return { value: '', caret: 0 }\n\n const intPart = stripLeadingZeros(intDigits || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const fracPadded =\n opts.decimalPlaces != null && opts.decimalPlaces > 0\n ? fracDigits.padEnd(opts.decimalPlaces, '0')\n : fracDigits\n const showFraction = opts.decimalPlaces === 0 ? false : opts.decimalPlaces != null || hasSeparator\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPadded : '')\n const signStr = isNegative ? '-' : ''\n const output = signStr + opts.prefix + numberStr + opts.suffix\n\n const clampedCaret = Math.max(0, Math.min(inputCaret, value.length))\n const { inFraction, digitsBefore } = locateCaretSegment(value, clampedCaret, opts)\n const prefixLen = signStr.length + opts.prefix.length\n // Left-padding zeros are synthetic — prepended ahead of every real typed\n // digit — so they shift where the caret's `digitsBefore`-th real digit\n // lands in `groupedInt` by the padding's width.\n const padLength = paddedInt.length - intPart.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore + padLength)\n\n return { value: output, caret }\n}\n\n/** Apply a decimal mask to a raw value and return just the masked string. */\nexport function processDecimal(value: string, options?: DecimalMaskOptions): string {\n return applyDecimalMask(value, value.length, options).value\n}\n\n/**\n * Parse a raw or already-masked decimal value back into a JS number.\n * Ignores prefix/suffix/thousands separator; returns `0` for an empty or\n * digit-less value.\n */\nexport function unmaskDecimal(value: string, options?: DecimalMaskOptions): number {\n const opts = resolveDecimalOptions(options)\n const { isNegative, intDigits, fracDigits } = computeDecimalParts(value, opts)\n const n = Number(fracDigits ? `${intDigits || '0'}.${fracDigits}` : intDigits || '0')\n return isNegative ? -n : n\n}\n\n/**\n * After a Backspace removes the decimal separator itself, the integer and\n * fraction digit runs collapse into one continuous stream (e.g. \"25.00\"\n * with the caret right after \".\" → Backspace deletes the \".\" → \"2500\").\n * Left alone, that reads as one big integer (\"$2,500.00\"). This restores\n * the segment boundary instead: the trailing `decimalPlaces` digits are\n * still the fraction, and the digit right before them — the one that used\n * to sit at the end of the integer part — is the one Backspace actually\n * removed, so it's dropped (not kept) — \"$25.00\" → \"$2.00\".\n *\n * Only applies when `decimalPlaces` is a fixed positive number — that's\n * what \"the trailing N digits are the fraction\" means. Every reformat\n * re-appends `decimalSeparator` in that case, so its absence from `value`\n * is an unambiguous signal that this exact keystroke just deleted it — no\n * \"value before this keystroke\" snapshot is needed. With `decimalPlaces`\n * unset (optional, uncapped fraction) there's no fixed width to reconstruct\n * from, so the merged digits are left as one continuous integer instead —\n * the same reasoning `numberPlaces` uses for an unbounded integer part.\n * Returns `null` when there's nothing to restore (the separator is still\n * present, `decimalPlaces` is `0` or unset, or too few digits remain), so\n * the caller falls through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskUnmergingSeparator(\n value: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n if (opts.decimalPlaces == null || opts.decimalPlaces <= 0) return null\n\n const { isNegative, intDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (hasSeparator || intDigits.length < opts.decimalPlaces + 1) return null\n\n const preMergeIntLength = intDigits.length - opts.decimalPlaces\n const fracDigits = intDigits.slice(preMergeIntLength)\n const remainingInt = intDigits.slice(0, preMergeIntLength - 1)\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + remainingInt + opts.decimalSeparator + fracDigits\n return applyDecimalMask(raw, signPart.length + remainingInt.length, opts)\n}\n\n/**\n * Special-cases typing a single digit into an integer segment that isn't\n * yet full of *real* digits — either because it's still the auto-inserted\n * zero placeholder (\"0\", or a wider \"00\" from a `numberPlaces`-padded field\n * that hasn't been touched), or because `numberPlaces` is left-padding a\n * partially-typed segment with synthetic zeros (e.g. \"02\" is really just\n * the one real digit \"2\", padded out to width 2 for display). Those padding\n * zeros aren't editable content, so the new digit extends the real digit\n * stream instead of combining with a padding zero at the caret:\n *\n * - \"$0.00\" + \"2\" → \"$2.00\" (not \"$20.00\")\n * - a `numberPlaces: 2` time field's untouched \"00:00\" + \"5\" → \"05:00\"\n * - that same field's \"02:00\" (one real digit, one padding zero) + \"4\" →\n * \"24:00\" — the real \"2\" is kept, the padding \"0\" is not\n *\n * A segment that's already full of real digits — e.g. \"23:00\", both digits\n * genuinely typed — doesn't match here and falls through to the default\n * {@link applyDecimalMask}, which already drops the overflow keystroke\n * (typing a 3rd real digit leaves \"23:00\" unchanged).\n *\n * `value`/`caret` must be the state *after* the browser has already\n * inserted `digit` at `caret - 1` (the same post-insertion snapshot\n * `applyDecimalMask` itself expects from `bindDecimal`). Returns `null`\n * when the pattern doesn't apply, so the caller falls through to a plain\n * {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskReplacingLoneZero(\n value: string,\n caret: number,\n digit: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n const insertIdx = caret - 1\n if (insertIdx < 0 || value[insertIdx] !== digit) return null\n\n const withoutDigit = value.slice(0, insertIdx) + value.slice(caret)\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(withoutDigit, opts)\n\n const realDigits = stripLeadingZeros(intDigits || '0')\n const hasRealDigits = realDigits !== '0'\n const hasPaddingRoom =\n hasRealDigits && opts.numberPlaces != null && realDigits.length < opts.numberPlaces\n if (hasRealDigits && !hasPaddingRoom) return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + intDigits.length) return null\n\n const signPart = isNegative ? '-' : ''\n const newIntDigits = (hasRealDigits ? realDigits : '') + digit\n const raw = signPart + newIntDigits + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + newIntDigits.length, opts)\n}\n\n/**\n * Format a plain JS number into its masked display string. With a fixed\n * `decimalPlaces` the fraction is rounded/padded to that exact width, even\n * for a whole number; left unset, the fraction is only shown when the value\n * actually has one, with as many digits as `value` naturally carries (no\n * padding, no rounding).\n */\nexport function formatDecimalValue(value: number, options?: DecimalMaskOptions): string {\n const opts = resolveDecimalOptions(options)\n if (!Number.isFinite(value)) return ''\n\n const isNegative = opts.allowNegative && value < 0\n const abs = Math.abs(value)\n const fixed = opts.decimalPlaces != null ? abs.toFixed(opts.decimalPlaces) : String(abs)\n const dotIdx = fixed.indexOf('.')\n const intRaw = dotIdx === -1 ? fixed : fixed.slice(0, dotIdx)\n const fracPart = dotIdx === -1 ? '' : fixed.slice(dotIdx + 1)\n const intPart = stripLeadingZeros(intRaw || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const showFraction = opts.decimalPlaces === 0 ? false : fracPart !== ''\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPart : '')\n\n return (isNegative ? '-' : '') + opts.prefix + numberStr + opts.suffix\n}\n","import {\n applyDecimalMask,\n applyDecimalMaskReplacingLoneZero,\n applyDecimalMaskUnmergingSeparator,\n resolveDecimalOptions,\n unmaskDecimal,\n} from './decimal-mask'\nimport { isIos } from './platform'\nimport type { BindDecimalOptions, DecimalMaskOptions, MaskResult } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindDecimalOptions(\n second:\n | BindDecimalOptions\n | ((value: string, numericValue: number) => void)\n | null\n | undefined,\n): BindDecimalOptions {\n if (second == null) return {}\n if (typeof second === 'function') return { onChange: second }\n return second\n}\n\nfunction isDigitKey(key: string): boolean {\n return key.length === 1 && key >= '0' && key <= '9'\n}\n\n/**\n * Bind a decimal/currency mask to an input element.\n *\n * Same contract as {@link bind}: idempotent (marked with `data-masked`),\n * returns a dispose function, and reformats on paste and keyboard-driven\n * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no\n * fixed pattern — the integer part grows and shrinks freely; formatting is\n * driven entirely by `options`.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.\n */\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n options?: BindDecimalOptions | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n onChange: ((value: string, numericValue: number) => void) | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n second?: BindDecimalOptions | ((value: string, numericValue: number) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, ...maskOptions } = toBindDecimalOptions(second)\n const decimalOptions: DecimalMaskOptions = maskOptions\n const { decimalSeparator, decimalPlaces } = resolveDecimalOptions(decimalOptions)\n\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, 'decimal')\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n\n let lockInput = false\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n target.setSelectionRange(m.caret, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n applyResult(target, applyDecimalMask(target.value, target.value.length, decimalOptions))\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n\n // Navigation (arrows, Home/End, Tab, ...), selection, and shortcut keys\n // (Ctrl/Cmd+A, Ctrl/Cmd+C, ...) don't change the text — leave the\n // browser's native caret/selection handling alone instead of recomputing\n // and overwriting it on every keystroke.\n if (!isBackspace && !isDelete && !isCharInsert) return\n\n // Everything below reads `target.value`/`selectionStart` inside the rAF\n // callback rather than synchronously here, since the browser's native\n // character insertion for this keystroke isn't guaranteed to have landed\n // yet at the point a keydown listener runs — only by the next frame.\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n\n // Backspace that just deleted the decimal separator merges the\n // integer and fraction digit runs into one continuous stream —\n // restore the boundary instead of treating that as one big integer.\n if (isBackspace) {\n const unmerged = applyDecimalMaskUnmergingSeparator(target.value, decimalOptions)\n if (unmerged) {\n applyResult(target, unmerged)\n return\n }\n }\n\n // A numeric keypad (or a locale mismatch) may only offer \".\" or \",\".\n // Normalize whichever one the user just typed to the configured\n // `decimalSeparator` so it reliably opens the fraction segment.\n if (\n decimalPlaces !== 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator &&\n pos > 0 &&\n target.value[pos - 1] === ke.key\n ) {\n target.value = target.value.slice(0, pos - 1) + decimalSeparator + target.value.slice(pos)\n }\n\n // Typing a digit into a field whose integer part isn't yet full of\n // real digits — the auto-inserted \"0\" placeholder, or a\n // `numberPlaces`-padded segment with fewer real digits than its width\n // — extends the real digit stream instead of combining with a\n // padding zero (e.g. \"$0.00\" + \"2\" → \"$2.00\", \"02:00\" + \"4\" →\n // \"24:00\"). A segment already full of real digits (e.g. \"23:00\")\n // falls through to the default mask, which drops the keystroke.\n const replaced = isDigitKey(ke.key)\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, ke.key, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n"],"mappings":"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"}
|
package/dist/mother-mask.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.MotherMask={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){return e>=`0`&&e<=`9`}function n(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function r(e){return e===`9`||e===`Z`||e===`A`}function i(e,r){return r===`9`?t(e):r===`Z`?n(e):t(e)||n(e)}function a(e,t,n){let r=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(i(t,u)){r+=a+t,a=``,d=!0,c||(o<=n?s=r.length:c=!0);break}}if(!d)break}return c||(s=r.length),{value:r,caret:s}}let o=new Map;function s(e){let t=o.get(e);if(t)return t;let n=[],i=0;for(;i<e.length;){let t=i,a=r(e[i]);for(;i<e.length&&r(e[i])===a;)i++;let o=e.slice(t,i);n.push(a?{kind:`slots`,chars:o}:{kind:`literal`,text:o})}return o.set(e,n),n}function c(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 l(e,r){let i=0;for(let a=r;a<e.length;a++){let r=e[a];(t(r)||n(r))&&i++}return i}function u(e,t,n){let r=s(t),a=``,o=``,u=0,d=0,f=!1,p=!1;for(let t=0;t<r.length&&!p;t++){let s=r[t];if(s.kind===`literal`){o+=s.text,e.startsWith(s.text,u)&&(u+=s.text.length);continue}let m=r[t+1],h=c(r,t+1);for(let t=0;t<s.chars.length;t++){let r=s.chars[t],c=!1;for(;u<e.length;){let t=e[u];if(i(t,r)){u++,a+=o+t,o=``,c=!0,f||(u<=n?d=a.length:f=!0);break}if(m?.kind===`literal`&&e.startsWith(m.text,u)&&l(e,u)<=h)break;u++}if(!c){u>=e.length&&(p=!0);break}}}return f||(d=a.length),{value:a,caret:d}}function d(e,t,n=0,r){return e?r?.segmented===!1?a(e,t,n):u(e,t,n):{value:``,caret:0}}function f(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 p(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function m(e,t){if(!Array.isArray(t))return t;let n=f(e),r=0;for(;r<t.length-1&&n>p(t[r]);)r++;return t[r]}function h(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var g=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=d(this._value,this._mask,this.caret,this._options);return this.caret=e.caret,e.value}};function _(e,t,n=0,r){return new g(e,m(e,t),n,r)}function v(e,t,n){return _(e,t,0,n).process()}let y;function b(){return y===void 0&&(y=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),y}let x=`data-masked`;function S(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function C(e,t,n){if(e.getAttribute(x)!==null)return()=>{};let{onChange:r,segmented:i}=S(n),a=[],o=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),a.push(t))},s=h(t);e.setAttribute(x,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=b()?`keyup`:`keydown`,u=new Set,d=e=>{let t=requestAnimationFrame(()=>{u.delete(t),e()});u.add(t)},f=e=>{let n=e.target;d(()=>{
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.MotherMask={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){return e>=`0`&&e<=`9`}function n(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function r(e){return e===`9`||e===`Z`||e===`A`}function i(e,r){return r===`9`?t(e):r===`Z`?n(e):t(e)||n(e)}function a(e,t,n){let r=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(i(t,u)){r+=a+t,a=``,d=!0,c||(o<=n?s=r.length:c=!0);break}}if(!d)break}return c||(s=r.length),{value:r,caret:s}}let o=new Map;function s(e){let t=o.get(e);if(t)return t;let n=[],i=0;for(;i<e.length;){let t=i,a=r(e[i]);for(;i<e.length&&r(e[i])===a;)i++;let o=e.slice(t,i);n.push(a?{kind:`slots`,chars:o}:{kind:`literal`,text:o})}return o.set(e,n),n}function c(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 l(e,r){let i=0;for(let a=r;a<e.length;a++){let r=e[a];(t(r)||n(r))&&i++}return i}function u(e,t,n){let r=s(t),a=``,o=``,u=0,d=0,f=!1,p=!1;for(let t=0;t<r.length&&!p;t++){let s=r[t];if(s.kind===`literal`){o+=s.text,e.startsWith(s.text,u)&&(u+=s.text.length);continue}let m=r[t+1],h=c(r,t+1);for(let t=0;t<s.chars.length;t++){let r=s.chars[t],c=!1;for(;u<e.length;){let t=e[u];if(i(t,r)){u++,a+=o+t,o=``,c=!0,f||(u<=n?d=a.length:f=!0);break}if(m?.kind===`literal`&&e.startsWith(m.text,u)&&l(e,u)<=h)break;u++}if(!c){u>=e.length&&(p=!0);break}}}return f||(d=a.length),{value:a,caret:d}}function d(e,t,n=0,r){return e?r?.segmented===!1?a(e,t,n):u(e,t,n):{value:``,caret:0}}function f(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 p(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function m(e,t){if(!Array.isArray(t))return t;let n=f(e),r=0;for(;r<t.length-1&&n>p(t[r]);)r++;return t[r]}function h(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var g=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=d(this._value,this._mask,this.caret,this._options);return this.caret=e.caret,e.value}};function _(e,t,n=0,r){return new g(e,m(e,t),n,r)}function v(e,t,n){return _(e,t,0,n).process()}let y;function b(){return y===void 0&&(y=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),y}let x=`data-masked`;function S(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function C(e,t,n){if(e.getAttribute(x)!==null)return()=>{};let{onChange:r,segmented:i}=S(n),a=[],o=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),a.push(t))},s=h(t);e.setAttribute(x,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=b()?`keyup`:`keydown`,u=new Set,d=e=>{let t=requestAnimationFrame(()=>{u.delete(t),e()});u.add(t)},f=e=>{let n=e.target;d(()=>{let e=_(n.value,t,0,{segmented:i});n.value=e.process(),r?.(n.value)})},p=e=>{let n=e,a=n.target,o=a.value;if(!n.key){c=!0,d(()=>{let e=a.selectionStart??999,n=_(a.value,t,e,{segmented:i});a.value=n.process(),a.setSelectionRange(n.caret,n.caret),d(()=>{c=!1})});return}if(n.key===`Meta`)return;let l=n.key===`Backspace`,u=n.key===`Delete`,f=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,p=n.key===`Unidentified`;if(f&&a.selectionStart===a.selectionEnd&&o.length>=s&&!b()){n.preventDefault();return}if(c){n.preventDefault();return}d(()=>{let e=a.selectionStart??999,n=_(a.value,t,e,{segmented:i});if(a.value=n.process(),p){let t=a.value.length>o.length?n.caret:e;a.setSelectionRange(t,t)}else if(u){let t=o.length===a.value.length?e+1:e;a.setSelectionRange(t,t)}else l?a.setSelectionRange(e,e):f&&a.setSelectionRange(n.caret,n.caret);r?.(a.value)})};return e.addEventListener(`paste`,f),e.addEventListener(l,p),()=>{e.removeEventListener(`paste`,f),e.removeEventListener(l,p),e.removeAttribute(x);for(let t of a)e.removeAttribute(t);for(let e of u)cancelAnimationFrame(e);u.clear()}}function w(e){return e>=`0`&&e<=`9`}function T(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 E(e){let t=0;for(;t<e.length-1&&e[t]===`0`;)t++;return e.slice(t)}function D(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 O(e,t){if(t<=0)return 0;let n=0;for(let r=0;r<e.length;r++)if(w(e[r])&&(n++,n===t))return r+1;return e.length}function k(e,t){let n=``,r=``,i=!1,a=!1,o=t.decimalPlaces!==0;for(let s of e){if(w(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 A(e,t,n){let r=n.decimalPlaces!==0,i=!1,a=0;for(let o=0;o<t;o++){let t=e[o];if(w(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 j(e,t=0,n){let r=T(n);if(!e)return{value:``,caret:0};let{isNegative:i,intDigits:a,fracDigits:o,hasSeparator:s}=k(e,r);if(a===``&&o===``&&!s)return{value:``,caret:0};let c=E(a||`0`),l=r.numberPlaces==null?c:c.padStart(r.numberPlaces,`0`),u=r.segmented?D(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}=A(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:_+O(u,g+v)}}function M(e,t){return j(e,e.length,t).value}function N(e,t){let{isNegative:n,intDigits:r,fracDigits:i}=k(e,T(t)),a=Number(i?`${r||`0`}.${i}`:r||`0`);return n?-a:a}function P(e,t){let n=T(t);if(n.decimalPlaces==null||n.decimalPlaces<=0)return null;let{isNegative:r,intDigits:i,hasSeparator:a}=k(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 j(l+c+n.decimalSeparator+s,l.length+c.length,n)}function F(e,t,n,r){let i=T(r),a=t-1;if(a<0||e[a]!==n)return null;let{isNegative:o,intDigits:s,fracDigits:c,hasSeparator:l}=k(e.slice(0,a)+e.slice(t),i),u=E(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 j(m+h+(l?i.decimalSeparator+c:``),m.length+h.length,i)}function I(e,t){let n=T(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=E(s||`0`),u=n.numberPlaces==null?l:l.padStart(n.numberPlaces,`0`),d=(n.segmented?D(u,n.separator):u)+(n.decimalPlaces!==0&&c!==``?n.decimalSeparator+c:``);return(r?`-`:``)+n.prefix+d+n.suffix}let L=`data-masked`;function R(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function z(e){return e.length===1&&e>=`0`&&e<=`9`}function B(e,t){if(e.getAttribute(L)!==null)return()=>{};let{onChange:n,...r}=R(t),i=r,{decimalSeparator:a,decimalPlaces:o}=T(i),s=[],c=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),s.push(t))};e.setAttribute(L,`decimal`),c(`autocomplete`,`off`),c(`autocorrect`,`off`),c(`autocapitalize`,`off`),c(`spellcheck`,`false`);let l=!1,u=b()?`keyup`:`keydown`,d=new Set,f=e=>{let t=requestAnimationFrame(()=>{d.delete(t),e()});d.add(t)},p=(e,t)=>{e.value=t.value,e.setSelectionRange(t.caret,t.caret),n?.(t.value,N(t.value,i))},m=e=>{let t=e.target;f(()=>{p(t,j(t.value,t.value.length,i))})},h=e=>{let t=e,n=t.target;if(!t.key){l=!0,f(()=>{let e=n.selectionStart??n.value.length;p(n,j(n.value,e,i)),f(()=>{l=!1})});return}if(t.key===`Meta`)return;if(l){t.preventDefault();return}let r=t.key===`Backspace`,s=t.key===`Delete`,c=t.key.length===1&&!t.ctrlKey&&!t.altKey&&!t.metaKey;!r&&!s&&!c||f(()=>{let e=n.selectionStart??n.value.length;if(r){let e=P(n.value,i);if(e){p(n,e);return}}o!==0&&(t.key===`.`||t.key===`,`)&&t.key!==a&&e>0&&n.value[e-1]===t.key&&(n.value=n.value.slice(0,e-1)+a+n.value.slice(e));let s=z(t.key)?F(n.value,e,t.key,i):null;p(n,s??j(n.value,e,i))})};return e.addEventListener(`paste`,m),e.addEventListener(u,h),()=>{e.removeEventListener(`paste`,m),e.removeEventListener(u,h),e.removeAttribute(L);for(let t of s)e.removeAttribute(t);for(let e of d)cancelAnimationFrame(e);d.clear()}}e.Mask=g,e.applyDecimalMask=j,e.applyMask=d,e.bind=C,e.bindDecimal=B,e.buildMask=_,e.formatDecimalValue=I,e.getMaxLength=h,e.process=v,e.processDecimal=M,e.unmaskDecimal=N});
|
|
2
2
|
//# sourceMappingURL=mother-mask.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mother-mask.umd.js","names":["isDigitChar","MASKED_ATTR"],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts","../src/decimal-mask.ts","../src/bind-decimal.ts"],"sourcesContent":["import type { ApplyMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction isSlotChar(ch: string): boolean {\n return ch === '9' || ch === 'Z' || ch === 'A'\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Flat masking (default) — treats the mask as one continuous character\n// stream. Best for continuous identifiers (phone numbers, CPF/CNPJ, credit\n// cards) where deleting/inserting a digit anywhere is expected to reflow\n// every digit after it — this is the classic mother-mask behavior and is\n// relied on by the majority of the test suite (paste, backspace, mid-string\n// insert, etc).\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nfunction applyFlatMask(value: string, mask: string, inputCaret: number): MaskResult {\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (opt-in) — treats literal separators as hard boundaries\n// between independent fields (e.g. day/month/year in \"99/99/9999\"). Editing\n// one segment never bleeds characters into a neighboring one, so replacing\n// the \"12\" in \"25/12/2025\" with a shorter or longer value keeps the year\n// exactly where it is instead of shifting digits across the \"/\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n// A bound input re-applies the same (static) mask string on every keystroke,\n// so tokenizing it is pure, repeated work — cache by mask string instead of\n// re-walking and re-allocating tokens on every keystroke.\nconst tokenCache = new Map<string, MaskToken[]>()\n\n/**\n * Split a mask into alternating literal and slot-run tokens (e.g. \"99/99/9999\"\n * → slots\"99\", literal\"/\", slots\"99\", literal\"/\", slots\"9999\"), so the masking\n * pass can reason about segment boundaries instead of a flat character stream.\n */\nfunction tokenizeMask(mask: string): MaskToken[] {\n const cached = tokenCache.get(mask)\n if (cached) return cached\n\n const tokens: MaskToken[] = []\n let i = 0\n while (i < mask.length) {\n const start = i\n const wantSlots = isSlotChar(mask[i])\n while (i < mask.length && isSlotChar(mask[i]) === wantSlots) i++\n const text = mask.slice(start, i)\n tokens.push(wantSlots ? { kind: 'slots', chars: text } : { kind: 'literal', text })\n }\n tokenCache.set(mask, tokens)\n return tokens\n}\n\n/** Total slot capacity from token index `from` (inclusive) to the end of `tokens`. */\nfunction slotCapacityFrom(tokens: MaskToken[], from: number): number {\n let capacity = 0\n for (let i = from; i < tokens.length; i++) {\n const token = tokens[i]\n if (token.kind === 'slots') capacity += token.chars.length\n }\n return capacity\n}\n\n/** Count of remaining slot-matchable (digit/letter) characters in `value` from `fromIdx` onward. */\nfunction remainingDataChars(value: string, fromIdx: number): number {\n let count = 0\n for (let i = fromIdx; i < value.length; i++) {\n const ch = value[i]\n if (isDigitChar(ch) || isLetterChar(ch)) count++\n }\n return count\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but walks the mask one *segment* at\n * a time (a run of slots, or a literal) rather than one character at a time.\n * The rule that keeps an edit inside a segment from crossing into its\n * neighbor is an **early stop**: if the value hits the literal that ends the\n * current slot run before all of that run's slots are filled (e.g. only one\n * digit typed into a two-digit month slot), the run is left partially filled\n * instead of skipping past the separator to steal a digit from the next\n * segment.\n *\n * That stop is only taken when it's actually safe — i.e. every character\n * still to come in `value` fits in the slot capacity that remains *after*\n * this segment. If stopping here would strand more data than the rest of\n * the mask can hold (e.g. pasting into a later segment while an earlier one\n * still sits under-filled from before), the \"separator\" is treated as stray\n * noise instead and skipped, letting this segment take the extra slot it\n * needs so nothing at the end gets silently dropped. This is also what lets\n * an array mask grow or shrink its pattern (moving every literal after the\n * change point) reflow correctly instead of losing a digit at the boundary.\n */\nfunction applySegmentedMask(value: string, mask: string, inputCaret: number): MaskResult {\n const tokens = tokenizeMask(mask)\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n let exhausted = false\n\n for (let t = 0; t < tokens.length && !exhausted; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n pending += token.text\n if (value.startsWith(token.text, valueIdx)) valueIdx += token.text.length\n continue\n }\n\n const nextToken = tokens[t + 1]\n const capacityAfter = slotCapacityFrom(tokens, t + 1)\n\n for (let s = 0; s < token.chars.length; s++) {\n const slotCh = token.chars[s]\n let found = false\n\n while (valueIdx < value.length) {\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, slotCh)) {\n valueIdx++\n output += pending + ch\n pending = ''\n found = true\n\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n\n // This segment's ending separator showed up before the run filled.\n // Stopping here is only safe if the rest of the value still fits in\n // whatever slot capacity remains after this segment — otherwise\n // stopping would strand data, so fall through and skip this char as\n // noise instead, letting the segment take the slot it needs.\n if (\n nextToken?.kind === 'literal' &&\n value.startsWith(nextToken.text, valueIdx) &&\n remainingDataChars(value, valueIdx) <= capacityAfter\n ) {\n break\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n if (!found) {\n if (valueIdx >= value.length) exhausted = true\n break\n }\n }\n }\n\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\nexport function applyMask(\n value: string,\n mask: string,\n inputCaret = 0,\n options?: ApplyMaskOptions,\n): MaskResult {\n if (!value) return { value: '', caret: 0 }\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret)\n : applySegmentedMask(value, mask, inputCaret)\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { ApplyMaskOptions, MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n private readonly _options: ApplyMaskOptions | undefined\n\n constructor(value: string, mask: string, caret = 0, options?: ApplyMaskOptions) {\n this._value = value\n this._mask = mask\n this.caret = caret\n this._options = options\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret, this._options)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(\n value: string,\n mask: MaskPattern,\n caret = 0,\n options?: ApplyMaskOptions,\n): Mask {\n return new Mask(value, resolveMask(value, mask), caret, options)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string {\n return buildMask(value, mask, 0, options).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, segmented } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n // Computed once here rather than inside `onKey` — the mask never changes\n // for the lifetime of this binding, and `onKey` is a synchronous, hot,\n // input-blocking path run on every keystroke.\n const maxLength = getMaxLength(mask)\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(maxLength))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n const m = buildMask(target.value, mask, 0, { segmented })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= maxLength && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n","import type { DecimalMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — mirrors apply-mask.ts)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\n// ---------------------------------------------------------------------------\n// Option resolution\n// ---------------------------------------------------------------------------\n\ninterface ResolvedDecimalOptions {\n /**\n * `undefined` means an optional, uncapped fraction (default) — the\n * decimal separator and fraction only appear once the user actually types\n * them, and there's no limit on how many digits follow. `0` means no\n * fraction at all. A positive number is a fixed, zero-padded width that's\n * always shown, even before the user types anything.\n */\n decimalPlaces: number | undefined\n /** `undefined` means unlimited (default) — the integer part grows freely. */\n numberPlaces: number | undefined\n segmented: boolean\n separator: string\n decimalSeparator: string\n prefix: string\n suffix: string\n allowNegative: boolean\n}\n\n/** @internal exported for {@link bindDecimal}'s \".\" / \",\" key normalization */\nexport function resolveDecimalOptions(options?: DecimalMaskOptions): ResolvedDecimalOptions {\n const rawPlaces = options?.decimalPlaces\n const decimalPlaces =\n rawPlaces != null && Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : undefined\n const rawNumberPlaces = options?.numberPlaces\n const numberPlaces =\n rawNumberPlaces != null && Number.isFinite(rawNumberPlaces)\n ? Math.max(1, Math.floor(rawNumberPlaces))\n : undefined\n return {\n decimalPlaces,\n numberPlaces,\n segmented: options?.segmented ?? true,\n separator: options?.separator ?? ',',\n decimalSeparator: options?.decimalSeparator ?? '.',\n prefix: options?.prefix ?? '',\n suffix: options?.suffix ?? '',\n allowNegative: options?.allowNegative ?? false,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Digit-stream helpers\n// ---------------------------------------------------------------------------\n\n/** Strip leading zeros from a digit string, always keeping at least one digit. */\nfunction stripLeadingZeros(s: string): string {\n let i = 0\n while (i < s.length - 1 && s[i] === '0') i++\n return s.slice(i)\n}\n\n/** Insert `sep` every 3 digits from the right (e.g. \"1234567\" → \"1,234,567\"). */\nfunction groupThousands(s: string, sep: string): string {\n if (!sep || s.length <= 3) return s\n const parts: string[] = []\n let i = s.length\n while (i > 3) {\n parts.unshift(s.slice(i - 3, i))\n i -= 3\n }\n parts.unshift(s.slice(0, i))\n return parts.join(sep)\n}\n\n/**\n * Find the position in `s` that leaves exactly `digitsBefore` digit\n * characters preceding it — the position immediately after that digit and\n * before any subsequent literal (grouping separator, ...), so the caret\n * stays glued to the last digit the user placed there.\n */\nfunction caretForDigitsBefore(s: string, digitsBefore: number): number {\n if (digitsBefore <= 0) return 0\n let count = 0\n for (let i = 0; i < s.length; i++) {\n if (isDigitChar(s[i])) {\n count++\n if (count === digitsBefore) return i + 1\n }\n }\n return s.length\n}\n\n// ---------------------------------------------------------------------------\n// Segmented parsing — integer digits before the decimal separator, fraction\n// digits after. Unlike a slot-pattern mask, the integer segment has no fixed\n// length. The fraction is only fixed-width when `decimalPlaces` is set to a\n// positive number — zero-padded on the right so a shorter fraction reads as\n// its low-order (trailing) digits being zero rather than reflowing/shifting\n// (e.g. editing \"423,42\" down to \"423,4\" produces \"423,40\", not \"42,34\").\n// When `decimalPlaces` is left unset, the fraction is optional and uncapped:\n// it only exists once the user types the separator, and grows to however\n// many digits they type.\n//\n// The decimal separator only has meaning as the *first* occurrence of\n// `opts.decimalSeparator`; every other non-digit character (thousands\n// separator, prefix/suffix text, a second stray separator, ...) is noise and\n// is dropped. This keeps re-parsing an already-masked value idempotent.\n// ---------------------------------------------------------------------------\n\ninterface DecimalParts {\n isNegative: boolean\n intDigits: string\n fracDigits: string\n hasSeparator: boolean\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n let intDigits = ''\n let fracDigits = ''\n let isNegative = false\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces !== 0\n\n for (const ch of raw) {\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else if (opts.numberPlaces == null || intDigits.length < opts.numberPlaces) {\n intDigits += ch\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n continue\n }\n if (ch === '-' && opts.allowNegative) isNegative = true\n // Anything else — thousands separator, prefix/suffix text, a repeated\n // separator, stray letters — is noise and is dropped.\n }\n\n return { isNegative, intDigits, fracDigits, hasSeparator: inFraction }\n}\n\n/**\n * Walk `raw[0:caret]` to find which segment the caret sits in (integer or\n * fraction) and how many digits of that segment precede it, so the same\n * position can be re-derived in the freshly formatted output.\n */\nfunction locateCaretSegment(\n raw: string,\n caret: number,\n opts: ResolvedDecimalOptions,\n): { inFraction: boolean; digitsBefore: number } {\n const canHaveFraction = opts.decimalPlaces !== 0\n let inFraction = false\n let digitsBefore = 0\n\n for (let i = 0; i < caret; i++) {\n const ch = raw[i]\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || digitsBefore < opts.decimalPlaces) digitsBefore++\n } else if (opts.numberPlaces == null || digitsBefore < opts.numberPlaces) {\n digitsBefore++\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n digitsBefore = 0\n }\n }\n\n return { inFraction, digitsBefore }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry points\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a decimal/currency mask to a value, producing the masked output and\n * a computed caret position. Digits typed before the decimal separator\n * extend the integer part; the fraction only starts once the separator is\n * typed. With a fixed `decimalPlaces` it's always displayed zero-padded to\n * that width, even before the user types it; left unset, the fraction is\n * optional — it only appears once the separator is typed, and is shown\n * exactly as typed (no padding, no cap on how many digits).\n */\nexport function applyDecimalMask(\n value: string,\n inputCaret = 0,\n options?: DecimalMaskOptions,\n): MaskResult {\n const opts = resolveDecimalOptions(options)\n if (!value) return { value: '', caret: 0 }\n\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (intDigits === '' && fracDigits === '' && !hasSeparator) return { value: '', caret: 0 }\n\n const intPart = stripLeadingZeros(intDigits || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const fracPadded =\n opts.decimalPlaces != null && opts.decimalPlaces > 0\n ? fracDigits.padEnd(opts.decimalPlaces, '0')\n : fracDigits\n const showFraction = opts.decimalPlaces === 0 ? false : opts.decimalPlaces != null || hasSeparator\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPadded : '')\n const signStr = isNegative ? '-' : ''\n const output = signStr + opts.prefix + numberStr + opts.suffix\n\n const clampedCaret = Math.max(0, Math.min(inputCaret, value.length))\n const { inFraction, digitsBefore } = locateCaretSegment(value, clampedCaret, opts)\n const prefixLen = signStr.length + opts.prefix.length\n // Left-padding zeros are synthetic — prepended ahead of every real typed\n // digit — so they shift where the caret's `digitsBefore`-th real digit\n // lands in `groupedInt` by the padding's width.\n const padLength = paddedInt.length - intPart.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore + padLength)\n\n return { value: output, caret }\n}\n\n/** Apply a decimal mask to a raw value and return just the masked string. */\nexport function processDecimal(value: string, options?: DecimalMaskOptions): string {\n return applyDecimalMask(value, value.length, options).value\n}\n\n/**\n * Parse a raw or already-masked decimal value back into a JS number.\n * Ignores prefix/suffix/thousands separator; returns `0` for an empty or\n * digit-less value.\n */\nexport function unmaskDecimal(value: string, options?: DecimalMaskOptions): number {\n const opts = resolveDecimalOptions(options)\n const { isNegative, intDigits, fracDigits } = computeDecimalParts(value, opts)\n const n = Number(fracDigits ? `${intDigits || '0'}.${fracDigits}` : intDigits || '0')\n return isNegative ? -n : n\n}\n\n/**\n * After a Backspace removes the decimal separator itself, the integer and\n * fraction digit runs collapse into one continuous stream (e.g. \"25.00\"\n * with the caret right after \".\" → Backspace deletes the \".\" → \"2500\").\n * Left alone, that reads as one big integer (\"$2,500.00\"). This restores\n * the segment boundary instead: the trailing `decimalPlaces` digits are\n * still the fraction, and the digit right before them — the one that used\n * to sit at the end of the integer part — is the one Backspace actually\n * removed, so it's dropped (not kept) — \"$25.00\" → \"$2.00\".\n *\n * Only applies when `decimalPlaces` is a fixed positive number — that's\n * what \"the trailing N digits are the fraction\" means. Every reformat\n * re-appends `decimalSeparator` in that case, so its absence from `value`\n * is an unambiguous signal that this exact keystroke just deleted it — no\n * \"value before this keystroke\" snapshot is needed. With `decimalPlaces`\n * unset (optional, uncapped fraction) there's no fixed width to reconstruct\n * from, so the merged digits are left as one continuous integer instead —\n * the same reasoning `numberPlaces` uses for an unbounded integer part.\n * Returns `null` when there's nothing to restore (the separator is still\n * present, `decimalPlaces` is `0` or unset, or too few digits remain), so\n * the caller falls through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskUnmergingSeparator(\n value: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n if (opts.decimalPlaces == null || opts.decimalPlaces <= 0) return null\n\n const { isNegative, intDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (hasSeparator || intDigits.length < opts.decimalPlaces + 1) return null\n\n const preMergeIntLength = intDigits.length - opts.decimalPlaces\n const fracDigits = intDigits.slice(preMergeIntLength)\n const remainingInt = intDigits.slice(0, preMergeIntLength - 1)\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + remainingInt + opts.decimalSeparator + fracDigits\n return applyDecimalMask(raw, signPart.length + remainingInt.length, opts)\n}\n\n/**\n * Special-cases typing a single digit into an integer segment that isn't\n * yet full of *real* digits — either because it's still the auto-inserted\n * zero placeholder (\"0\", or a wider \"00\" from a `numberPlaces`-padded field\n * that hasn't been touched), or because `numberPlaces` is left-padding a\n * partially-typed segment with synthetic zeros (e.g. \"02\" is really just\n * the one real digit \"2\", padded out to width 2 for display). Those padding\n * zeros aren't editable content, so the new digit extends the real digit\n * stream instead of combining with a padding zero at the caret:\n *\n * - \"$0.00\" + \"2\" → \"$2.00\" (not \"$20.00\")\n * - a `numberPlaces: 2` time field's untouched \"00:00\" + \"5\" → \"05:00\"\n * - that same field's \"02:00\" (one real digit, one padding zero) + \"4\" →\n * \"24:00\" — the real \"2\" is kept, the padding \"0\" is not\n *\n * A segment that's already full of real digits — e.g. \"23:00\", both digits\n * genuinely typed — doesn't match here and falls through to the default\n * {@link applyDecimalMask}, which already drops the overflow keystroke\n * (typing a 3rd real digit leaves \"23:00\" unchanged).\n *\n * `value`/`caret` must be the state *after* the browser has already\n * inserted `digit` at `caret - 1` (the same post-insertion snapshot\n * `applyDecimalMask` itself expects from `bindDecimal`). Returns `null`\n * when the pattern doesn't apply, so the caller falls through to a plain\n * {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskReplacingLoneZero(\n value: string,\n caret: number,\n digit: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n const insertIdx = caret - 1\n if (insertIdx < 0 || value[insertIdx] !== digit) return null\n\n const withoutDigit = value.slice(0, insertIdx) + value.slice(caret)\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(withoutDigit, opts)\n\n const realDigits = stripLeadingZeros(intDigits || '0')\n const hasRealDigits = realDigits !== '0'\n const hasPaddingRoom =\n hasRealDigits && opts.numberPlaces != null && realDigits.length < opts.numberPlaces\n if (hasRealDigits && !hasPaddingRoom) return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + intDigits.length) return null\n\n const signPart = isNegative ? '-' : ''\n const newIntDigits = (hasRealDigits ? realDigits : '') + digit\n const raw = signPart + newIntDigits + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + newIntDigits.length, opts)\n}\n\n/**\n * Format a plain JS number into its masked display string. With a fixed\n * `decimalPlaces` the fraction is rounded/padded to that exact width, even\n * for a whole number; left unset, the fraction is only shown when the value\n * actually has one, with as many digits as `value` naturally carries (no\n * padding, no rounding).\n */\nexport function formatDecimalValue(value: number, options?: DecimalMaskOptions): string {\n const opts = resolveDecimalOptions(options)\n if (!Number.isFinite(value)) return ''\n\n const isNegative = opts.allowNegative && value < 0\n const abs = Math.abs(value)\n const fixed = opts.decimalPlaces != null ? abs.toFixed(opts.decimalPlaces) : String(abs)\n const dotIdx = fixed.indexOf('.')\n const intRaw = dotIdx === -1 ? fixed : fixed.slice(0, dotIdx)\n const fracPart = dotIdx === -1 ? '' : fixed.slice(dotIdx + 1)\n const intPart = stripLeadingZeros(intRaw || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const showFraction = opts.decimalPlaces === 0 ? false : fracPart !== ''\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPart : '')\n\n return (isNegative ? '-' : '') + opts.prefix + numberStr + opts.suffix\n}\n","import {\n applyDecimalMask,\n applyDecimalMaskReplacingLoneZero,\n applyDecimalMaskUnmergingSeparator,\n resolveDecimalOptions,\n unmaskDecimal,\n} from './decimal-mask'\nimport { isIos } from './platform'\nimport type { BindDecimalOptions, DecimalMaskOptions, MaskResult } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindDecimalOptions(\n second:\n | BindDecimalOptions\n | ((value: string, numericValue: number) => void)\n | null\n | undefined,\n): BindDecimalOptions {\n if (second == null) return {}\n if (typeof second === 'function') return { onChange: second }\n return second\n}\n\nfunction isDigitKey(key: string): boolean {\n return key.length === 1 && key >= '0' && key <= '9'\n}\n\n/**\n * Bind a decimal/currency mask to an input element.\n *\n * Same contract as {@link bind}: idempotent (marked with `data-masked`),\n * returns a dispose function, and reformats on paste and keyboard-driven\n * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no\n * fixed pattern — the integer part grows and shrinks freely; formatting is\n * driven entirely by `options`.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.\n */\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n options?: BindDecimalOptions | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n onChange: ((value: string, numericValue: number) => void) | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n second?: BindDecimalOptions | ((value: string, numericValue: number) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, ...maskOptions } = toBindDecimalOptions(second)\n const decimalOptions: DecimalMaskOptions = maskOptions\n const { decimalSeparator, decimalPlaces } = resolveDecimalOptions(decimalOptions)\n\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, 'decimal')\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n\n let lockInput = false\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n target.setSelectionRange(m.caret, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n applyResult(target, applyDecimalMask(target.value, target.value.length, decimalOptions))\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n\n // Navigation (arrows, Home/End, Tab, ...), selection, and shortcut keys\n // (Ctrl/Cmd+A, Ctrl/Cmd+C, ...) don't change the text — leave the\n // browser's native caret/selection handling alone instead of recomputing\n // and overwriting it on every keystroke.\n if (!isBackspace && !isDelete && !isCharInsert) return\n\n // Everything below reads `target.value`/`selectionStart` inside the rAF\n // callback rather than synchronously here, since the browser's native\n // character insertion for this keystroke isn't guaranteed to have landed\n // yet at the point a keydown listener runs — only by the next frame.\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n\n // Backspace that just deleted the decimal separator merges the\n // integer and fraction digit runs into one continuous stream —\n // restore the boundary instead of treating that as one big integer.\n if (isBackspace) {\n const unmerged = applyDecimalMaskUnmergingSeparator(target.value, decimalOptions)\n if (unmerged) {\n applyResult(target, unmerged)\n return\n }\n }\n\n // A numeric keypad (or a locale mismatch) may only offer \".\" or \",\".\n // Normalize whichever one the user just typed to the configured\n // `decimalSeparator` so it reliably opens the fraction segment.\n if (\n decimalPlaces !== 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator &&\n pos > 0 &&\n target.value[pos - 1] === ke.key\n ) {\n target.value = target.value.slice(0, pos - 1) + decimalSeparator + target.value.slice(pos)\n }\n\n // Typing a digit into a field whose integer part isn't yet full of\n // real digits — the auto-inserted \"0\" placeholder, or a\n // `numberPlaces`-padded segment with fewer real digits than its width\n // — extends the real digit stream instead of combining with a\n // padding zero (e.g. \"$0.00\" + \"2\" → \"$2.00\", \"02:00\" + \"4\" →\n // \"24:00\"). A segment already full of real digits (e.g. \"23:00\")\n // falls through to the default mask, which drops the keystroke.\n const replaced = isDigitKey(ke.key)\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, ke.key, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mother-mask.umd.js","names":["isDigitChar","MASKED_ATTR"],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts","../src/decimal-mask.ts","../src/bind-decimal.ts"],"sourcesContent":["import type { ApplyMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction isSlotChar(ch: string): boolean {\n return ch === '9' || ch === 'Z' || ch === 'A'\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Flat masking (default) — treats the mask as one continuous character\n// stream. Best for continuous identifiers (phone numbers, CPF/CNPJ, credit\n// cards) where deleting/inserting a digit anywhere is expected to reflow\n// every digit after it — this is the classic mother-mask behavior and is\n// relied on by the majority of the test suite (paste, backspace, mid-string\n// insert, etc).\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nfunction applyFlatMask(value: string, mask: string, inputCaret: number): MaskResult {\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Segmented masking (opt-in) — treats literal separators as hard boundaries\n// between independent fields (e.g. day/month/year in \"99/99/9999\"). Editing\n// one segment never bleeds characters into a neighboring one, so replacing\n// the \"12\" in \"25/12/2025\" with a shorter or longer value keeps the year\n// exactly where it is instead of shifting digits across the \"/\".\n// ---------------------------------------------------------------------------\n\ntype MaskToken = { kind: 'literal'; text: string } | { kind: 'slots'; chars: string }\n\n// A bound input re-applies the same (static) mask string on every keystroke,\n// so tokenizing it is pure, repeated work — cache by mask string instead of\n// re-walking and re-allocating tokens on every keystroke.\nconst tokenCache = new Map<string, MaskToken[]>()\n\n/**\n * Split a mask into alternating literal and slot-run tokens (e.g. \"99/99/9999\"\n * → slots\"99\", literal\"/\", slots\"99\", literal\"/\", slots\"9999\"), so the masking\n * pass can reason about segment boundaries instead of a flat character stream.\n */\nfunction tokenizeMask(mask: string): MaskToken[] {\n const cached = tokenCache.get(mask)\n if (cached) return cached\n\n const tokens: MaskToken[] = []\n let i = 0\n while (i < mask.length) {\n const start = i\n const wantSlots = isSlotChar(mask[i])\n while (i < mask.length && isSlotChar(mask[i]) === wantSlots) i++\n const text = mask.slice(start, i)\n tokens.push(wantSlots ? { kind: 'slots', chars: text } : { kind: 'literal', text })\n }\n tokenCache.set(mask, tokens)\n return tokens\n}\n\n/** Total slot capacity from token index `from` (inclusive) to the end of `tokens`. */\nfunction slotCapacityFrom(tokens: MaskToken[], from: number): number {\n let capacity = 0\n for (let i = from; i < tokens.length; i++) {\n const token = tokens[i]\n if (token.kind === 'slots') capacity += token.chars.length\n }\n return capacity\n}\n\n/** Count of remaining slot-matchable (digit/letter) characters in `value` from `fromIdx` onward. */\nfunction remainingDataChars(value: string, fromIdx: number): number {\n let count = 0\n for (let i = fromIdx; i < value.length; i++) {\n const ch = value[i]\n if (isDigitChar(ch) || isLetterChar(ch)) count++\n }\n return count\n}\n\n/**\n * Same contract as {@link applyFlatMask}, but walks the mask one *segment* at\n * a time (a run of slots, or a literal) rather than one character at a time.\n * The rule that keeps an edit inside a segment from crossing into its\n * neighbor is an **early stop**: if the value hits the literal that ends the\n * current slot run before all of that run's slots are filled (e.g. only one\n * digit typed into a two-digit month slot), the run is left partially filled\n * instead of skipping past the separator to steal a digit from the next\n * segment.\n *\n * That stop is only taken when it's actually safe — i.e. every character\n * still to come in `value` fits in the slot capacity that remains *after*\n * this segment. If stopping here would strand more data than the rest of\n * the mask can hold (e.g. pasting into a later segment while an earlier one\n * still sits under-filled from before), the \"separator\" is treated as stray\n * noise instead and skipped, letting this segment take the extra slot it\n * needs so nothing at the end gets silently dropped. This is also what lets\n * an array mask grow or shrink its pattern (moving every literal after the\n * change point) reflow correctly instead of losing a digit at the boundary.\n */\nfunction applySegmentedMask(value: string, mask: string, inputCaret: number): MaskResult {\n const tokens = tokenizeMask(mask)\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n let exhausted = false\n\n for (let t = 0; t < tokens.length && !exhausted; t++) {\n const token = tokens[t]\n\n if (token.kind === 'literal') {\n pending += token.text\n if (value.startsWith(token.text, valueIdx)) valueIdx += token.text.length\n continue\n }\n\n const nextToken = tokens[t + 1]\n const capacityAfter = slotCapacityFrom(tokens, t + 1)\n\n for (let s = 0; s < token.chars.length; s++) {\n const slotCh = token.chars[s]\n let found = false\n\n while (valueIdx < value.length) {\n const ch = value[valueIdx]\n\n if (matchesSlot(ch, slotCh)) {\n valueIdx++\n output += pending + ch\n pending = ''\n found = true\n\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n\n // This segment's ending separator showed up before the run filled.\n // Stopping here is only safe if the rest of the value still fits in\n // whatever slot capacity remains after this segment — otherwise\n // stopping would strand data, so fall through and skip this char as\n // noise instead, letting the segment take the slot it needs.\n if (\n nextToken?.kind === 'literal' &&\n value.startsWith(nextToken.text, valueIdx) &&\n remainingDataChars(value, valueIdx) <= capacityAfter\n ) {\n break\n }\n\n valueIdx++ // stray/noise char — skip it\n }\n\n if (!found) {\n if (valueIdx >= value.length) exhausted = true\n break\n }\n }\n }\n\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\nexport function applyMask(\n value: string,\n mask: string,\n inputCaret = 0,\n options?: ApplyMaskOptions,\n): MaskResult {\n if (!value) return { value: '', caret: 0 }\n return options?.segmented === false\n ? applyFlatMask(value, mask, inputCaret)\n : applySegmentedMask(value, mask, inputCaret)\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { ApplyMaskOptions, MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n private readonly _options: ApplyMaskOptions | undefined\n\n constructor(value: string, mask: string, caret = 0, options?: ApplyMaskOptions) {\n this._value = value\n this._mask = mask\n this.caret = caret\n this._options = options\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret, this._options)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(\n value: string,\n mask: MaskPattern,\n caret = 0,\n options?: ApplyMaskOptions,\n): Mask {\n return new Mask(value, resolveMask(value, mask), caret, options)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern, options?: ApplyMaskOptions): string {\n return buildMask(value, mask, 0, options).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, segmented } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n // Computed once here rather than inside `onKey` — the mask never changes\n // for the lifetime of this binding, and `onKey` is a synchronous, hot,\n // input-blocking path run on every keystroke.\n const maxLength = getMaxLength(mask)\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(maxLength))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n const m = buildMask(target.value, mask, 0, { segmented })\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= maxLength && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n scheduleFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos, { segmented })\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n","import type { DecimalMaskOptions, MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — mirrors apply-mask.ts)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\n// ---------------------------------------------------------------------------\n// Option resolution\n// ---------------------------------------------------------------------------\n\ninterface ResolvedDecimalOptions {\n /**\n * `undefined` means an optional, uncapped fraction (default) — the\n * decimal separator and fraction only appear once the user actually types\n * them, and there's no limit on how many digits follow. `0` means no\n * fraction at all. A positive number is a fixed, zero-padded width that's\n * always shown, even before the user types anything.\n */\n decimalPlaces: number | undefined\n /** `undefined` means unlimited (default) — the integer part grows freely. */\n numberPlaces: number | undefined\n segmented: boolean\n separator: string\n decimalSeparator: string\n prefix: string\n suffix: string\n allowNegative: boolean\n}\n\n/** @internal exported for {@link bindDecimal}'s \".\" / \",\" key normalization */\nexport function resolveDecimalOptions(options?: DecimalMaskOptions): ResolvedDecimalOptions {\n const rawPlaces = options?.decimalPlaces\n const decimalPlaces =\n rawPlaces != null && Number.isFinite(rawPlaces) ? Math.max(0, Math.floor(rawPlaces)) : undefined\n const rawNumberPlaces = options?.numberPlaces\n const numberPlaces =\n rawNumberPlaces != null && Number.isFinite(rawNumberPlaces)\n ? Math.max(1, Math.floor(rawNumberPlaces))\n : undefined\n return {\n decimalPlaces,\n numberPlaces,\n segmented: options?.segmented ?? true,\n separator: options?.separator ?? ',',\n decimalSeparator: options?.decimalSeparator ?? '.',\n prefix: options?.prefix ?? '',\n suffix: options?.suffix ?? '',\n allowNegative: options?.allowNegative ?? false,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Digit-stream helpers\n// ---------------------------------------------------------------------------\n\n/** Strip leading zeros from a digit string, always keeping at least one digit. */\nfunction stripLeadingZeros(s: string): string {\n let i = 0\n while (i < s.length - 1 && s[i] === '0') i++\n return s.slice(i)\n}\n\n/** Insert `sep` every 3 digits from the right (e.g. \"1234567\" → \"1,234,567\"). */\nfunction groupThousands(s: string, sep: string): string {\n if (!sep || s.length <= 3) return s\n const parts: string[] = []\n let i = s.length\n while (i > 3) {\n parts.unshift(s.slice(i - 3, i))\n i -= 3\n }\n parts.unshift(s.slice(0, i))\n return parts.join(sep)\n}\n\n/**\n * Find the position in `s` that leaves exactly `digitsBefore` digit\n * characters preceding it — the position immediately after that digit and\n * before any subsequent literal (grouping separator, ...), so the caret\n * stays glued to the last digit the user placed there.\n */\nfunction caretForDigitsBefore(s: string, digitsBefore: number): number {\n if (digitsBefore <= 0) return 0\n let count = 0\n for (let i = 0; i < s.length; i++) {\n if (isDigitChar(s[i])) {\n count++\n if (count === digitsBefore) return i + 1\n }\n }\n return s.length\n}\n\n// ---------------------------------------------------------------------------\n// Segmented parsing — integer digits before the decimal separator, fraction\n// digits after. Unlike a slot-pattern mask, the integer segment has no fixed\n// length. The fraction is only fixed-width when `decimalPlaces` is set to a\n// positive number — zero-padded on the right so a shorter fraction reads as\n// its low-order (trailing) digits being zero rather than reflowing/shifting\n// (e.g. editing \"423,42\" down to \"423,4\" produces \"423,40\", not \"42,34\").\n// When `decimalPlaces` is left unset, the fraction is optional and uncapped:\n// it only exists once the user types the separator, and grows to however\n// many digits they type.\n//\n// The decimal separator only has meaning as the *first* occurrence of\n// `opts.decimalSeparator`; every other non-digit character (thousands\n// separator, prefix/suffix text, a second stray separator, ...) is noise and\n// is dropped. This keeps re-parsing an already-masked value idempotent.\n// ---------------------------------------------------------------------------\n\ninterface DecimalParts {\n isNegative: boolean\n intDigits: string\n fracDigits: string\n hasSeparator: boolean\n}\n\nfunction computeDecimalParts(raw: string, opts: ResolvedDecimalOptions): DecimalParts {\n let intDigits = ''\n let fracDigits = ''\n let isNegative = false\n let inFraction = false\n const canHaveFraction = opts.decimalPlaces !== 0\n\n for (const ch of raw) {\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || fracDigits.length < opts.decimalPlaces) fracDigits += ch\n } else if (opts.numberPlaces == null || intDigits.length < opts.numberPlaces) {\n intDigits += ch\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n continue\n }\n if (ch === '-' && opts.allowNegative) isNegative = true\n // Anything else — thousands separator, prefix/suffix text, a repeated\n // separator, stray letters — is noise and is dropped.\n }\n\n return { isNegative, intDigits, fracDigits, hasSeparator: inFraction }\n}\n\n/**\n * Walk `raw[0:caret]` to find which segment the caret sits in (integer or\n * fraction) and how many digits of that segment precede it, so the same\n * position can be re-derived in the freshly formatted output.\n */\nfunction locateCaretSegment(\n raw: string,\n caret: number,\n opts: ResolvedDecimalOptions,\n): { inFraction: boolean; digitsBefore: number } {\n const canHaveFraction = opts.decimalPlaces !== 0\n let inFraction = false\n let digitsBefore = 0\n\n for (let i = 0; i < caret; i++) {\n const ch = raw[i]\n if (isDigitChar(ch)) {\n if (inFraction) {\n if (opts.decimalPlaces == null || digitsBefore < opts.decimalPlaces) digitsBefore++\n } else if (opts.numberPlaces == null || digitsBefore < opts.numberPlaces) {\n digitsBefore++\n }\n continue\n }\n if (canHaveFraction && !inFraction && ch === opts.decimalSeparator) {\n inFraction = true\n digitsBefore = 0\n }\n }\n\n return { inFraction, digitsBefore }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry points\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a decimal/currency mask to a value, producing the masked output and\n * a computed caret position. Digits typed before the decimal separator\n * extend the integer part; the fraction only starts once the separator is\n * typed. With a fixed `decimalPlaces` it's always displayed zero-padded to\n * that width, even before the user types it; left unset, the fraction is\n * optional — it only appears once the separator is typed, and is shown\n * exactly as typed (no padding, no cap on how many digits).\n */\nexport function applyDecimalMask(\n value: string,\n inputCaret = 0,\n options?: DecimalMaskOptions,\n): MaskResult {\n const opts = resolveDecimalOptions(options)\n if (!value) return { value: '', caret: 0 }\n\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (intDigits === '' && fracDigits === '' && !hasSeparator) return { value: '', caret: 0 }\n\n const intPart = stripLeadingZeros(intDigits || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const fracPadded =\n opts.decimalPlaces != null && opts.decimalPlaces > 0\n ? fracDigits.padEnd(opts.decimalPlaces, '0')\n : fracDigits\n const showFraction = opts.decimalPlaces === 0 ? false : opts.decimalPlaces != null || hasSeparator\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPadded : '')\n const signStr = isNegative ? '-' : ''\n const output = signStr + opts.prefix + numberStr + opts.suffix\n\n const clampedCaret = Math.max(0, Math.min(inputCaret, value.length))\n const { inFraction, digitsBefore } = locateCaretSegment(value, clampedCaret, opts)\n const prefixLen = signStr.length + opts.prefix.length\n // Left-padding zeros are synthetic — prepended ahead of every real typed\n // digit — so they shift where the caret's `digitsBefore`-th real digit\n // lands in `groupedInt` by the padding's width.\n const padLength = paddedInt.length - intPart.length\n const caret = inFraction\n ? prefixLen + groupedInt.length + opts.decimalSeparator.length + digitsBefore\n : prefixLen + caretForDigitsBefore(groupedInt, digitsBefore + padLength)\n\n return { value: output, caret }\n}\n\n/** Apply a decimal mask to a raw value and return just the masked string. */\nexport function processDecimal(value: string, options?: DecimalMaskOptions): string {\n return applyDecimalMask(value, value.length, options).value\n}\n\n/**\n * Parse a raw or already-masked decimal value back into a JS number.\n * Ignores prefix/suffix/thousands separator; returns `0` for an empty or\n * digit-less value.\n */\nexport function unmaskDecimal(value: string, options?: DecimalMaskOptions): number {\n const opts = resolveDecimalOptions(options)\n const { isNegative, intDigits, fracDigits } = computeDecimalParts(value, opts)\n const n = Number(fracDigits ? `${intDigits || '0'}.${fracDigits}` : intDigits || '0')\n return isNegative ? -n : n\n}\n\n/**\n * After a Backspace removes the decimal separator itself, the integer and\n * fraction digit runs collapse into one continuous stream (e.g. \"25.00\"\n * with the caret right after \".\" → Backspace deletes the \".\" → \"2500\").\n * Left alone, that reads as one big integer (\"$2,500.00\"). This restores\n * the segment boundary instead: the trailing `decimalPlaces` digits are\n * still the fraction, and the digit right before them — the one that used\n * to sit at the end of the integer part — is the one Backspace actually\n * removed, so it's dropped (not kept) — \"$25.00\" → \"$2.00\".\n *\n * Only applies when `decimalPlaces` is a fixed positive number — that's\n * what \"the trailing N digits are the fraction\" means. Every reformat\n * re-appends `decimalSeparator` in that case, so its absence from `value`\n * is an unambiguous signal that this exact keystroke just deleted it — no\n * \"value before this keystroke\" snapshot is needed. With `decimalPlaces`\n * unset (optional, uncapped fraction) there's no fixed width to reconstruct\n * from, so the merged digits are left as one continuous integer instead —\n * the same reasoning `numberPlaces` uses for an unbounded integer part.\n * Returns `null` when there's nothing to restore (the separator is still\n * present, `decimalPlaces` is `0` or unset, or too few digits remain), so\n * the caller falls through to a plain {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskUnmergingSeparator(\n value: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n if (opts.decimalPlaces == null || opts.decimalPlaces <= 0) return null\n\n const { isNegative, intDigits, hasSeparator } = computeDecimalParts(value, opts)\n if (hasSeparator || intDigits.length < opts.decimalPlaces + 1) return null\n\n const preMergeIntLength = intDigits.length - opts.decimalPlaces\n const fracDigits = intDigits.slice(preMergeIntLength)\n const remainingInt = intDigits.slice(0, preMergeIntLength - 1)\n\n const signPart = isNegative ? '-' : ''\n const raw = signPart + remainingInt + opts.decimalSeparator + fracDigits\n return applyDecimalMask(raw, signPart.length + remainingInt.length, opts)\n}\n\n/**\n * Special-cases typing a single digit into an integer segment that isn't\n * yet full of *real* digits — either because it's still the auto-inserted\n * zero placeholder (\"0\", or a wider \"00\" from a `numberPlaces`-padded field\n * that hasn't been touched), or because `numberPlaces` is left-padding a\n * partially-typed segment with synthetic zeros (e.g. \"02\" is really just\n * the one real digit \"2\", padded out to width 2 for display). Those padding\n * zeros aren't editable content, so the new digit extends the real digit\n * stream instead of combining with a padding zero at the caret:\n *\n * - \"$0.00\" + \"2\" → \"$2.00\" (not \"$20.00\")\n * - a `numberPlaces: 2` time field's untouched \"00:00\" + \"5\" → \"05:00\"\n * - that same field's \"02:00\" (one real digit, one padding zero) + \"4\" →\n * \"24:00\" — the real \"2\" is kept, the padding \"0\" is not\n *\n * A segment that's already full of real digits — e.g. \"23:00\", both digits\n * genuinely typed — doesn't match here and falls through to the default\n * {@link applyDecimalMask}, which already drops the overflow keystroke\n * (typing a 3rd real digit leaves \"23:00\" unchanged).\n *\n * `value`/`caret` must be the state *after* the browser has already\n * inserted `digit` at `caret - 1` (the same post-insertion snapshot\n * `applyDecimalMask` itself expects from `bindDecimal`). Returns `null`\n * when the pattern doesn't apply, so the caller falls through to a plain\n * {@link applyDecimalMask} call.\n */\nexport function applyDecimalMaskReplacingLoneZero(\n value: string,\n caret: number,\n digit: string,\n options?: DecimalMaskOptions,\n): MaskResult | null {\n const opts = resolveDecimalOptions(options)\n const insertIdx = caret - 1\n if (insertIdx < 0 || value[insertIdx] !== digit) return null\n\n const withoutDigit = value.slice(0, insertIdx) + value.slice(caret)\n const { isNegative, intDigits, fracDigits, hasSeparator } = computeDecimalParts(withoutDigit, opts)\n\n const realDigits = stripLeadingZeros(intDigits || '0')\n const hasRealDigits = realDigits !== '0'\n const hasPaddingRoom =\n hasRealDigits && opts.numberPlaces != null && realDigits.length < opts.numberPlaces\n if (hasRealDigits && !hasPaddingRoom) return null\n\n const prefixLen = (isNegative ? 1 : 0) + opts.prefix.length\n if (insertIdx < prefixLen || insertIdx > prefixLen + intDigits.length) return null\n\n const signPart = isNegative ? '-' : ''\n const newIntDigits = (hasRealDigits ? realDigits : '') + digit\n const raw = signPart + newIntDigits + (hasSeparator ? opts.decimalSeparator + fracDigits : '')\n return applyDecimalMask(raw, signPart.length + newIntDigits.length, opts)\n}\n\n/**\n * Format a plain JS number into its masked display string. With a fixed\n * `decimalPlaces` the fraction is rounded/padded to that exact width, even\n * for a whole number; left unset, the fraction is only shown when the value\n * actually has one, with as many digits as `value` naturally carries (no\n * padding, no rounding).\n */\nexport function formatDecimalValue(value: number, options?: DecimalMaskOptions): string {\n const opts = resolveDecimalOptions(options)\n if (!Number.isFinite(value)) return ''\n\n const isNegative = opts.allowNegative && value < 0\n const abs = Math.abs(value)\n const fixed = opts.decimalPlaces != null ? abs.toFixed(opts.decimalPlaces) : String(abs)\n const dotIdx = fixed.indexOf('.')\n const intRaw = dotIdx === -1 ? fixed : fixed.slice(0, dotIdx)\n const fracPart = dotIdx === -1 ? '' : fixed.slice(dotIdx + 1)\n const intPart = stripLeadingZeros(intRaw || '0')\n const paddedInt = opts.numberPlaces != null ? intPart.padStart(opts.numberPlaces, '0') : intPart\n\n const groupedInt = opts.segmented ? groupThousands(paddedInt, opts.separator) : paddedInt\n const showFraction = opts.decimalPlaces === 0 ? false : fracPart !== ''\n const numberStr = groupedInt + (showFraction ? opts.decimalSeparator + fracPart : '')\n\n return (isNegative ? '-' : '') + opts.prefix + numberStr + opts.suffix\n}\n","import {\n applyDecimalMask,\n applyDecimalMaskReplacingLoneZero,\n applyDecimalMaskUnmergingSeparator,\n resolveDecimalOptions,\n unmaskDecimal,\n} from './decimal-mask'\nimport { isIos } from './platform'\nimport type { BindDecimalOptions, DecimalMaskOptions, MaskResult } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindDecimalOptions(\n second:\n | BindDecimalOptions\n | ((value: string, numericValue: number) => void)\n | null\n | undefined,\n): BindDecimalOptions {\n if (second == null) return {}\n if (typeof second === 'function') return { onChange: second }\n return second\n}\n\nfunction isDigitKey(key: string): boolean {\n return key.length === 1 && key >= '0' && key <= '9'\n}\n\n/**\n * Bind a decimal/currency mask to an input element.\n *\n * Same contract as {@link bind}: idempotent (marked with `data-masked`),\n * returns a dispose function, and reformats on paste and keyboard-driven\n * changes via `requestAnimationFrame`. Unlike the pattern masks, there is no\n * fixed pattern — the integer part grows and shrinks freely; formatting is\n * driven entirely by `options`.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param options - `DecimalMaskOptions` plus an optional `onChange`, or pass a callback (legacy) directly.\n */\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n options?: BindDecimalOptions | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n onChange: ((value: string, numericValue: number) => void) | null,\n): () => void\nexport function bindDecimal(\n input: HTMLInputElement | Element,\n second?: BindDecimalOptions | ((value: string, numericValue: number) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange, ...maskOptions } = toBindDecimalOptions(second)\n const decimalOptions: DecimalMaskOptions = maskOptions\n const { decimalSeparator, decimalPlaces } = resolveDecimalOptions(decimalOptions)\n\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, 'decimal')\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n\n let lockInput = false\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n // requestAnimationFrame callbacks scheduled below outlive a single keystroke\n // handler and close over `target` (the input element). If `dispose()` runs\n // before a frame fires — e.g. the field unmounts right after the user types\n // — the uncancelled callback keeps that element (and this closure) alive\n // until the next paint, which can be a very long time on a backgrounded\n // tab. Track every scheduled frame so dispose can cancel what's pending.\n const pendingFrames = new Set<number>()\n const scheduleFrame = (callback: () => void): void => {\n const id = requestAnimationFrame(() => {\n pendingFrames.delete(id)\n callback()\n })\n pendingFrames.add(id)\n }\n\n const applyResult = (target: HTMLInputElement, m: MaskResult): void => {\n target.value = m.value\n target.setSelectionRange(m.caret, m.caret)\n onChange?.(m.value, unmaskDecimal(m.value, decimalOptions))\n }\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n scheduleFrame(() => {\n applyResult(target, applyDecimalMask(target.value, target.value.length, decimalOptions))\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n applyResult(target, applyDecimalMask(target.value, pos, decimalOptions))\n scheduleFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n\n // Navigation (arrows, Home/End, Tab, ...), selection, and shortcut keys\n // (Ctrl/Cmd+A, Ctrl/Cmd+C, ...) don't change the text — leave the\n // browser's native caret/selection handling alone instead of recomputing\n // and overwriting it on every keystroke.\n if (!isBackspace && !isDelete && !isCharInsert) return\n\n // Everything below reads `target.value`/`selectionStart` inside the rAF\n // callback rather than synchronously here, since the browser's native\n // character insertion for this keystroke isn't guaranteed to have landed\n // yet at the point a keydown listener runs — only by the next frame.\n scheduleFrame(() => {\n const pos = target.selectionStart ?? target.value.length\n\n // Backspace that just deleted the decimal separator merges the\n // integer and fraction digit runs into one continuous stream —\n // restore the boundary instead of treating that as one big integer.\n if (isBackspace) {\n const unmerged = applyDecimalMaskUnmergingSeparator(target.value, decimalOptions)\n if (unmerged) {\n applyResult(target, unmerged)\n return\n }\n }\n\n // A numeric keypad (or a locale mismatch) may only offer \".\" or \",\".\n // Normalize whichever one the user just typed to the configured\n // `decimalSeparator` so it reliably opens the fraction segment.\n if (\n decimalPlaces !== 0 &&\n (ke.key === '.' || ke.key === ',') &&\n ke.key !== decimalSeparator &&\n pos > 0 &&\n target.value[pos - 1] === ke.key\n ) {\n target.value = target.value.slice(0, pos - 1) + decimalSeparator + target.value.slice(pos)\n }\n\n // Typing a digit into a field whose integer part isn't yet full of\n // real digits — the auto-inserted \"0\" placeholder, or a\n // `numberPlaces`-padded segment with fewer real digits than its width\n // — extends the real digit stream instead of combining with a\n // padding zero (e.g. \"$0.00\" + \"2\" → \"$2.00\", \"02:00\" + \"4\" →\n // \"24:00\"). A segment already full of real digits (e.g. \"23:00\")\n // falls through to the default mask, which drops the keystroke.\n const replaced = isDigitKey(ke.key)\n ? applyDecimalMaskReplacingLoneZero(target.value, pos, ke.key, decimalOptions)\n : null\n\n applyResult(target, replaced ?? applyDecimalMask(target.value, pos, decimalOptions))\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n for (const id of pendingFrames) cancelAnimationFrame(id)\n pendingFrames.clear()\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mother-mask",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Lightweight input mask library for browsers",
|
|
7
7
|
"author": "Danilo Celestino de Castro <dan2dev>",
|
|
@@ -39,25 +39,16 @@
|
|
|
39
39
|
"test": "vitest run --coverage",
|
|
40
40
|
"test:watch": "vitest --watch --coverage",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
|
-
"lint": "eslint .",
|
|
43
|
-
"lint:fix": "eslint . --fix",
|
|
44
42
|
"release": "bumpp",
|
|
45
43
|
"prepublishOnly": "bun run build"
|
|
46
44
|
},
|
|
47
45
|
"devDependencies": {
|
|
48
|
-
"@
|
|
49
|
-
"@eslint/json": "^1.2.0",
|
|
50
|
-
"@eslint/markdown": "^8.0.3",
|
|
51
|
-
"@types/node": "^25.9.5",
|
|
52
|
-
"@typescript/native-preview": "7.0.0-dev.20260405.1",
|
|
46
|
+
"@types/node": "^26.1.1",
|
|
53
47
|
"@vitest/coverage-v8": "^4.1.10",
|
|
54
|
-
"bumpp": "^
|
|
55
|
-
"eslint": "^10.7.0",
|
|
56
|
-
"globals": "^17.7.0",
|
|
48
|
+
"bumpp": "^12.0.0",
|
|
57
49
|
"jsdom": "^29.1.1",
|
|
58
|
-
"tsdown": "^0.
|
|
59
|
-
"typescript": "^
|
|
60
|
-
"typescript-eslint": "^8.65.0",
|
|
50
|
+
"tsdown": "^0.22.13",
|
|
51
|
+
"typescript": "^7.0.2",
|
|
61
52
|
"vitest": "^4.1.10"
|
|
62
53
|
},
|
|
63
54
|
"packageManager": "pnpm@10.33.0",
|
|
@@ -68,5 +59,5 @@
|
|
|
68
59
|
"bugs": {
|
|
69
60
|
"url": "https://github.com/dan2dev/mother-mask/issues"
|
|
70
61
|
},
|
|
71
|
-
"homepage": "https://github.
|
|
62
|
+
"homepage": "https://dan2dev.github.io/mother-mask/"
|
|
72
63
|
}
|