mother-mask 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # mother-mask
2
2
 
3
- Lightweight input mask library for browsers. Zero dependencies, TypeScript-first, ships ESM + CJS + UMD.
3
+ Lightweight input mask library for browsers. Zero runtime dependencies, TypeScript-first, ships **ESM**, **CJS**, and **UMD**.
4
+
5
+ Published as [`mother-mask` on npm](https://www.npmjs.com/package/mother-mask).
6
+
7
+ ## Live demo
8
+
9
+ **[Try it on StackBlitz →](https://stackblitz.com/edit/mother-mask-simple-demo?file=src%2Fmain.ts)**
4
10
 
5
11
  ## Install
6
12
 
@@ -12,9 +18,13 @@ pnpm add mother-mask
12
18
 
13
19
  ## Usage
14
20
 
15
- ### `bind(input, mask, callback?)` — DOM binding
21
+ ### `bind(input, mask, options?)`
16
22
 
17
- Attach a mask to any input element. Idempotent calling `bind()` twice on the same element has no effect.
23
+ Attach a mask to any input element — this is the main API.
24
+
25
+ - **Idempotent** — calling `bind()` again on the same element does nothing (the element is marked with `data-masked`).
26
+ - **Returns a dispose function** — call it to remove listeners and attributes so you can bind again later.
27
+ - Sets sensible defaults when missing: `autocomplete`, `autocorrect`, `autocapitalize`, `spellcheck`, and `maxlength` from the mask.
18
28
 
19
29
  ```ts
20
30
  import { bind } from 'mother-mask'
@@ -22,48 +32,38 @@ import { bind } from 'mother-mask'
22
32
  const input = document.getElementById('phone') as HTMLInputElement
23
33
 
24
34
  // Fixed mask
25
- bind(input, '(99) 99999-9999')
35
+ const dispose = bind(input, '(99) 99999-9999')
26
36
 
27
- // Dynamic mask — switches automatically as the user types
37
+ // Dynamic mask — picks the pattern from an ordered list (shortest → longest)
28
38
  bind(input, ['(99) 9999-9999', '(99) 99999-9999'])
29
39
 
30
- // Optional callback receives the masked value on every change
40
+ // Callback after paste or keyboard-driven changes
31
41
  bind(input, '999.999.999-99', (value) => {
32
42
  console.log(value) // e.g. "123.456.789-01"
33
43
  })
34
- ```
35
44
 
36
- ### `process(value, mask)` pure function
45
+ // Or options object (same as callback for a single `onChange`)
46
+ bind(input, '999.999.999-99', { onChange: (value) => console.log(value) })
37
47
 
38
- Apply a mask to a raw string without touching the DOM. Useful for formatting stored values.
39
-
40
- ```ts
41
- import { process } from 'mother-mask'
42
-
43
- process('12345678901', '999.999.999-99') // → "123.456.789-01"
44
- process('01012024', '99/99/9999') // → "01/01/2024"
45
- process('01310100', '99999-999') // → "01310-100"
46
- process('1AB2C3D45E6F78', 'AA.AAA.AAA/AAAA-99') // → "1A.B2C.3D4/5E6F-78"
48
+ // Later: allow rebinding
49
+ dispose()
47
50
  ```
48
51
 
49
52
  ## Pattern syntax
50
53
 
51
- | Character | Matches |
52
- |-----------|----------------|
53
- | `9` | Digit (09) |
54
- | `Z` | Letter (az, AZ) |
55
- | `A` | Alphanumeric (0–9, a–z, A–Z) |
56
- | anything else | Literal — inserted automatically |
54
+ | Character | Matches |
55
+ |-----------|---------|
56
+ | `9` | Digit (`0`–`9`) |
57
+ | `Z` | Letter (`a`–`z`, `A`–`Z`) |
58
+ | `A` | Alphanumeric (digit or letter) |
59
+ | Anything else | Literal — inserted as the user fills slots |
57
60
 
58
61
  ## Array masks
59
62
 
60
- Pass an ordered array (shortest → longest) to support variable-length inputs. The mask is selected by comparing the current value length against each mask's total length.
63
+ Pass an ordered array **shortest → longest** for variable-length inputs. The active mask is chosen from the **count of alphanumeric “data” characters** in the current value, so it works for both progressively masked input and fast typing.
61
64
 
62
65
  ```ts
63
- // Brazilian phone: 8-digit → 9-digit landline / mobile
64
66
  bind(input, ['(99) 9999-9999', '(99) 99999-9999'])
65
-
66
- // CPF / CNPJ alfanumérico
67
67
  bind(input, ['999.999.999-99', 'AA.AAA.AAA/AAAA-99'])
68
68
  ```
69
69
 
@@ -72,51 +72,51 @@ bind(input, ['999.999.999-99', 'AA.AAA.AAA/AAAA-99'])
72
72
  ```html
73
73
  <script src="https://unpkg.com/mother-mask/dist/mother-mask.umd.js"></script>
74
74
  <script>
75
- MotherMask.bind(document.getElementById('cpf'), '999.999.999-99')
75
+ const dispose = MotherMask.bind(document.getElementById('cpf'), '999.999.999-99')
76
76
  </script>
77
77
  ```
78
78
 
79
+ The global name is **`MotherMask`**.
80
+
79
81
  ## API reference
80
82
 
81
- ```ts
82
- // Apply mask to a string — no DOM required
83
- process(value: string, mask: MaskPattern): string
83
+ ### `bind` (primary)
84
84
 
85
- // Bind a mask to an input element (idempotent)
86
- bind(
87
- input: HTMLInputElement | Element,
88
- mask: MaskPattern,
89
- callback?: ((value: string) => void) | null,
90
- ): void
85
+ | | |
86
+ |--|--|
87
+ | **Signature** | `bind(input, mask, options?)` |
88
+ | **Returns** | `() => void` — call to remove listeners and attributes so the input can be bound again |
89
+ | **Third argument** | `{ onChange?: (value: string) => void }`, or a legacy `(value) => void` callback |
91
90
 
92
- // Build a Mask instance directly (advanced use)
93
- buildMask(value: string, mask: MaskPattern, caret?: number): Mask
91
+ ### Other exports
94
92
 
95
- // Maximum allowed input length for a given mask
96
- getMaxLength(mask: MaskPattern): number
93
+ | Export | Description |
94
+ |--------|-------------|
95
+ | `buildMask(value, mask, caret?)` | Build a `Mask` instance (array `mask` is resolved to one string first). |
96
+ | `getMaxLength(mask)` | Maximum string length for the mask (for array masks, the longest pattern). |
97
+ | `applyMask(value, mask, inputCaret?)` | Low-level: apply a **single** mask string; returns `{ value, caret }`. |
97
98
 
98
- // Pattern type
99
- type MaskPattern = string | string[]
100
- ```
99
+ ### `Mask` class
101
100
 
102
- ## Development
101
+ `buildMask` returns a `Mask` for advanced use. The instance applies the pattern and keeps a `caret` position aligned with the masked output (see TypeScript definitions in the package).
103
102
 
104
- ```bash
105
- make install # install dependencies
106
- make test # run tests + coverage
107
- make build # build ESM + CJS + UMD
108
- make dev # watch mode
109
- make lint # lint source files
110
- ```
103
+ ### Types
111
104
 
112
- ## Release
105
+ ```ts
106
+ type MaskPattern = string | string[]
113
107
 
114
- ```bash
115
- make publish # bump patch, publish, commit, tag, push
116
- make publish BUMP=minor
117
- make publish BUMP=major
108
+ interface MaskResult {
109
+ readonly value: string
110
+ readonly caret: number
111
+ }
112
+
113
+ interface BindOptions {
114
+ onChange?: (value: string) => void
115
+ }
118
116
  ```
119
117
 
118
+ `MaskPattern`, `MaskResult`, and `BindOptions` are exported as types.
119
+
120
120
  ## License
121
121
 
122
122
  MIT — [Danilo Celestino de Castro](https://github.com/dan2dev)
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function r(e,t){if(!Array.isArray(t))return t;let n=0;for(;n<t.length-1&&e.length>t[n].length;)n++;return t[n]}function i(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}function a(e,t,r=0){if(!e)return{value:``,caret:0};let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(n(t,u)){i+=a+t,a=``,d=!0,c||(o<=r?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}var o=class{caret;_value;_mask;constructor(e,t,n=0){this._value=e,this._mask=t,this.caret=n}process(){let e=a(this._value,this._mask,this.caret);return this.caret=e.caret,e.value}};function s(e,t,n=0){return new o(e,r(e,t),n)}function c(e,t){return s(e,t).process()}const l=`data-masked`;let u;function d(){return u===void 0&&(u=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),u}function f(e,t,n=null){if(e.getAttribute(l)!==null)return;e.setAttribute(l,Array.isArray(t)?t.join(`|`):t),e.setAttribute(`autocomplete`,`off`),e.setAttribute(`autocorrect`,`off`),e.setAttribute(`autocapitalize`,`off`),e.setAttribute(`spellcheck`,`false`),e.setAttribute(`maxlength`,String(i(t)));let r=!1;e.addEventListener(`paste`,e=>{let r=e.target;requestAnimationFrame(()=>{r.value=s(r.value,t).process(),n?.(r.value)})}),e.addEventListener(d()?`keyup`:`keydown`,e=>{let a=e,o=a.target,c=o.value;if(!a.key){r=!0,requestAnimationFrame(()=>{let e=o.selectionStart??999,n=s(o.value,t,e);o.value=n.process(),o.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{r=!1})});return}if(a.key===`Meta`)return;let l=a.key===`Backspace`,u=a.key===`Delete`,f=a.key.length===1&&!a.ctrlKey&&!a.altKey&&!a.metaKey,p=a.key===`Unidentified`;if(f&&o.selectionStart===o.selectionEnd&&c.length>=i(t)&&!d()){a.preventDefault();return}if(r){a.preventDefault();return}r=!0,requestAnimationFrame(()=>{let e=o.selectionStart??999,i=s(o.value,t,e);if(o.value=i.process(),p){let t=o.value.length>c.length?i.caret:e;o.setSelectionRange(t,t)}else if(u){let t=c.length===o.value.length?e+1:e;o.setSelectionRange(t,t)}else l?o.setSelectionRange(e,e):f&&o.setSelectionRange(i.caret,i.caret);n?.(o.value),requestAnimationFrame(()=>{r=!1})})})}exports.Mask=o,exports.applyMask=a,exports.bind=f,exports.buildMask=s,exports.getMaxLength=i,exports.process=c;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function r(e,t,r=0){if(!e)return{value:``,caret:0};let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(n(t,u)){i+=a+t,a=``,d=!0,c||(o<=r?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}function i(e){let t=0;for(let n of e)(n>=`0`&&n<=`9`||n>=`a`&&n<=`z`||n>=`A`&&n<=`Z`)&&t++;return t}function a(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function o(e,t){if(!Array.isArray(t))return t;let n=i(e),r=0;for(;r<t.length-1&&n>a(t[r]);)r++;return t[r]}function s(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var c=class{caret;_value;_mask;constructor(e,t,n=0){this._value=e,this._mask=t,this.caret=n}process(){let e=r(this._value,this._mask,this.caret);return this.caret=e.caret,e.value}};function l(e,t,n=0){return new c(e,o(e,t),n)}function u(e,t){return l(e,t).process()}let d;function f(){return d===void 0&&(d=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),d}const p=`data-masked`;function m(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function h(e,t,n){if(e.getAttribute(p)!==null)return()=>{};let{onChange:r}=m(n),i=[],a=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),i.push(t))};e.setAttribute(p,Array.isArray(t)?t.join(`|`):t),a(`autocomplete`,`off`),a(`autocorrect`,`off`),a(`autocapitalize`,`off`),a(`spellcheck`,`false`),a(`maxlength`,String(s(t)));let o=!1,c=f()?`keyup`:`keydown`,u=e=>{let n=e.target;requestAnimationFrame(()=>{n.value=l(n.value,t).process(),r?.(n.value)})},d=e=>{let n=e,i=n.target,a=i.value;if(!n.key){o=!0,requestAnimationFrame(()=>{let e=i.selectionStart??999,n=l(i.value,t,e);i.value=n.process(),i.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{o=!1})});return}if(n.key===`Meta`)return;let c=n.key===`Backspace`,u=n.key===`Delete`,d=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,p=n.key===`Unidentified`;if(d&&i.selectionStart===i.selectionEnd&&a.length>=s(t)&&!f()){n.preventDefault();return}if(o){n.preventDefault();return}requestAnimationFrame(()=>{let e=i.selectionStart??999,n=l(i.value,t,e);if(i.value=n.process(),p){let t=i.value.length>a.length?n.caret:e;i.setSelectionRange(t,t)}else if(u){let t=a.length===i.value.length?e+1:e;i.setSelectionRange(t,t)}else c?i.setSelectionRange(e,e):d&&i.setSelectionRange(n.caret,n.caret);r?.(i.value)})};return e.addEventListener(`paste`,u),e.addEventListener(c,d),()=>{e.removeEventListener(`paste`,u),e.removeEventListener(c,d),e.removeAttribute(p);for(let t of i)e.removeAttribute(t)}}exports.Mask=c,exports.applyMask=r,exports.bind=h,exports.buildMask=l,exports.getMaxLength=s,exports.process=u;
2
2
  //# sourceMappingURL=mother-mask.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mother-mask.cjs","names":[],"sources":["../src/mask.ts","../src/bind.ts"],"sourcesContent":["/**\n * Mask pattern — a single pattern string or an array ordered from shortest to longest.\n * `9` matches a digit, `Z` matches a letter, `A` matches alphanumeric (digit or letter),\n * anything else is a literal character.\n *\n * @example\n * '(99) 99999-9999'\n * ['(99) 9999-9999', '(99) 99999-9999']\n * 'AA.AAA.AAA/AAAA-99' // CNPJ alfanumérico\n */\nexport type MaskPattern = string | string[]\n\n/** Result of applying a mask to a value. */\nexport interface MaskResult {\n readonly value: string\n readonly caret: number\n}\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Mask resolution\n// ---------------------------------------------------------------------------\n\n/** Select the right mask string for the current value length. */\nfunction resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n let i = 0\n while (i < mask.length - 1 && value.length > mask[i].length) 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\n// ---------------------------------------------------------------------------\n// Core masking — pure function\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nexport function applyMask(value: string, mask: string, inputCaret = 0): MaskResult {\n if (!value) return { value: '', caret: 0 }\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n\n constructor(value: string, mask: string, caret = 0) {\n this._value = value\n this._mask = mask\n this.caret = caret\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(value: string, mask: MaskPattern, caret = 0): Mask {\n return new Mask(value, resolveMask(value, mask), caret)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern): string {\n return buildMask(value, mask).process()\n}\n","import type { MaskPattern } from './mask'\nimport { buildMask, getMaxLength } from './mask'\n\nconst MASKED_ATTR = 'data-masked'\n\nlet cachedIsIos: boolean | undefined\n\nfunction 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\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 * @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 callback - Optional callback called with the masked value on every change.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n callback: ((value: string) => void) | null = null,\n): void {\n if (input.getAttribute(MASKED_ATTR) !== null) return\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n input.setAttribute('autocomplete', 'off')\n input.setAttribute('autocorrect', 'off')\n input.setAttribute('autocapitalize', 'off')\n input.setAttribute('spellcheck', 'false')\n input.setAttribute('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n input.addEventListener('paste', (e: Event) => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask)\n target.value = m.process()\n callback?.(target.value)\n })\n })\n\n input.addEventListener(isIos() ? 'keyup' : 'keydown', (e: Event) => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n\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 callback?.(target.value)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mother-mask.cjs","names":[],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts"],"sourcesContent":["import type { MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Core masking — pure function\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nexport function applyMask(value: string, mask: string, inputCaret = 0): MaskResult {\n if (!value) return { value: '', caret: 0 }\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n\n constructor(value: string, mask: string, caret = 0) {\n this._value = value\n this._mask = mask\n this.caret = caret\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(value: string, mask: MaskPattern, caret = 0): Mask {\n return new Mask(value, resolveMask(value, mask), caret)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern): string {\n return buildMask(value, mask).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask)\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- //#region src/mask.d.ts
1
+ //#region src/types.d.ts
2
2
  /**
3
3
  * Mask pattern — a single pattern string or an array ordered from shortest to longest.
4
4
  * `9` matches a digit, `Z` matches a letter, `A` matches alphanumeric (digit or letter),
@@ -15,8 +15,13 @@ interface MaskResult {
15
15
  readonly value: string;
16
16
  readonly caret: number;
17
17
  }
18
- /** Maximum allowed input length for the given mask. */
19
- declare function getMaxLength(mask: MaskPattern): number;
18
+ /** Options for {@link bind}. */
19
+ interface BindOptions {
20
+ /** Fires with the masked value after paste or keyboard-driven changes. */
21
+ onChange?: (value: string) => void;
22
+ }
23
+ //#endregion
24
+ //#region src/apply-mask.d.ts
20
25
  /**
21
26
  * Apply a single mask string to a value, producing the masked output and
22
27
  * a computed caret position.
@@ -29,6 +34,29 @@ declare function getMaxLength(mask: MaskPattern): number;
29
34
  * characters that are skipped because they don't match the current slot.
30
35
  */
31
36
  declare function applyMask(value: string, mask: string, inputCaret?: number): MaskResult;
37
+ //#endregion
38
+ //#region src/bind.d.ts
39
+ /**
40
+ * Bind a mask pattern to an input element.
41
+ *
42
+ * Idempotent — calling `bind()` on an already-bound element has no effect.
43
+ * The element receives a `data-masked` attribute marking it as bound.
44
+ *
45
+ * Returns a function that removes listeners and clears `data-masked` so the
46
+ * element can be bound again later.
47
+ *
48
+ * @param input - Any `HTMLInputElement` or `Element` that behaves like one.
49
+ * @param mask - A single pattern string or an ordered array (shortest → longest).
50
+ * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.
51
+ */
52
+ declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, options?: BindOptions | null): () => void;
53
+ declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, onChange: ((value: string) => void) | null): () => void;
54
+ //#endregion
55
+ //#region src/pattern.d.ts
56
+ /** Maximum allowed input length for the given mask. */
57
+ declare function getMaxLength(mask: MaskPattern): number;
58
+ //#endregion
59
+ //#region src/mask.d.ts
32
60
  /** Low-level mask processor. Tracks caret position after masking. */
33
61
  declare class Mask {
34
62
  /** Caret position after `process()` runs. */
@@ -44,18 +72,5 @@ declare function buildMask(value: string, mask: MaskPattern, caret?: number): Ma
44
72
  /** Apply a mask pattern to a raw value string and return the masked result. */
45
73
  declare function process(value: string, mask: MaskPattern): string;
46
74
  //#endregion
47
- //#region src/bind.d.ts
48
- /**
49
- * Bind a mask pattern to an input element.
50
- *
51
- * Idempotent — calling `bind()` on an already-bound element has no effect.
52
- * The element receives a `data-masked` attribute marking it as bound.
53
- *
54
- * @param input - Any `HTMLInputElement` or `Element` that behaves like one.
55
- * @param mask - A single pattern string or an ordered array (shortest → longest).
56
- * @param callback - Optional callback called with the masked value on every change.
57
- */
58
- declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, callback?: ((value: string) => void) | null): void;
59
- //#endregion
60
- export { Mask, type MaskPattern, type MaskResult, applyMask, bind, buildMask, getMaxLength, process };
75
+ export { type BindOptions, Mask, type MaskPattern, type MaskResult, applyMask, bind, buildMask, getMaxLength, process };
61
76
  //# sourceMappingURL=mother-mask.d.cts.map
@@ -1,4 +1,4 @@
1
- //#region src/mask.d.ts
1
+ //#region src/types.d.ts
2
2
  /**
3
3
  * Mask pattern — a single pattern string or an array ordered from shortest to longest.
4
4
  * `9` matches a digit, `Z` matches a letter, `A` matches alphanumeric (digit or letter),
@@ -15,8 +15,13 @@ interface MaskResult {
15
15
  readonly value: string;
16
16
  readonly caret: number;
17
17
  }
18
- /** Maximum allowed input length for the given mask. */
19
- declare function getMaxLength(mask: MaskPattern): number;
18
+ /** Options for {@link bind}. */
19
+ interface BindOptions {
20
+ /** Fires with the masked value after paste or keyboard-driven changes. */
21
+ onChange?: (value: string) => void;
22
+ }
23
+ //#endregion
24
+ //#region src/apply-mask.d.ts
20
25
  /**
21
26
  * Apply a single mask string to a value, producing the masked output and
22
27
  * a computed caret position.
@@ -29,6 +34,29 @@ declare function getMaxLength(mask: MaskPattern): number;
29
34
  * characters that are skipped because they don't match the current slot.
30
35
  */
31
36
  declare function applyMask(value: string, mask: string, inputCaret?: number): MaskResult;
37
+ //#endregion
38
+ //#region src/bind.d.ts
39
+ /**
40
+ * Bind a mask pattern to an input element.
41
+ *
42
+ * Idempotent — calling `bind()` on an already-bound element has no effect.
43
+ * The element receives a `data-masked` attribute marking it as bound.
44
+ *
45
+ * Returns a function that removes listeners and clears `data-masked` so the
46
+ * element can be bound again later.
47
+ *
48
+ * @param input - Any `HTMLInputElement` or `Element` that behaves like one.
49
+ * @param mask - A single pattern string or an ordered array (shortest → longest).
50
+ * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.
51
+ */
52
+ declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, options?: BindOptions | null): () => void;
53
+ declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, onChange: ((value: string) => void) | null): () => void;
54
+ //#endregion
55
+ //#region src/pattern.d.ts
56
+ /** Maximum allowed input length for the given mask. */
57
+ declare function getMaxLength(mask: MaskPattern): number;
58
+ //#endregion
59
+ //#region src/mask.d.ts
32
60
  /** Low-level mask processor. Tracks caret position after masking. */
33
61
  declare class Mask {
34
62
  /** Caret position after `process()` runs. */
@@ -44,18 +72,5 @@ declare function buildMask(value: string, mask: MaskPattern, caret?: number): Ma
44
72
  /** Apply a mask pattern to a raw value string and return the masked result. */
45
73
  declare function process(value: string, mask: MaskPattern): string;
46
74
  //#endregion
47
- //#region src/bind.d.ts
48
- /**
49
- * Bind a mask pattern to an input element.
50
- *
51
- * Idempotent — calling `bind()` on an already-bound element has no effect.
52
- * The element receives a `data-masked` attribute marking it as bound.
53
- *
54
- * @param input - Any `HTMLInputElement` or `Element` that behaves like one.
55
- * @param mask - A single pattern string or an ordered array (shortest → longest).
56
- * @param callback - Optional callback called with the masked value on every change.
57
- */
58
- declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, callback?: ((value: string) => void) | null): void;
59
- //#endregion
60
- export { Mask, type MaskPattern, type MaskResult, applyMask, bind, buildMask, getMaxLength, process };
75
+ export { type BindOptions, Mask, type MaskPattern, type MaskResult, applyMask, bind, buildMask, getMaxLength, process };
61
76
  //# sourceMappingURL=mother-mask.d.mts.map
@@ -1,2 +1,2 @@
1
- function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function r(e,t){if(!Array.isArray(t))return t;let n=0;for(;n<t.length-1&&e.length>t[n].length;)n++;return t[n]}function i(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}function a(e,t,r=0){if(!e)return{value:``,caret:0};let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(n(t,u)){i+=a+t,a=``,d=!0,c||(o<=r?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}var o=class{caret;_value;_mask;constructor(e,t,n=0){this._value=e,this._mask=t,this.caret=n}process(){let e=a(this._value,this._mask,this.caret);return this.caret=e.caret,e.value}};function s(e,t,n=0){return new o(e,r(e,t),n)}function c(e,t){return s(e,t).process()}const l=`data-masked`;let u;function d(){return u===void 0&&(u=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),u}function f(e,t,n=null){if(e.getAttribute(l)!==null)return;e.setAttribute(l,Array.isArray(t)?t.join(`|`):t),e.setAttribute(`autocomplete`,`off`),e.setAttribute(`autocorrect`,`off`),e.setAttribute(`autocapitalize`,`off`),e.setAttribute(`spellcheck`,`false`),e.setAttribute(`maxlength`,String(i(t)));let r=!1;e.addEventListener(`paste`,e=>{let r=e.target;requestAnimationFrame(()=>{r.value=s(r.value,t).process(),n?.(r.value)})}),e.addEventListener(d()?`keyup`:`keydown`,e=>{let a=e,o=a.target,c=o.value;if(!a.key){r=!0,requestAnimationFrame(()=>{let e=o.selectionStart??999,n=s(o.value,t,e);o.value=n.process(),o.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{r=!1})});return}if(a.key===`Meta`)return;let l=a.key===`Backspace`,u=a.key===`Delete`,f=a.key.length===1&&!a.ctrlKey&&!a.altKey&&!a.metaKey,p=a.key===`Unidentified`;if(f&&o.selectionStart===o.selectionEnd&&c.length>=i(t)&&!d()){a.preventDefault();return}if(r){a.preventDefault();return}r=!0,requestAnimationFrame(()=>{let e=o.selectionStart??999,i=s(o.value,t,e);if(o.value=i.process(),p){let t=o.value.length>c.length?i.caret:e;o.setSelectionRange(t,t)}else if(u){let t=c.length===o.value.length?e+1:e;o.setSelectionRange(t,t)}else l?o.setSelectionRange(e,e):f&&o.setSelectionRange(i.caret,i.caret);n?.(o.value),requestAnimationFrame(()=>{r=!1})})})}export{o as Mask,a as applyMask,f as bind,s as buildMask,i as getMaxLength,c as process};
1
+ function e(e){return e>=`0`&&e<=`9`}function t(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function n(n,r){return r===`9`?e(n):r===`Z`?t(n):e(n)||t(n)}function r(e,t,r=0){if(!e)return{value:``,caret:0};let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(n(t,u)){i+=a+t,a=``,d=!0,c||(o<=r?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}function i(e){let t=0;for(let n of e)(n>=`0`&&n<=`9`||n>=`a`&&n<=`z`||n>=`A`&&n<=`Z`)&&t++;return t}function a(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function o(e,t){if(!Array.isArray(t))return t;let n=i(e),r=0;for(;r<t.length-1&&n>a(t[r]);)r++;return t[r]}function s(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var c=class{caret;_value;_mask;constructor(e,t,n=0){this._value=e,this._mask=t,this.caret=n}process(){let e=r(this._value,this._mask,this.caret);return this.caret=e.caret,e.value}};function l(e,t,n=0){return new c(e,o(e,t),n)}function u(e,t){return l(e,t).process()}let d;function f(){return d===void 0&&(d=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),d}const p=`data-masked`;function m(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function h(e,t,n){if(e.getAttribute(p)!==null)return()=>{};let{onChange:r}=m(n),i=[],a=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),i.push(t))};e.setAttribute(p,Array.isArray(t)?t.join(`|`):t),a(`autocomplete`,`off`),a(`autocorrect`,`off`),a(`autocapitalize`,`off`),a(`spellcheck`,`false`),a(`maxlength`,String(s(t)));let o=!1,c=f()?`keyup`:`keydown`,u=e=>{let n=e.target;requestAnimationFrame(()=>{n.value=l(n.value,t).process(),r?.(n.value)})},d=e=>{let n=e,i=n.target,a=i.value;if(!n.key){o=!0,requestAnimationFrame(()=>{let e=i.selectionStart??999,n=l(i.value,t,e);i.value=n.process(),i.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{o=!1})});return}if(n.key===`Meta`)return;let c=n.key===`Backspace`,u=n.key===`Delete`,d=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,p=n.key===`Unidentified`;if(d&&i.selectionStart===i.selectionEnd&&a.length>=s(t)&&!f()){n.preventDefault();return}if(o){n.preventDefault();return}requestAnimationFrame(()=>{let e=i.selectionStart??999,n=l(i.value,t,e);if(i.value=n.process(),p){let t=i.value.length>a.length?n.caret:e;i.setSelectionRange(t,t)}else if(u){let t=a.length===i.value.length?e+1:e;i.setSelectionRange(t,t)}else c?i.setSelectionRange(e,e):d&&i.setSelectionRange(n.caret,n.caret);r?.(i.value)})};return e.addEventListener(`paste`,u),e.addEventListener(c,d),()=>{e.removeEventListener(`paste`,u),e.removeEventListener(c,d),e.removeAttribute(p);for(let t of i)e.removeAttribute(t)}}export{c as Mask,r as applyMask,h as bind,l as buildMask,s as getMaxLength,u as process};
2
2
  //# sourceMappingURL=mother-mask.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mother-mask.mjs","names":[],"sources":["../src/mask.ts","../src/bind.ts"],"sourcesContent":["/**\n * Mask pattern — a single pattern string or an array ordered from shortest to longest.\n * `9` matches a digit, `Z` matches a letter, `A` matches alphanumeric (digit or letter),\n * anything else is a literal character.\n *\n * @example\n * '(99) 99999-9999'\n * ['(99) 9999-9999', '(99) 99999-9999']\n * 'AA.AAA.AAA/AAAA-99' // CNPJ alfanumérico\n */\nexport type MaskPattern = string | string[]\n\n/** Result of applying a mask to a value. */\nexport interface MaskResult {\n readonly value: string\n readonly caret: number\n}\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Mask resolution\n// ---------------------------------------------------------------------------\n\n/** Select the right mask string for the current value length. */\nfunction resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n let i = 0\n while (i < mask.length - 1 && value.length > mask[i].length) 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\n// ---------------------------------------------------------------------------\n// Core masking — pure function\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nexport function applyMask(value: string, mask: string, inputCaret = 0): MaskResult {\n if (!value) return { value: '', caret: 0 }\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n\n constructor(value: string, mask: string, caret = 0) {\n this._value = value\n this._mask = mask\n this.caret = caret\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(value: string, mask: MaskPattern, caret = 0): Mask {\n return new Mask(value, resolveMask(value, mask), caret)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern): string {\n return buildMask(value, mask).process()\n}\n","import type { MaskPattern } from './mask'\nimport { buildMask, getMaxLength } from './mask'\n\nconst MASKED_ATTR = 'data-masked'\n\nlet cachedIsIos: boolean | undefined\n\nfunction 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\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 * @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 callback - Optional callback called with the masked value on every change.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n callback: ((value: string) => void) | null = null,\n): void {\n if (input.getAttribute(MASKED_ATTR) !== null) return\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n input.setAttribute('autocomplete', 'off')\n input.setAttribute('autocorrect', 'off')\n input.setAttribute('autocapitalize', 'off')\n input.setAttribute('spellcheck', 'false')\n input.setAttribute('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n input.addEventListener('paste', (e: Event) => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask)\n target.value = m.process()\n callback?.(target.value)\n })\n })\n\n input.addEventListener(isIos() ? 'keyup' : 'keydown', (e: Event) => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n\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 callback?.(target.value)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mother-mask.mjs","names":[],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts"],"sourcesContent":["import type { MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Core masking — pure function\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nexport function applyMask(value: string, mask: string, inputCaret = 0): MaskResult {\n if (!value) return { value: '', caret: 0 }\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n\n constructor(value: string, mask: string, caret = 0) {\n this._value = value\n this._mask = mask\n this.caret = caret\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(value: string, mask: MaskPattern, caret = 0): Mask {\n return new Mask(value, resolveMask(value, mask), caret)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern): string {\n return buildMask(value, mask).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask)\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.MotherMask={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){return e>=`0`&&e<=`9`}function n(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function r(e,r){return r===`9`?t(e):r===`Z`?n(e):t(e)||n(e)}function i(e,t){if(!Array.isArray(t))return t;let n=0;for(;n<t.length-1&&e.length>t[n].length;)n++;return t[n]}function a(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}function o(e,t,n=0){if(!e)return{value:``,caret:0};let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(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}}var s=class{caret;_value;_mask;constructor(e,t,n=0){this._value=e,this._mask=t,this.caret=n}process(){let e=o(this._value,this._mask,this.caret);return this.caret=e.caret,e.value}};function c(e,t,n=0){return new s(e,i(e,t),n)}function l(e,t){return c(e,t).process()}let u=`data-masked`,d;function f(){return d===void 0&&(d=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),d}function p(e,t,n=null){if(e.getAttribute(u)!==null)return;e.setAttribute(u,Array.isArray(t)?t.join(`|`):t),e.setAttribute(`autocomplete`,`off`),e.setAttribute(`autocorrect`,`off`),e.setAttribute(`autocapitalize`,`off`),e.setAttribute(`spellcheck`,`false`),e.setAttribute(`maxlength`,String(a(t)));let r=!1;e.addEventListener(`paste`,e=>{let r=e.target;requestAnimationFrame(()=>{r.value=c(r.value,t).process(),n?.(r.value)})}),e.addEventListener(f()?`keyup`:`keydown`,e=>{let i=e,o=i.target,s=o.value;if(!i.key){r=!0,requestAnimationFrame(()=>{let e=o.selectionStart??999,n=c(o.value,t,e);o.value=n.process(),o.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{r=!1})});return}if(i.key===`Meta`)return;let l=i.key===`Backspace`,u=i.key===`Delete`,d=i.key.length===1&&!i.ctrlKey&&!i.altKey&&!i.metaKey,p=i.key===`Unidentified`;if(d&&o.selectionStart===o.selectionEnd&&s.length>=a(t)&&!f()){i.preventDefault();return}if(r){i.preventDefault();return}r=!0,requestAnimationFrame(()=>{let e=o.selectionStart??999,i=c(o.value,t,e);if(o.value=i.process(),p){let t=o.value.length>s.length?i.caret:e;o.setSelectionRange(t,t)}else if(u){let t=s.length===o.value.length?e+1:e;o.setSelectionRange(t,t)}else l?o.setSelectionRange(e,e):d&&o.setSelectionRange(i.caret,i.caret);n?.(o.value),requestAnimationFrame(()=>{r=!1})})})}e.Mask=s,e.applyMask=o,e.bind=p,e.buildMask=c,e.getMaxLength=a,e.process=l});
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.MotherMask={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){return e>=`0`&&e<=`9`}function n(e){return e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`}function r(e,r){return r===`9`?t(e):r===`Z`?n(e):t(e)||n(e)}function i(e,t,n=0){if(!e)return{value:``,caret:0};let i=``,a=``,o=0,s=0,c=!1;for(let l=0;l<t.length;l++){let u=t[l];if(u!==`9`&&u!==`Z`&&u!==`A`){a+=u;continue}let d=!1;for(;o<e.length;){let t=e[o++];if(r(t,u)){i+=a+t,a=``,d=!0,c||(o<=n?s=i.length:c=!0);break}}if(!d)break}return c||(s=i.length),{value:i,caret:s}}function a(e){let t=0;for(let n of e)(n>=`0`&&n<=`9`||n>=`a`&&n<=`z`||n>=`A`&&n<=`Z`)&&t++;return t}function o(e){let t=0;for(let n of e)(n===`9`||n===`Z`||n===`A`)&&t++;return t}function s(e,t){if(!Array.isArray(t))return t;let n=a(e),r=0;for(;r<t.length-1&&n>o(t[r]);)r++;return t[r]}function c(e){return Array.isArray(e)?e.length>0?Math.max(...e.map(e=>e.length)):0:e.length}var l=class{caret;_value;_mask;constructor(e,t,n=0){this._value=e,this._mask=t,this.caret=n}process(){let e=i(this._value,this._mask,this.caret);return this.caret=e.caret,e.value}};function u(e,t,n=0){return new l(e,s(e,t),n)}function d(e,t){return u(e,t).process()}let f;function p(){return f===void 0&&(f=typeof navigator<`u`&&/iPad|iPhone|iPod/i.test(navigator.userAgent)),f}let m=`data-masked`;function h(e){return e==null?{}:typeof e==`function`?{onChange:e}:e}function g(e,t,n){if(e.getAttribute(m)!==null)return()=>{};let{onChange:r}=h(n),i=[],a=(t,n)=>{e.hasAttribute(t)||(e.setAttribute(t,n),i.push(t))};e.setAttribute(m,Array.isArray(t)?t.join(`|`):t),a(`autocomplete`,`off`),a(`autocorrect`,`off`),a(`autocapitalize`,`off`),a(`spellcheck`,`false`),a(`maxlength`,String(c(t)));let o=!1,s=p()?`keyup`:`keydown`,l=e=>{let n=e.target;requestAnimationFrame(()=>{n.value=u(n.value,t).process(),r?.(n.value)})},d=e=>{let n=e,i=n.target,a=i.value;if(!n.key){o=!0,requestAnimationFrame(()=>{let e=i.selectionStart??999,n=u(i.value,t,e);i.value=n.process(),i.setSelectionRange(n.caret,n.caret),requestAnimationFrame(()=>{o=!1})});return}if(n.key===`Meta`)return;let s=n.key===`Backspace`,l=n.key===`Delete`,d=n.key.length===1&&!n.ctrlKey&&!n.altKey&&!n.metaKey,f=n.key===`Unidentified`;if(d&&i.selectionStart===i.selectionEnd&&a.length>=c(t)&&!p()){n.preventDefault();return}if(o){n.preventDefault();return}requestAnimationFrame(()=>{let e=i.selectionStart??999,n=u(i.value,t,e);if(i.value=n.process(),f){let t=i.value.length>a.length?n.caret:e;i.setSelectionRange(t,t)}else if(l){let t=a.length===i.value.length?e+1:e;i.setSelectionRange(t,t)}else s?i.setSelectionRange(e,e):d&&i.setSelectionRange(n.caret,n.caret);r?.(i.value)})};return e.addEventListener(`paste`,l),e.addEventListener(s,d),()=>{e.removeEventListener(`paste`,l),e.removeEventListener(s,d),e.removeAttribute(m);for(let t of i)e.removeAttribute(t)}}e.Mask=l,e.applyMask=i,e.bind=g,e.buildMask=u,e.getMaxLength=c,e.process=d});
2
2
  //# sourceMappingURL=mother-mask.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mother-mask.umd.js","names":[],"sources":["../src/mask.ts","../src/bind.ts"],"sourcesContent":["/**\n * Mask pattern — a single pattern string or an array ordered from shortest to longest.\n * `9` matches a digit, `Z` matches a letter, `A` matches alphanumeric (digit or letter),\n * anything else is a literal character.\n *\n * @example\n * '(99) 99999-9999'\n * ['(99) 9999-9999', '(99) 99999-9999']\n * 'AA.AAA.AAA/AAAA-99' // CNPJ alfanumérico\n */\nexport type MaskPattern = string | string[]\n\n/** Result of applying a mask to a value. */\nexport interface MaskResult {\n readonly value: string\n readonly caret: number\n}\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Mask resolution\n// ---------------------------------------------------------------------------\n\n/** Select the right mask string for the current value length. */\nfunction resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n let i = 0\n while (i < mask.length - 1 && value.length > mask[i].length) 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\n// ---------------------------------------------------------------------------\n// Core masking — pure function\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nexport function applyMask(value: string, mask: string, inputCaret = 0): MaskResult {\n if (!value) return { value: '', caret: 0 }\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n\n constructor(value: string, mask: string, caret = 0) {\n this._value = value\n this._mask = mask\n this.caret = caret\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(value: string, mask: MaskPattern, caret = 0): Mask {\n return new Mask(value, resolveMask(value, mask), caret)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern): string {\n return buildMask(value, mask).process()\n}\n","import type { MaskPattern } from './mask'\nimport { buildMask, getMaxLength } from './mask'\n\nconst MASKED_ATTR = 'data-masked'\n\nlet cachedIsIos: boolean | undefined\n\nfunction 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\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 * @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 callback - Optional callback called with the masked value on every change.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n callback: ((value: string) => void) | null = null,\n): void {\n if (input.getAttribute(MASKED_ATTR) !== null) return\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n input.setAttribute('autocomplete', 'off')\n input.setAttribute('autocorrect', 'off')\n input.setAttribute('autocapitalize', 'off')\n input.setAttribute('spellcheck', 'false')\n input.setAttribute('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n input.addEventListener('paste', (e: Event) => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask)\n target.value = m.process()\n callback?.(target.value)\n })\n })\n\n input.addEventListener(isIos() ? 'keyup' : 'keydown', (e: Event) => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n\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 callback?.(target.value)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mother-mask.umd.js","names":[],"sources":["../src/apply-mask.ts","../src/pattern.ts","../src/mask.ts","../src/platform.ts","../src/bind.ts"],"sourcesContent":["import type { MaskResult } from './types'\n\n// ---------------------------------------------------------------------------\n// Character classification (no regex — avoids the empty-string pitfall)\n// ---------------------------------------------------------------------------\n\nfunction isDigitChar(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction isLetterChar(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n}\n\nfunction matchesSlot(ch: string, slot: string): boolean {\n if (slot === '9') return isDigitChar(ch)\n if (slot === 'Z') return isLetterChar(ch)\n // slot === 'A' → alphanumeric\n return isDigitChar(ch) || isLetterChar(ch)\n}\n\n// ---------------------------------------------------------------------------\n// Core masking — pure function\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a single mask string to a value, producing the masked output and\n * a computed caret position.\n *\n * **Caret algorithm**: as the mask consumes characters from `value`, every\n * time a *matching* input character at a position *before* `inputCaret` is\n * written to the output (including any preceding pending literals that were\n * just flushed), the output caret is updated to the current output length.\n * This correctly handles literal insertion, middle-of-string edits, and\n * characters that are skipped because they don't match the current slot.\n */\nexport function applyMask(value: string, mask: string, inputCaret = 0): MaskResult {\n if (!value) return { value: '', caret: 0 }\n\n let output = ''\n let pending = ''\n let valueIdx = 0\n let outputCaret = 0\n let caretResolved = false\n\n for (let maskIdx = 0; maskIdx < mask.length; maskIdx++) {\n const maskCh = mask[maskIdx]\n\n if (maskCh !== '9' && maskCh !== 'Z' && maskCh !== 'A') {\n pending += maskCh\n continue\n }\n\n // Find next value character that matches this slot\n let found = false\n while (valueIdx < value.length) {\n const ch = value[valueIdx++]\n\n if (matchesSlot(ch, maskCh)) {\n // Flush pending literals then write the matched char\n output += pending + ch\n pending = ''\n found = true\n\n // Caret tracking: if this consumed char was before the input caret,\n // the output caret is (at least) at the current output length.\n if (!caretResolved) {\n if (valueIdx <= inputCaret) {\n outputCaret = output.length\n } else {\n caretResolved = true\n }\n }\n break\n }\n // Non-matching chars are silently skipped (iterative, no recursion).\n }\n\n if (!found) break\n }\n\n // If every matched char was before the input caret (or no chars matched at\n // all past the caret), place the output caret at the end of the output.\n if (!caretResolved) outputCaret = output.length\n\n return { value: output, caret: outputCaret }\n}\n","import type { MaskPattern } from './types'\n\n/**\n * Count alphanumeric characters in a value.\n *\n * Used by `resolveMask` so that both raw input (e.g. \"11999887766\") and\n * already-masked values (e.g. \"(11) 99988-7766\") produce the same data-char\n * count, enabling correct mask selection even when all keystrokes arrive\n * before the first rAF fires (fast typing).\n */\nfunction countDataChars(value: string): number {\n let n = 0\n for (const ch of value) {\n if (\n (ch >= '0' && ch <= '9') ||\n (ch >= 'a' && ch <= 'z') ||\n (ch >= 'A' && ch <= 'Z')\n )\n n++\n }\n return n\n}\n\n/** Count input slots (9, Z, A) in a mask string. */\nfunction countMaskSlots(mask: string): number {\n let n = 0\n for (const ch of mask) {\n if (ch === '9' || ch === 'Z' || ch === 'A') n++\n }\n return n\n}\n\n/**\n * Select the right mask string for the current value.\n *\n * Compares the number of alphanumeric data characters in `value` to the\n * slot capacity of each candidate mask. This correctly handles both\n * progressively-masked values (normal typing) and raw accumulated characters\n * (fast typing where multiple keystrokes arrive before any rAF fires).\n */\nexport function resolveMask(value: string, mask: MaskPattern): string {\n if (!Array.isArray(mask)) return mask\n const dataCount = countDataChars(value)\n let i = 0\n while (i < mask.length - 1 && dataCount > countMaskSlots(mask[i])) i++\n return mask[i]\n}\n\n/** Maximum allowed input length for the given mask. */\nexport function getMaxLength(mask: MaskPattern): number {\n if (Array.isArray(mask)) {\n return mask.length > 0 ? Math.max(...mask.map((m) => m.length)) : 0\n }\n return mask.length\n}\n","import { applyMask } from './apply-mask'\nimport { resolveMask } from './pattern'\nimport type { MaskPattern } from './types'\n\nexport { getMaxLength } from './pattern'\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Low-level mask processor. Tracks caret position after masking. */\nexport class Mask {\n /** Caret position after `process()` runs. */\n caret: number\n\n private readonly _value: string\n private readonly _mask: string\n\n constructor(value: string, mask: string, caret = 0) {\n this._value = value\n this._mask = mask\n this.caret = caret\n }\n\n /** Apply the mask to the value and return the masked string. */\n process(): string {\n const result = applyMask(this._value, this._mask, this.caret)\n this.caret = result.caret\n return result.value\n }\n}\n\n/** Build a `Mask` instance, resolving array patterns by value length. */\nexport function buildMask(value: string, mask: MaskPattern, caret = 0): Mask {\n return new Mask(value, resolveMask(value, mask), caret)\n}\n\n/** Apply a mask pattern to a raw value string and return the masked result. */\nexport function process(value: string, mask: MaskPattern): string {\n return buildMask(value, mask).process()\n}\n","let cachedIsIos: boolean | undefined\n\n/** True when the runtime looks like iOS Safari / WebKit (affects key event choice in {@link bind}). */\nexport function isIos(): boolean {\n if (cachedIsIos !== undefined) return cachedIsIos\n cachedIsIos =\n typeof navigator !== 'undefined' && /iPad|iPhone|iPod/i.test(navigator.userAgent)\n return cachedIsIos\n}\n","import { buildMask, getMaxLength } from './mask'\nimport { isIos } from './platform'\nimport type { BindOptions, MaskPattern } from './types'\n\nconst MASKED_ATTR = 'data-masked'\n\nfunction toBindOptions(\n third: BindOptions | ((value: string) => void) | null | undefined,\n): BindOptions {\n if (third == null) return {}\n if (typeof third === 'function') return { onChange: third }\n return third\n}\n\n/**\n * Bind a mask pattern to an input element.\n *\n * Idempotent — calling `bind()` on an already-bound element has no effect.\n * The element receives a `data-masked` attribute marking it as bound.\n *\n * Returns a function that removes listeners and clears `data-masked` so the\n * element can be bound again later.\n *\n * @param input - Any `HTMLInputElement` or `Element` that behaves like one.\n * @param mask - A single pattern string or an ordered array (shortest → longest).\n * @param options - Optional `{ onChange }`, or pass a callback (legacy) as the third argument.\n */\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n options?: BindOptions | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n onChange: ((value: string) => void) | null,\n): () => void\nexport function bind(\n input: HTMLInputElement | Element,\n mask: MaskPattern,\n third?: BindOptions | ((value: string) => void) | null,\n): () => void {\n if (input.getAttribute(MASKED_ATTR) !== null) return () => {}\n\n const { onChange } = toBindOptions(third)\n\n /** Attribute names set by this bind call; removed on dispose so a later `bind()` can re-apply. */\n const attrsSetHere: string[] = []\n const setIfMissing = (name: string, value: string): void => {\n if (!input.hasAttribute(name)) {\n input.setAttribute(name, value)\n attrsSetHere.push(name)\n }\n }\n\n input.setAttribute(MASKED_ATTR, Array.isArray(mask) ? mask.join('|') : mask)\n setIfMissing('autocomplete', 'off')\n setIfMissing('autocorrect', 'off')\n setIfMissing('autocapitalize', 'off')\n setIfMissing('spellcheck', 'false')\n setIfMissing('maxlength', String(getMaxLength(mask)))\n\n let lockInput = false\n\n const keyEventName = isIos() ? 'keyup' : 'keydown'\n\n const onPaste = (e: Event): void => {\n const target = e.target as HTMLInputElement\n requestAnimationFrame(() => {\n const m = buildMask(target.value, mask)\n target.value = m.process()\n onChange?.(target.value)\n })\n }\n\n const onKey = (e: Event): void => {\n const ke = e as KeyboardEvent\n const target = ke.target as HTMLInputElement\n const oldValue = target.value\n\n // Older Android WebViews may fire key events without a `key` value.\n if (!(ke as { key?: string }).key) {\n lockInput = true\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n target.setSelectionRange(m.caret, m.caret)\n requestAnimationFrame(() => {\n lockInput = false\n })\n })\n return\n }\n\n if (ke.key === 'Meta') return\n\n const isBackspace = ke.key === 'Backspace'\n const isDelete = ke.key === 'Delete'\n const isCharInsert = ke.key.length === 1 && !ke.ctrlKey && !ke.altKey && !ke.metaKey\n const isUnidentified = ke.key === 'Unidentified'\n\n // Block inserting when mask is full (desktop only — iOS handles this natively)\n if (isCharInsert && target.selectionStart === target.selectionEnd) {\n if (oldValue.length >= getMaxLength(mask) && !isIos()) {\n ke.preventDefault()\n return\n }\n }\n\n // `lockInput` is only set by the Android WebView path above; block normal\n // key events while that asynchronous path is in flight.\n if (lockInput) {\n ke.preventDefault()\n return\n }\n\n requestAnimationFrame(() => {\n const pos = target.selectionStart ?? 999\n const m = buildMask(target.value, mask, pos)\n target.value = m.process()\n\n if (isUnidentified) {\n const newPos = target.value.length > oldValue.length ? m.caret : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isDelete) {\n const newPos = oldValue.length === target.value.length ? pos + 1 : pos\n target.setSelectionRange(newPos, newPos)\n } else if (isBackspace) {\n target.setSelectionRange(pos, pos)\n } else if (isCharInsert) {\n target.setSelectionRange(m.caret, m.caret)\n }\n\n onChange?.(target.value)\n })\n }\n\n input.addEventListener('paste', onPaste)\n input.addEventListener(keyEventName, onKey)\n\n return () => {\n input.removeEventListener('paste', onPaste)\n input.removeEventListener(keyEventName, onKey)\n input.removeAttribute(MASKED_ATTR)\n for (const name of attrsSetHere) input.removeAttribute(name)\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mother-mask",
3
3
  "private": false,
4
- "version": "2.0.3",
4
+ "version": "2.0.5",
5
5
  "type": "module",
6
6
  "description": "Lightweight input mask library for browsers",
7
7
  "author": "Danilo Celestino de Castro <dan2dev>",
@@ -57,6 +57,7 @@
57
57
  "homepage": "https://github.com/dan2dev/mother-mask#readme",
58
58
  "scripts": {
59
59
  "build": "tsdown",
60
+ "build:watch": "tsdown --watch",
60
61
  "dev": "tsdown --watch",
61
62
  "clean": "rm -rf dist *.tsbuildinfo",
62
63
  "test": "vitest run --coverage",