mother-mask 2.0.2 → 2.0.3
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 +7 -5
- package/dist/mother-mask.cjs +1 -1
- package/dist/mother-mask.cjs.map +1 -1
- package/dist/mother-mask.d.cts +24 -11
- package/dist/mother-mask.d.mts +24 -11
- package/dist/mother-mask.mjs +1 -1
- package/dist/mother-mask.mjs.map +1 -1
- package/dist/mother-mask.umd.js +1 -1
- package/dist/mother-mask.umd.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,9 +40,10 @@ Apply a mask to a raw string without touching the DOM. Useful for formatting sto
|
|
|
40
40
|
```ts
|
|
41
41
|
import { process } from 'mother-mask'
|
|
42
42
|
|
|
43
|
-
process('12345678901', '999.999.999-99')
|
|
44
|
-
process('01012024', '99/99/9999')
|
|
45
|
-
process('01310100', '99999-999')
|
|
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"
|
|
46
47
|
```
|
|
47
48
|
|
|
48
49
|
## Pattern syntax
|
|
@@ -51,6 +52,7 @@ process('01310100', '99999-999') // → "01310-100"
|
|
|
51
52
|
|-----------|----------------|
|
|
52
53
|
| `9` | Digit (0–9) |
|
|
53
54
|
| `Z` | Letter (a–z, A–Z) |
|
|
55
|
+
| `A` | Alphanumeric (0–9, a–z, A–Z) |
|
|
54
56
|
| anything else | Literal — inserted automatically |
|
|
55
57
|
|
|
56
58
|
## Array masks
|
|
@@ -61,8 +63,8 @@ Pass an ordered array (shortest → longest) to support variable-length inputs.
|
|
|
61
63
|
// Brazilian phone: 8-digit → 9-digit landline / mobile
|
|
62
64
|
bind(input, ['(99) 9999-9999', '(99) 99999-9999'])
|
|
63
65
|
|
|
64
|
-
// CPF / CNPJ
|
|
65
|
-
bind(input, ['999.999.999-99', '
|
|
66
|
+
// CPF / CNPJ alfanumérico
|
|
67
|
+
bind(input, ['999.999.999-99', 'AA.AAA.AAA/AAAA-99'])
|
|
66
68
|
```
|
|
67
69
|
|
|
68
70
|
## UMD / CDN
|
package/dist/mother-mask.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});
|
|
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;
|
|
2
2
|
//# sourceMappingURL=mother-mask.cjs.map
|
package/dist/mother-mask.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mother-mask.cjs","names":[],"sources":["../src/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, anything else is a literal character.\n *\n * @example\n * '(99) 99999-9999'\n * ['(99) 9999-9999', '(99) 99999-9999']\n */\nexport type MaskPattern = string | string[]\n\nconst numberRegex = /\\D/\nconst letterRegex = /[a-zA-Z]/\n\nfunction isDigit(ch: string): boolean {\n return !numberRegex.test(ch)\n}\n\nfunction isLetter(ch: string): boolean {\n return letterRegex.test(ch)\n}\n\nenum CharType {\n NUMBER,\n LETTER,\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 _mask: string\n private readonly _value: string\n\n private _maskPos = -1\n private _maskChar: string | CharType = ''\n private _valuePos = -1\n private _valueChar = ''\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 if (!this._value) return ''\n\n let output = ''\n const pending: string[] = []\n\n while (this._nextMaskChar()) {\n if (typeof this._maskChar === 'string') {\n pending.push(this._maskChar)\n } else if (this._nextValueChar(this._maskChar) && this._valueChar) {\n while (pending.length > 0) {\n if (this._maskPos <= this.caret + 1 && this._maskPos >= this.caret) {\n this.caret++\n }\n output += pending.shift()\n }\n output += this._valueChar\n }\n }\n\n return output\n }\n\n private _nextMaskChar(): boolean {\n this._maskPos++\n if (this._maskPos > this._mask.length) return false\n\n const ch = this._mask.charAt(this._maskPos)\n if (ch === '9') {\n this._maskChar = CharType.NUMBER\n } else if (ch === 'Z') {\n this._maskChar = CharType.LETTER\n } else {\n this._maskChar = ch\n }\n return true\n }\n\n private _nextValueChar(type: CharType): boolean {\n this._valuePos++\n if (this._valuePos > this._value.length) return false\n\n const ch = this._value.charAt(this._valuePos)\n this._valueChar = ch\n\n if (type === CharType.NUMBER && isDigit(ch)) return true\n if (type === CharType.LETTER && isLetter(ch)) return true\n\n return this._nextValueChar(type)\n }\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\n let i = 0\n while (i < mask.length - 1 && value.length > mask[i].length) {\n i++\n }\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)) return Math.max(...mask.map((m) => m.length))\n return mask.length\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/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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"}
|
package/dist/mother-mask.d.cts
CHANGED
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
//#region src/mask.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Mask pattern — a single pattern string or an array ordered from shortest to longest.
|
|
4
|
-
* `9` matches a digit, `Z` matches a letter,
|
|
4
|
+
* `9` matches a digit, `Z` matches a letter, `A` matches alphanumeric (digit or letter),
|
|
5
|
+
* anything else is a literal character.
|
|
5
6
|
*
|
|
6
7
|
* @example
|
|
7
8
|
* '(99) 99999-9999'
|
|
8
9
|
* ['(99) 9999-9999', '(99) 99999-9999']
|
|
10
|
+
* 'AA.AAA.AAA/AAAA-99' // CNPJ alfanumérico
|
|
9
11
|
*/
|
|
10
12
|
type MaskPattern = string | string[];
|
|
13
|
+
/** Result of applying a mask to a value. */
|
|
14
|
+
interface MaskResult {
|
|
15
|
+
readonly value: string;
|
|
16
|
+
readonly caret: number;
|
|
17
|
+
}
|
|
18
|
+
/** Maximum allowed input length for the given mask. */
|
|
19
|
+
declare function getMaxLength(mask: MaskPattern): number;
|
|
20
|
+
/**
|
|
21
|
+
* Apply a single mask string to a value, producing the masked output and
|
|
22
|
+
* a computed caret position.
|
|
23
|
+
*
|
|
24
|
+
* **Caret algorithm**: as the mask consumes characters from `value`, every
|
|
25
|
+
* time a *matching* input character at a position *before* `inputCaret` is
|
|
26
|
+
* written to the output (including any preceding pending literals that were
|
|
27
|
+
* just flushed), the output caret is updated to the current output length.
|
|
28
|
+
* This correctly handles literal insertion, middle-of-string edits, and
|
|
29
|
+
* characters that are skipped because they don't match the current slot.
|
|
30
|
+
*/
|
|
31
|
+
declare function applyMask(value: string, mask: string, inputCaret?: number): MaskResult;
|
|
11
32
|
/** Low-level mask processor. Tracks caret position after masking. */
|
|
12
33
|
declare class Mask {
|
|
13
34
|
/** Caret position after `process()` runs. */
|
|
14
35
|
caret: number;
|
|
15
|
-
private readonly _mask;
|
|
16
36
|
private readonly _value;
|
|
17
|
-
private
|
|
18
|
-
private _maskChar;
|
|
19
|
-
private _valuePos;
|
|
20
|
-
private _valueChar;
|
|
37
|
+
private readonly _mask;
|
|
21
38
|
constructor(value: string, mask: string, caret?: number);
|
|
22
39
|
/** Apply the mask to the value and return the masked string. */
|
|
23
40
|
process(): string;
|
|
24
|
-
private _nextMaskChar;
|
|
25
|
-
private _nextValueChar;
|
|
26
41
|
}
|
|
27
|
-
/** Maximum allowed input length for the given mask. */
|
|
28
|
-
declare function getMaxLength(mask: MaskPattern): number;
|
|
29
42
|
/** Build a `Mask` instance, resolving array patterns by value length. */
|
|
30
43
|
declare function buildMask(value: string, mask: MaskPattern, caret?: number): Mask;
|
|
31
44
|
/** Apply a mask pattern to a raw value string and return the masked result. */
|
|
@@ -44,5 +57,5 @@ declare function process(value: string, mask: MaskPattern): string;
|
|
|
44
57
|
*/
|
|
45
58
|
declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, callback?: ((value: string) => void) | null): void;
|
|
46
59
|
//#endregion
|
|
47
|
-
export { Mask, type MaskPattern, bind, buildMask, getMaxLength, process };
|
|
60
|
+
export { Mask, type MaskPattern, type MaskResult, applyMask, bind, buildMask, getMaxLength, process };
|
|
48
61
|
//# sourceMappingURL=mother-mask.d.cts.map
|
package/dist/mother-mask.d.mts
CHANGED
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
//#region src/mask.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Mask pattern — a single pattern string or an array ordered from shortest to longest.
|
|
4
|
-
* `9` matches a digit, `Z` matches a letter,
|
|
4
|
+
* `9` matches a digit, `Z` matches a letter, `A` matches alphanumeric (digit or letter),
|
|
5
|
+
* anything else is a literal character.
|
|
5
6
|
*
|
|
6
7
|
* @example
|
|
7
8
|
* '(99) 99999-9999'
|
|
8
9
|
* ['(99) 9999-9999', '(99) 99999-9999']
|
|
10
|
+
* 'AA.AAA.AAA/AAAA-99' // CNPJ alfanumérico
|
|
9
11
|
*/
|
|
10
12
|
type MaskPattern = string | string[];
|
|
13
|
+
/** Result of applying a mask to a value. */
|
|
14
|
+
interface MaskResult {
|
|
15
|
+
readonly value: string;
|
|
16
|
+
readonly caret: number;
|
|
17
|
+
}
|
|
18
|
+
/** Maximum allowed input length for the given mask. */
|
|
19
|
+
declare function getMaxLength(mask: MaskPattern): number;
|
|
20
|
+
/**
|
|
21
|
+
* Apply a single mask string to a value, producing the masked output and
|
|
22
|
+
* a computed caret position.
|
|
23
|
+
*
|
|
24
|
+
* **Caret algorithm**: as the mask consumes characters from `value`, every
|
|
25
|
+
* time a *matching* input character at a position *before* `inputCaret` is
|
|
26
|
+
* written to the output (including any preceding pending literals that were
|
|
27
|
+
* just flushed), the output caret is updated to the current output length.
|
|
28
|
+
* This correctly handles literal insertion, middle-of-string edits, and
|
|
29
|
+
* characters that are skipped because they don't match the current slot.
|
|
30
|
+
*/
|
|
31
|
+
declare function applyMask(value: string, mask: string, inputCaret?: number): MaskResult;
|
|
11
32
|
/** Low-level mask processor. Tracks caret position after masking. */
|
|
12
33
|
declare class Mask {
|
|
13
34
|
/** Caret position after `process()` runs. */
|
|
14
35
|
caret: number;
|
|
15
|
-
private readonly _mask;
|
|
16
36
|
private readonly _value;
|
|
17
|
-
private
|
|
18
|
-
private _maskChar;
|
|
19
|
-
private _valuePos;
|
|
20
|
-
private _valueChar;
|
|
37
|
+
private readonly _mask;
|
|
21
38
|
constructor(value: string, mask: string, caret?: number);
|
|
22
39
|
/** Apply the mask to the value and return the masked string. */
|
|
23
40
|
process(): string;
|
|
24
|
-
private _nextMaskChar;
|
|
25
|
-
private _nextValueChar;
|
|
26
41
|
}
|
|
27
|
-
/** Maximum allowed input length for the given mask. */
|
|
28
|
-
declare function getMaxLength(mask: MaskPattern): number;
|
|
29
42
|
/** Build a `Mask` instance, resolving array patterns by value length. */
|
|
30
43
|
declare function buildMask(value: string, mask: MaskPattern, caret?: number): Mask;
|
|
31
44
|
/** Apply a mask pattern to a raw value string and return the masked result. */
|
|
@@ -44,5 +57,5 @@ declare function process(value: string, mask: MaskPattern): string;
|
|
|
44
57
|
*/
|
|
45
58
|
declare function bind(input: HTMLInputElement | Element, mask: MaskPattern, callback?: ((value: string) => void) | null): void;
|
|
46
59
|
//#endregion
|
|
47
|
-
export { Mask, type MaskPattern, bind, buildMask, getMaxLength, process };
|
|
60
|
+
export { Mask, type MaskPattern, type MaskResult, applyMask, bind, buildMask, getMaxLength, process };
|
|
48
61
|
//# sourceMappingURL=mother-mask.d.mts.map
|
package/dist/mother-mask.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
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};
|
|
2
2
|
//# sourceMappingURL=mother-mask.mjs.map
|
package/dist/mother-mask.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mother-mask.mjs","names":[],"sources":["../src/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, anything else is a literal character.\n *\n * @example\n * '(99) 99999-9999'\n * ['(99) 9999-9999', '(99) 99999-9999']\n */\nexport type MaskPattern = string | string[]\n\nconst numberRegex = /\\D/\nconst letterRegex = /[a-zA-Z]/\n\nfunction isDigit(ch: string): boolean {\n return !numberRegex.test(ch)\n}\n\nfunction isLetter(ch: string): boolean {\n return letterRegex.test(ch)\n}\n\nenum CharType {\n NUMBER,\n LETTER,\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 _mask: string\n private readonly _value: string\n\n private _maskPos = -1\n private _maskChar: string | CharType = ''\n private _valuePos = -1\n private _valueChar = ''\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 if (!this._value) return ''\n\n let output = ''\n const pending: string[] = []\n\n while (this._nextMaskChar()) {\n if (typeof this._maskChar === 'string') {\n pending.push(this._maskChar)\n } else if (this._nextValueChar(this._maskChar) && this._valueChar) {\n while (pending.length > 0) {\n if (this._maskPos <= this.caret + 1 && this._maskPos >= this.caret) {\n this.caret++\n }\n output += pending.shift()\n }\n output += this._valueChar\n }\n }\n\n return output\n }\n\n private _nextMaskChar(): boolean {\n this._maskPos++\n if (this._maskPos > this._mask.length) return false\n\n const ch = this._mask.charAt(this._maskPos)\n if (ch === '9') {\n this._maskChar = CharType.NUMBER\n } else if (ch === 'Z') {\n this._maskChar = CharType.LETTER\n } else {\n this._maskChar = ch\n }\n return true\n }\n\n private _nextValueChar(type: CharType): boolean {\n this._valuePos++\n if (this._valuePos > this._value.length) return false\n\n const ch = this._value.charAt(this._valuePos)\n this._valueChar = ch\n\n if (type === CharType.NUMBER && isDigit(ch)) return true\n if (type === CharType.LETTER && isLetter(ch)) return true\n\n return this._nextValueChar(type)\n }\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\n let i = 0\n while (i < mask.length - 1 && value.length > mask[i].length) {\n i++\n }\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)) return Math.max(...mask.map((m) => m.length))\n return mask.length\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/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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"}
|
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`});
|
|
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});
|
|
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, anything else is a literal character.\n *\n * @example\n * '(99) 99999-9999'\n * ['(99) 9999-9999', '(99) 99999-9999']\n */\nexport type MaskPattern = string | string[]\n\nconst numberRegex = /\\D/\nconst letterRegex = /[a-zA-Z]/\n\nfunction isDigit(ch: string): boolean {\n return !numberRegex.test(ch)\n}\n\nfunction isLetter(ch: string): boolean {\n return letterRegex.test(ch)\n}\n\nenum CharType {\n NUMBER,\n LETTER,\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 _mask: string\n private readonly _value: string\n\n private _maskPos = -1\n private _maskChar: string | CharType = ''\n private _valuePos = -1\n private _valueChar = ''\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 if (!this._value) return ''\n\n let output = ''\n const pending: string[] = []\n\n while (this._nextMaskChar()) {\n if (typeof this._maskChar === 'string') {\n pending.push(this._maskChar)\n } else if (this._nextValueChar(this._maskChar) && this._valueChar) {\n while (pending.length > 0) {\n if (this._maskPos <= this.caret + 1 && this._maskPos >= this.caret) {\n this.caret++\n }\n output += pending.shift()\n }\n output += this._valueChar\n }\n }\n\n return output\n }\n\n private _nextMaskChar(): boolean {\n this._maskPos++\n if (this._maskPos > this._mask.length) return false\n\n const ch = this._mask.charAt(this._maskPos)\n if (ch === '9') {\n this._maskChar = CharType.NUMBER\n } else if (ch === 'Z') {\n this._maskChar = CharType.LETTER\n } else {\n this._maskChar = ch\n }\n return true\n }\n\n private _nextValueChar(type: CharType): boolean {\n this._valuePos++\n if (this._valuePos > this._value.length) return false\n\n const ch = this._value.charAt(this._valuePos)\n this._valueChar = ch\n\n if (type === CharType.NUMBER && isDigit(ch)) return true\n if (type === CharType.LETTER && isLetter(ch)) return true\n\n return this._nextValueChar(type)\n }\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\n let i = 0\n while (i < mask.length - 1 && value.length > mask[i].length) {\n i++\n }\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)) return Math.max(...mask.map((m) => m.length))\n return mask.length\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/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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"}
|