use-mask-input 3.11.1 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +102 -1
- package/dist/antd.cjs +1 -1
- package/dist/antd.cjs.map +1 -1
- package/dist/antd.d.cts +1 -1
- package/dist/antd.d.mts +1 -1
- package/dist/antd.mjs +1 -1
- package/dist/antd.mjs.map +1 -1
- package/dist/{withMask-BQpBm8zG.cjs → core-BopfTGvA.cjs} +2 -2
- package/dist/core-BopfTGvA.cjs.map +1 -0
- package/dist/{withMask-DlC13MV7.mjs → core-CNGYEIpO.mjs} +2 -2
- package/dist/core-CNGYEIpO.mjs.map +1 -0
- package/dist/{index-HfKPnOg0.d.cts → index-CqBLkcNO.d.cts} +12 -3
- package/dist/{index-HfKPnOg0.d.mts → index-CqBLkcNO.d.mts} +12 -3
- package/dist/index-DAfluDSv.d.cts +25 -0
- package/dist/index-DGAwRzYx.d.mts +25 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -23
- package/dist/index.d.mts +2 -23
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/vue.cjs +2 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.cts +92 -0
- package/dist/vue.d.mts +92 -0
- package/dist/vue.mjs +2 -0
- package/dist/vue.mjs.map +1 -0
- package/dist/withMask--iSmiRbB.cjs +2 -0
- package/dist/withMask--iSmiRbB.cjs.map +1 -0
- package/dist/withMask-DOmJHTJ4.mjs +2 -0
- package/dist/withMask-DOmJHTJ4.mjs.map +1 -0
- package/package.json +50 -6
- package/src/@types/inputmask.d.ts +6 -0
- package/src/types/index.ts +6 -26
- package/src/types/mask.ts +37 -0
- package/src/vue/applyMask.ts +99 -0
- package/src/vue/directive.ts +59 -0
- package/src/vue/index.ts +13 -0
- package/src/vue/resolveVueElement.ts +34 -0
- package/src/vue/types.ts +35 -0
- package/src/vue/useMaskInput.ts +56 -0
- package/dist/withMask-BQpBm8zG.cjs.map +0 -1
- package/dist/withMask-DlC13MV7.mjs.map +0 -1
package/dist/vue.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vue.mjs","names":[],"sources":["../src/vue/applyMask.ts","../src/vue/resolveVueElement.ts","../src/vue/directive.ts","../src/vue/useMaskInput.ts"],"sourcesContent":["import { applyMaskToElement } from '../core';\nimport { makeMaskCacheKey } from '../utils';\nimport isServer from '../utils/isServer';\n\nimport type { Mask, Options, VueMaskBinding, VueMaskConfig } from './types';\n\ntype MaskedElement = HTMLElement & {\n inputmask?: { remove?: () => void };\n};\n\n/**\n * Single implementation both Vue surfaces route through, so the directive and\n * the composable cannot drift apart on what a given mask means.\n */\n\nfunction isConfig(binding: VueMaskBinding): binding is VueMaskConfig {\n return (\n typeof binding === 'object'\n && binding !== null\n && !Array.isArray(binding)\n && 'mask' in binding\n );\n}\n\n/** Unpacks the two accepted binding shapes into a mask and its options. */\nexport function normalizeBinding(binding: VueMaskBinding): {\n mask: Mask;\n options?: Options;\n} {\n if (isConfig(binding)) {\n return { mask: binding.mask, options: binding.options };\n }\n\n return { mask: binding as Mask };\n}\n\n/**\n * Shallow equality over mask options.\n *\n * ponytail: shallow on purpose. An options object mutated in place, or one\n * holding functions, won't compare equal-then-changed correctly — but deep\n * comparison on every re-render is exactly the cost this guard exists to\n * avoid. Documented as \"replace options, don't mutate them\".\n */\nfunction sameOptions(a?: Options, b?: Options): boolean {\n if (a === b) return true;\n if (!a || !b) return false;\n\n const keysA = Object.keys(a) as (keyof Options)[];\n const keysB = Object.keys(b) as (keyof Options)[];\n if (keysA.length !== keysB.length) return false;\n\n return keysA.every((key) => Object.is(a[key], b[key]));\n}\n\n/**\n * True when two bindings mean the same mask, so an unrelated re-render doesn't\n * rebuild Inputmask's buffer and drop the caret to the end mid-typing.\n *\n * Compares structurally, not by identity: an inline `v-mask-input=\"{ mask: 'cpf' }\"`\n * allocates a fresh object every render and would otherwise never look equal.\n */\nexport function sameBinding(a: VueMaskBinding, b: VueMaskBinding): boolean {\n const left = normalizeBinding(a);\n const right = normalizeBinding(b);\n\n if (makeMaskCacheKey('', left.mask) !== makeMaskCacheKey('', right.mask)) {\n return false;\n }\n\n return sameOptions(left.options, right.options);\n}\n\n/**\n * Applies a binding's mask to an already-resolved element.\n *\n * A null mask is not merely \"do nothing\": it has to tear down whatever mask is\n * already on the element. A binding is reactive, so `v-mask-input=\"on ? 'cpf' : null\"`\n * turns the mask off, and simply returning early would leave the element still\n * formatting as a CPF.\n *\n * @param element - The element to mask\n * @param binding - Mask, or `{ mask, options }`\n */\nexport default function applyMask(\n element: HTMLElement | null,\n binding: VueMaskBinding,\n): void {\n if (isServer || !element) return;\n\n const { mask, options } = normalizeBinding(binding);\n\n if (mask === null || mask === undefined) {\n (element as MaskedElement).inputmask?.remove?.();\n return;\n }\n\n applyMaskToElement(element, mask, options);\n}\n","import { findInputElement, isHTMLElement } from '../core/elementResolver';\n\nimport type { MaskRefTarget } from './types';\n\n/**\n * Resolves whatever Vue hands us into the element that should carry the mask.\n *\n * A directive always receives a DOM element. A `:ref` on a component receives\n * the component's public instance instead, so the element has to be dug out of\n * `$el` first. Either way the existing `findInputElement` does the final\n * wrapper search, which is the same code path that makes Ant Design work on the\n * React side.\n *\n * Fragment-root components resolve to `null` on purpose: their `$el` is a text\n * anchor node, and Vue itself refuses this case for directives\n * (\"Runtime directive used on component with non-element root node\"). Walking\n * up to `parentElement` could just as easily find a sibling input as the\n * intended one.\n *\n * @param target - Element, component public instance, or null\n * @returns The element to mask, or null if there isn't one\n */\nexport default function resolveVueElement(target: MaskRefTarget): HTMLElement | null {\n if (!target) return null;\n\n if (isHTMLElement(target)) {\n return findInputElement(target);\n }\n\n const { $el } = target as { $el?: unknown };\n if (!isHTMLElement($el)) return null;\n\n return findInputElement($el);\n}\n","import applyMask, { sameBinding } from './applyMask';\nimport resolveVueElement from './resolveVueElement';\n\nimport type { ObjectDirective } from 'vue';\n\nimport type { VueMaskBinding } from './types';\n\ntype MaskedElement = HTMLElement & {\n inputmask?: { remove?: () => void };\n};\n\n/**\n * `v-mask-input` — the only thing in the Vue entry that applies a mask.\n *\n * Getting real lifecycle hooks is why this, and not the composable, is the\n * primitive: the React surface has no equivalent of `unmounted`, which is why\n * it never tears Inputmask down.\n *\n * Note there is no `v-model` handling here, and none is needed. Inputmask\n * replaces the element's `value` property with its own accessor, so `v-model`\n * reads and writes through the engine rather than around it — including\n * returning the unmasked value under `autoUnmask`.\n *\n * @example\n * ```vue\n * <script setup>\n * import { vMaskInput } from 'use-mask-input/vue'\n * </script>\n *\n * <template>\n * <input v-mask-input=\"'cpf'\" />\n * <input v-mask-input=\"{ mask: 'currency', options: { prefix: 'R$ ' } }\" />\n * </template>\n * ```\n */\nconst vMaskInput: ObjectDirective<HTMLElement, VueMaskBinding> = {\n mounted(el, binding) {\n applyMask(resolveVueElement(el), binding.value);\n },\n\n updated(el, binding) {\n // Every parent re-render lands here. Re-masking an unchanged binding would\n // rebuild the buffer and send the caret to the end while the user types.\n if (sameBinding(binding.value, binding.oldValue as VueMaskBinding)) return;\n\n applyMask(resolveVueElement(el), binding.value);\n },\n\n unmounted(el) {\n const target = resolveVueElement(el) as MaskedElement | null;\n target?.inputmask?.remove?.();\n },\n\n // Without this, @vue/server-renderer warns about an unhandled custom\n // directive. There is nothing to render server-side.\n getSSRProps: () => ({}),\n};\n\nexport default vMaskInput;\n","import applyMask from './applyMask';\nimport resolveVueElement from './resolveVueElement';\nimport { getUnmaskedValue } from '../utils';\nimport isServer from '../utils/isServer';\n\nimport type {\n MaskRefTarget, Mask, Options, UseMaskInputReturn,\n} from './types';\n\n/**\n * Composable form, for imperative use and for reading the raw value when\n * `autoUnmask` is off.\n *\n * Narrower than the directive on purpose: wrapper components (PrimeVue,\n * Element Plus, Ant Design Vue) are already covered by `v-mask-input`, because\n * Vue applies directives to a component's root element and the mask engine\n * searches inside it. Reach for this when you need `unmaskedValue()` or a ref\n * you can hold.\n *\n * @param mask - The mask pattern or alias\n * @param options - Optional mask configuration options\n * @returns A ref callback to bind, and an unmasked-value accessor\n *\n * @example\n * ```vue\n * <script setup>\n * import { useMaskInput } from 'use-mask-input/vue'\n * const { maskRef, unmaskedValue } = useMaskInput('cpf')\n * function submit() { console.log(unmaskedValue()) }\n * </script>\n *\n * <template><input :ref=\"maskRef\" /></template>\n * ```\n */\nexport default function useMaskInput(mask: Mask, options?: Options): UseMaskInputReturn {\n if (isServer) {\n return {\n maskRef: () => {\n // server doesn't have dom, so just do nothing\n },\n unmaskedValue: () => '',\n };\n }\n\n let element: HTMLElement | null = null;\n\n const maskRef = (target: MaskRefTarget): void => {\n element = resolveVueElement(target);\n applyMask(element, { mask, options });\n };\n\n return {\n maskRef,\n unmaskedValue: () => getUnmaskedValue(element),\n };\n}\n"],"mappings":"yFAeA,SAAS,EAAS,EAAmD,CACnE,OACE,OAAO,GAAY,YAChB,GACA,CAAC,MAAM,QAAQ,CAAO,GACtB,SAAU,CAEjB,CAGA,SAAgB,EAAiB,EAG/B,CAKA,OAJI,EAAS,CAAO,EACX,CAAE,KAAM,EAAQ,KAAM,QAAS,EAAQ,OAAQ,EAGjD,CAAE,KAAM,CAAgB,CACjC,CAUA,SAAS,EAAY,EAAa,EAAsB,CACtD,GAAI,IAAM,EAAG,MAAO,GACpB,GAAI,CAAC,GAAK,CAAC,EAAG,MAAO,GAErB,IAAM,EAAQ,OAAO,KAAK,CAAC,EACrB,EAAQ,OAAO,KAAK,CAAC,EAG3B,OAFI,EAAM,SAAW,EAAM,QAEpB,EAAM,MAAO,GAAQ,OAAO,GAAG,EAAE,GAAM,EAAE,EAAI,CAAC,CACvD,CASA,SAAgB,EAAY,EAAmB,EAA4B,CACzE,IAAM,EAAO,EAAiB,CAAC,EACzB,EAAQ,EAAiB,CAAC,EAMhC,OAJI,EAAiB,GAAI,EAAK,IAAI,IAAM,EAAiB,GAAI,EAAM,IAAI,GAIhE,EAAY,EAAK,QAAS,EAAM,OAAO,CAChD,CAaA,SAAwB,EACtB,EACA,EACM,CACN,GAAI,GAAY,CAAC,EAAS,OAE1B,GAAM,CAAE,OAAM,WAAY,EAAiB,CAAO,EAElD,GAAI,GAAS,KAA4B,CACvC,EAA2B,WAAW,SAAS,EAC/C,MACF,CAEA,EAAmB,EAAS,EAAM,CAAO,CAC3C,CC5EA,SAAwB,EAAkB,EAA2C,CACnF,GAAI,CAAC,EAAQ,OAAO,KAEpB,GAAI,EAAc,CAAM,EACtB,OAAO,EAAiB,CAAM,EAGhC,GAAM,CAAE,OAAQ,EAGhB,OAFK,EAAc,CAAG,EAEf,EAAiB,CAAG,EAFK,IAGlC,CCEA,MAAM,EAA2D,CAC/D,QAAQ,EAAI,EAAS,CACnB,EAAU,EAAkB,CAAE,EAAG,EAAQ,KAAK,CAChD,EAEA,QAAQ,EAAI,EAAS,CAGf,EAAY,EAAQ,MAAO,EAAQ,QAA0B,GAEjE,EAAU,EAAkB,CAAE,EAAG,EAAQ,KAAK,CAChD,EAEA,UAAU,EAAI,CAEZ,EADiC,CAC5B,CAAC,EAAE,WAAW,SAAS,CAC9B,EAIA,iBAAoB,CAAC,EACvB,ECtBA,SAAwB,EAAa,EAAY,EAAuC,CACtF,GAAI,EACF,MAAO,CACL,YAAe,CAEf,EACA,kBAAqB,EACvB,EAGF,IAAI,EAA8B,KAOlC,MAAO,CACL,QANe,GAAgC,CAC/C,EAAU,EAAkB,CAAM,EAClC,EAAU,EAAS,CAAE,OAAM,SAAQ,CAAC,CACtC,EAIE,kBAAqB,EAAiB,CAAO,CAC/C,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./core-BopfTGvA.cjs"),t=new Map;function n(n,r){if(!r){let r=e.o(``,n);if(t.has(r))return t.get(r)}let i=null,a=(t=>{if(e.u||n===null||!t)return;i=t;let a=e.l(e.f)(e.d(n,r));e.r(t,n,r),a.mask(t)});if(!r){let r=e.o(``,n);t.set(r,a)}return e.c(a,()=>e.a(i))}Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return n}});
|
|
2
|
+
//# sourceMappingURL=withMask--iSmiRbB.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withMask--iSmiRbB.cjs","names":["makeMaskCacheKey","isServer","interopDefaultSync","inputmask","getMaskOptions","setUnmaskedValue","getUnmaskedValue"],"sources":["../src/api/withMask.ts"],"sourcesContent":["/* eslint-disable import-x/no-extraneous-dependencies */\nimport inputmask from '../core/inputmask';\n\nimport { getMaskOptions } from '../core/maskConfig';\nimport { stripMaxLength } from '../core/maskEngine';\nimport { getUnmaskedValue, makeMaskCacheKey, setUnmaskedValue } from '../utils';\nimport isServer from '../utils/isServer';\nimport interopDefaultSync from '../utils/moduleInterop';\n\nimport type { Input, Mask, Options, UseMaskInputReturn } from '../types';\n\nconst callbackCache = new Map<string, UseMaskInputReturn>();\n\n/**\n * Higher-order function that creates a ref callback for applying input masks.\n * Simple function to apply mask via ref. No hooks, no drama.\n *\n * @param mask - The mask pattern to apply\n * @param options - Optional mask configuration options\n * @returns A ref callback function that applies the mask\n */\nexport default function withMask(mask: Mask, options?: Options): UseMaskInputReturn {\n // without options, we cant cache, so we always return a fresh callback. :P\n if (!options) {\n const cacheKey = makeMaskCacheKey('', mask);\n if (callbackCache.has(cacheKey)) {\n return callbackCache.get(cacheKey) as UseMaskInputReturn;\n }\n }\n\n let currentInput: Input | null = null;\n\n const callback = ((input: Input | null): void => {\n if (isServer || mask === null || !input) return;\n\n currentInput = input;\n const maskInput = interopDefaultSync(inputmask)(getMaskOptions(mask, options));\n stripMaxLength(input, mask, options);\n maskInput.mask(input as HTMLElement);\n }) as UseMaskInputReturn;\n\n if (!options) {\n const cacheKey = makeMaskCacheKey('', mask);\n callbackCache.set(cacheKey, callback);\n }\n\n return setUnmaskedValue(callback, () => getUnmaskedValue(currentInput));\n}\n"],"mappings":"uCAWM,EAAgB,IAAI,IAU1B,SAAwB,EAAS,EAAY,EAAuC,CAElF,GAAI,CAAC,EAAS,CACZ,IAAM,EAAWA,EAAAA,EAAiB,GAAI,CAAI,EAC1C,GAAI,EAAc,IAAI,CAAQ,EAC5B,OAAO,EAAc,IAAI,CAAQ,CAErC,CAEA,IAAI,EAA6B,KAE3B,GAAa,GAA8B,CAC/C,GAAIC,EAAAA,GAAY,IAAS,MAAQ,CAAC,EAAO,OAEzC,EAAe,EACf,IAAM,EAAYC,EAAAA,EAAmBC,EAAAA,CAAS,CAAC,CAACC,EAAAA,EAAe,EAAM,CAAO,CAAC,EAC7E,EAAA,EAAe,EAAO,EAAM,CAAO,EACnC,EAAU,KAAK,CAAoB,CACrC,GAEA,GAAI,CAAC,EAAS,CACZ,IAAM,EAAWJ,EAAAA,EAAiB,GAAI,CAAI,EAC1C,EAAc,IAAI,EAAU,CAAQ,CACtC,CAEA,OAAOK,EAAAA,EAAiB,MAAgBC,EAAAA,EAAiB,CAAY,CAAC,CACxE"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as e,c as t,d as n,f as r,l as i,o as a,r as o,u as s}from"./core-CNGYEIpO.mjs";const c=new Map;function l(l,u){if(!u){let e=a(``,l);if(c.has(e))return c.get(e)}let d=null,f=(e=>{if(s||l===null||!e)return;d=e;let t=i(r)(n(l,u));o(e,l,u),t.mask(e)});if(!u){let e=a(``,l);c.set(e,f)}return t(f,()=>e(d))}export{l as t};
|
|
2
|
+
//# sourceMappingURL=withMask-DOmJHTJ4.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withMask-DOmJHTJ4.mjs","names":["inputmask"],"sources":["../src/api/withMask.ts"],"sourcesContent":["/* eslint-disable import-x/no-extraneous-dependencies */\nimport inputmask from '../core/inputmask';\n\nimport { getMaskOptions } from '../core/maskConfig';\nimport { stripMaxLength } from '../core/maskEngine';\nimport { getUnmaskedValue, makeMaskCacheKey, setUnmaskedValue } from '../utils';\nimport isServer from '../utils/isServer';\nimport interopDefaultSync from '../utils/moduleInterop';\n\nimport type { Input, Mask, Options, UseMaskInputReturn } from '../types';\n\nconst callbackCache = new Map<string, UseMaskInputReturn>();\n\n/**\n * Higher-order function that creates a ref callback for applying input masks.\n * Simple function to apply mask via ref. No hooks, no drama.\n *\n * @param mask - The mask pattern to apply\n * @param options - Optional mask configuration options\n * @returns A ref callback function that applies the mask\n */\nexport default function withMask(mask: Mask, options?: Options): UseMaskInputReturn {\n // without options, we cant cache, so we always return a fresh callback. :P\n if (!options) {\n const cacheKey = makeMaskCacheKey('', mask);\n if (callbackCache.has(cacheKey)) {\n return callbackCache.get(cacheKey) as UseMaskInputReturn;\n }\n }\n\n let currentInput: Input | null = null;\n\n const callback = ((input: Input | null): void => {\n if (isServer || mask === null || !input) return;\n\n currentInput = input;\n const maskInput = interopDefaultSync(inputmask)(getMaskOptions(mask, options));\n stripMaxLength(input, mask, options);\n maskInput.mask(input as HTMLElement);\n }) as UseMaskInputReturn;\n\n if (!options) {\n const cacheKey = makeMaskCacheKey('', mask);\n callbackCache.set(cacheKey, callback);\n }\n\n return setUnmaskedValue(callback, () => getUnmaskedValue(currentInput));\n}\n"],"mappings":"yFAWA,MAAM,EAAgB,IAAI,IAU1B,SAAwB,EAAS,EAAY,EAAuC,CAElF,GAAI,CAAC,EAAS,CACZ,IAAM,EAAW,EAAiB,GAAI,CAAI,EAC1C,GAAI,EAAc,IAAI,CAAQ,EAC5B,OAAO,EAAc,IAAI,CAAQ,CAErC,CAEA,IAAI,EAA6B,KAE3B,GAAa,GAA8B,CAC/C,GAAI,GAAY,IAAS,MAAQ,CAAC,EAAO,OAEzC,EAAe,EACf,IAAM,EAAY,EAAmBA,CAAS,CAAC,CAAC,EAAe,EAAM,CAAO,CAAC,EAC7E,EAAe,EAAO,EAAM,CAAO,EACnC,EAAU,KAAK,CAAoB,CACrC,GAEA,GAAI,CAAC,EAAS,CACZ,IAAM,EAAW,EAAiB,GAAI,CAAI,EAC1C,EAAc,IAAI,EAAU,CAAQ,CACtC,CAEA,OAAO,EAAiB,MAAgB,EAAiB,CAAY,CAAC,CACxE"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-mask-input",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Input masks for React and Vue 3. Works with React Hook Form, TanStack Form, vee-validate, and Ant Design.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Eduardo Borges<euduardoborges@gmail.com>",
|
|
8
8
|
"type": "module",
|
|
@@ -33,6 +33,16 @@
|
|
|
33
33
|
"types": "./dist/antd.d.cts",
|
|
34
34
|
"default": "./dist/antd.cjs"
|
|
35
35
|
}
|
|
36
|
+
},
|
|
37
|
+
"./vue": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/vue.d.mts",
|
|
40
|
+
"default": "./dist/vue.mjs"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/vue.d.cts",
|
|
44
|
+
"default": "./dist/vue.cjs"
|
|
45
|
+
}
|
|
36
46
|
}
|
|
37
47
|
},
|
|
38
48
|
"engines": {
|
|
@@ -50,11 +60,25 @@
|
|
|
50
60
|
"peerDependencies": {
|
|
51
61
|
"antd": ">=5",
|
|
52
62
|
"react": ">=17",
|
|
53
|
-
"react-dom": ">=17"
|
|
63
|
+
"react-dom": ">=17",
|
|
64
|
+
"vee-validate": ">=4",
|
|
65
|
+
"vue": ">=3.4"
|
|
54
66
|
},
|
|
55
67
|
"peerDependenciesMeta": {
|
|
56
68
|
"antd": {
|
|
57
69
|
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"react": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"react-dom": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"vee-validate": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"vue": {
|
|
81
|
+
"optional": true
|
|
58
82
|
}
|
|
59
83
|
},
|
|
60
84
|
"devDependencies": {
|
|
@@ -65,23 +89,43 @@
|
|
|
65
89
|
"@types/node": "^26.1.1",
|
|
66
90
|
"@types/react": "^19.2.17",
|
|
67
91
|
"@types/react-dom": "^19.2.3",
|
|
92
|
+
"@vitest/browser": "4.1.10",
|
|
93
|
+
"@vitest/browser-playwright": "^4.1.10",
|
|
68
94
|
"@vitest/coverage-v8": "4.1.10",
|
|
95
|
+
"@vue/server-renderer": "^3.5.40",
|
|
96
|
+
"@vue/test-utils": "^2.4.11",
|
|
69
97
|
"antd": "^6.5.1",
|
|
70
98
|
"inputmask": "5.0.10-beta.61",
|
|
71
99
|
"jsdom": "^29.1.1",
|
|
72
100
|
"oxlint": "1.74.0",
|
|
101
|
+
"playwright": "^1.62.0",
|
|
73
102
|
"react-hook-form": "7.82.0",
|
|
74
103
|
"tsdown": "^0.22.9",
|
|
75
104
|
"typescript": "~6.0.3",
|
|
76
|
-
"
|
|
105
|
+
"vee-validate": "^4.15.1",
|
|
106
|
+
"vitest": "4.1.10",
|
|
107
|
+
"vue": "^3.5.40"
|
|
77
108
|
},
|
|
109
|
+
"keywords": [
|
|
110
|
+
"react",
|
|
111
|
+
"vue",
|
|
112
|
+
"react-hook-form",
|
|
113
|
+
"tanstack-form",
|
|
114
|
+
"vee-validate",
|
|
115
|
+
"antd",
|
|
116
|
+
"input-mask",
|
|
117
|
+
"input-masking",
|
|
118
|
+
"vue-mask",
|
|
119
|
+
"mask"
|
|
120
|
+
],
|
|
78
121
|
"scripts": {
|
|
79
122
|
"build": "tsdown",
|
|
80
123
|
"dev": "tsdown --watch",
|
|
81
124
|
"lint": "oxlint ./src",
|
|
82
|
-
"test": "vitest --dir ./src --run --coverage",
|
|
125
|
+
"test": "vitest --project=unit --dir ./src --run --coverage",
|
|
126
|
+
"test:browser": "vitest --project=browser --run",
|
|
83
127
|
"type-check": "tsc --noEmit",
|
|
84
128
|
"clean": "rm -rf dist",
|
|
85
|
-
"postbuild": "cp ../../README.md README.md"
|
|
129
|
+
"postbuild": "node scripts/assert-entry-isolation.mjs && cp ../../README.md README.md"
|
|
86
130
|
}
|
|
87
131
|
}
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
interface InputmaskInstance {
|
|
13
13
|
unmaskedvalue?: () => string;
|
|
14
|
+
/** Resolved configuration, after alias defaults are merged with user options. */
|
|
15
|
+
opts?: Record<string, unknown>;
|
|
16
|
+
/** Detaches the mask from its element. Used by the Vue directive on unmount. */
|
|
17
|
+
remove?: () => void;
|
|
18
|
+
/** Pushes a value through the engine's own validation and formatting. */
|
|
19
|
+
setValue?: (value: string) => void;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
interface HTMLInputElement {
|
package/src/types/index.ts
CHANGED
|
@@ -4,35 +4,15 @@ import type {
|
|
|
4
4
|
UseFormRegisterReturn,
|
|
5
5
|
} from 'react-hook-form';
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type { UnmaskedValueApi } from './mask';
|
|
8
8
|
|
|
9
9
|
export type { UseFormRegister, UseFormRegisterReturn } from 'react-hook-form';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
| 'integer'
|
|
17
|
-
| 'percentage'
|
|
18
|
-
| 'url'
|
|
19
|
-
| 'ip'
|
|
20
|
-
| 'mac'
|
|
21
|
-
| 'ssn'
|
|
22
|
-
| 'brl-currency'
|
|
23
|
-
| 'cpf'
|
|
24
|
-
| 'cnpj'
|
|
25
|
-
| 'br-bank-account'
|
|
26
|
-
| 'br-bank-agency'
|
|
27
|
-
| (string & {})
|
|
28
|
-
| (string[] & {})
|
|
29
|
-
| null;
|
|
30
|
-
export type Options = MaskOptions;
|
|
31
|
-
export type Input = HTMLInputElement | HTMLTextAreaElement | HTMLElement;
|
|
32
|
-
|
|
33
|
-
export interface UnmaskedValueApi {
|
|
34
|
-
unmaskedValue: () => string;
|
|
35
|
-
}
|
|
11
|
+
// Framework-agnostic types live in ./mask so the Vue entry can import them
|
|
12
|
+
// without pulling this module's react / react-hook-form imports with them.
|
|
13
|
+
export type {
|
|
14
|
+
Input, Mask, Options, UnmaskedValueApi,
|
|
15
|
+
} from './mask';
|
|
36
16
|
|
|
37
17
|
export type UseMaskInputReturn = RefCallback<HTMLElement | null> & UnmaskedValueApi;
|
|
38
18
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Options as MaskOptions } from './inputmask.types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Framework-agnostic mask types.
|
|
5
|
+
*
|
|
6
|
+
* Split out of `./index.ts` so the Vue entry can import them without dragging
|
|
7
|
+
* in that module's `react` and `react-hook-form` type imports. `./index.ts`
|
|
8
|
+
* re-exports everything here, so the React-facing surface is unchanged.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type Mask = 'datetime'
|
|
12
|
+
| 'email'
|
|
13
|
+
| 'numeric'
|
|
14
|
+
| 'currency'
|
|
15
|
+
| 'decimal'
|
|
16
|
+
| 'integer'
|
|
17
|
+
| 'percentage'
|
|
18
|
+
| 'url'
|
|
19
|
+
| 'ip'
|
|
20
|
+
| 'mac'
|
|
21
|
+
| 'ssn'
|
|
22
|
+
| 'brl-currency'
|
|
23
|
+
| 'cpf'
|
|
24
|
+
| 'cnpj'
|
|
25
|
+
| 'br-bank-account'
|
|
26
|
+
| 'br-bank-agency'
|
|
27
|
+
| (string & {})
|
|
28
|
+
| (string[] & {})
|
|
29
|
+
| null;
|
|
30
|
+
|
|
31
|
+
export type Options = MaskOptions;
|
|
32
|
+
|
|
33
|
+
export type Input = HTMLInputElement | HTMLTextAreaElement | HTMLElement;
|
|
34
|
+
|
|
35
|
+
export interface UnmaskedValueApi {
|
|
36
|
+
unmaskedValue: () => string;
|
|
37
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { applyMaskToElement } from '../core';
|
|
2
|
+
import { makeMaskCacheKey } from '../utils';
|
|
3
|
+
import isServer from '../utils/isServer';
|
|
4
|
+
|
|
5
|
+
import type { Mask, Options, VueMaskBinding, VueMaskConfig } from './types';
|
|
6
|
+
|
|
7
|
+
type MaskedElement = HTMLElement & {
|
|
8
|
+
inputmask?: { remove?: () => void };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Single implementation both Vue surfaces route through, so the directive and
|
|
13
|
+
* the composable cannot drift apart on what a given mask means.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function isConfig(binding: VueMaskBinding): binding is VueMaskConfig {
|
|
17
|
+
return (
|
|
18
|
+
typeof binding === 'object'
|
|
19
|
+
&& binding !== null
|
|
20
|
+
&& !Array.isArray(binding)
|
|
21
|
+
&& 'mask' in binding
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Unpacks the two accepted binding shapes into a mask and its options. */
|
|
26
|
+
export function normalizeBinding(binding: VueMaskBinding): {
|
|
27
|
+
mask: Mask;
|
|
28
|
+
options?: Options;
|
|
29
|
+
} {
|
|
30
|
+
if (isConfig(binding)) {
|
|
31
|
+
return { mask: binding.mask, options: binding.options };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { mask: binding as Mask };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shallow equality over mask options.
|
|
39
|
+
*
|
|
40
|
+
* ponytail: shallow on purpose. An options object mutated in place, or one
|
|
41
|
+
* holding functions, won't compare equal-then-changed correctly — but deep
|
|
42
|
+
* comparison on every re-render is exactly the cost this guard exists to
|
|
43
|
+
* avoid. Documented as "replace options, don't mutate them".
|
|
44
|
+
*/
|
|
45
|
+
function sameOptions(a?: Options, b?: Options): boolean {
|
|
46
|
+
if (a === b) return true;
|
|
47
|
+
if (!a || !b) return false;
|
|
48
|
+
|
|
49
|
+
const keysA = Object.keys(a) as (keyof Options)[];
|
|
50
|
+
const keysB = Object.keys(b) as (keyof Options)[];
|
|
51
|
+
if (keysA.length !== keysB.length) return false;
|
|
52
|
+
|
|
53
|
+
return keysA.every((key) => Object.is(a[key], b[key]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* True when two bindings mean the same mask, so an unrelated re-render doesn't
|
|
58
|
+
* rebuild Inputmask's buffer and drop the caret to the end mid-typing.
|
|
59
|
+
*
|
|
60
|
+
* Compares structurally, not by identity: an inline `v-mask-input="{ mask: 'cpf' }"`
|
|
61
|
+
* allocates a fresh object every render and would otherwise never look equal.
|
|
62
|
+
*/
|
|
63
|
+
export function sameBinding(a: VueMaskBinding, b: VueMaskBinding): boolean {
|
|
64
|
+
const left = normalizeBinding(a);
|
|
65
|
+
const right = normalizeBinding(b);
|
|
66
|
+
|
|
67
|
+
if (makeMaskCacheKey('', left.mask) !== makeMaskCacheKey('', right.mask)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return sameOptions(left.options, right.options);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Applies a binding's mask to an already-resolved element.
|
|
76
|
+
*
|
|
77
|
+
* A null mask is not merely "do nothing": it has to tear down whatever mask is
|
|
78
|
+
* already on the element. A binding is reactive, so `v-mask-input="on ? 'cpf' : null"`
|
|
79
|
+
* turns the mask off, and simply returning early would leave the element still
|
|
80
|
+
* formatting as a CPF.
|
|
81
|
+
*
|
|
82
|
+
* @param element - The element to mask
|
|
83
|
+
* @param binding - Mask, or `{ mask, options }`
|
|
84
|
+
*/
|
|
85
|
+
export default function applyMask(
|
|
86
|
+
element: HTMLElement | null,
|
|
87
|
+
binding: VueMaskBinding,
|
|
88
|
+
): void {
|
|
89
|
+
if (isServer || !element) return;
|
|
90
|
+
|
|
91
|
+
const { mask, options } = normalizeBinding(binding);
|
|
92
|
+
|
|
93
|
+
if (mask === null || mask === undefined) {
|
|
94
|
+
(element as MaskedElement).inputmask?.remove?.();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
applyMaskToElement(element, mask, options);
|
|
99
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import applyMask, { sameBinding } from './applyMask';
|
|
2
|
+
import resolveVueElement from './resolveVueElement';
|
|
3
|
+
|
|
4
|
+
import type { ObjectDirective } from 'vue';
|
|
5
|
+
|
|
6
|
+
import type { VueMaskBinding } from './types';
|
|
7
|
+
|
|
8
|
+
type MaskedElement = HTMLElement & {
|
|
9
|
+
inputmask?: { remove?: () => void };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `v-mask-input` — the only thing in the Vue entry that applies a mask.
|
|
14
|
+
*
|
|
15
|
+
* Getting real lifecycle hooks is why this, and not the composable, is the
|
|
16
|
+
* primitive: the React surface has no equivalent of `unmounted`, which is why
|
|
17
|
+
* it never tears Inputmask down.
|
|
18
|
+
*
|
|
19
|
+
* Note there is no `v-model` handling here, and none is needed. Inputmask
|
|
20
|
+
* replaces the element's `value` property with its own accessor, so `v-model`
|
|
21
|
+
* reads and writes through the engine rather than around it — including
|
|
22
|
+
* returning the unmasked value under `autoUnmask`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```vue
|
|
26
|
+
* <script setup>
|
|
27
|
+
* import { vMaskInput } from 'use-mask-input/vue'
|
|
28
|
+
* </script>
|
|
29
|
+
*
|
|
30
|
+
* <template>
|
|
31
|
+
* <input v-mask-input="'cpf'" />
|
|
32
|
+
* <input v-mask-input="{ mask: 'currency', options: { prefix: 'R$ ' } }" />
|
|
33
|
+
* </template>
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
const vMaskInput: ObjectDirective<HTMLElement, VueMaskBinding> = {
|
|
37
|
+
mounted(el, binding) {
|
|
38
|
+
applyMask(resolveVueElement(el), binding.value);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
updated(el, binding) {
|
|
42
|
+
// Every parent re-render lands here. Re-masking an unchanged binding would
|
|
43
|
+
// rebuild the buffer and send the caret to the end while the user types.
|
|
44
|
+
if (sameBinding(binding.value, binding.oldValue as VueMaskBinding)) return;
|
|
45
|
+
|
|
46
|
+
applyMask(resolveVueElement(el), binding.value);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
unmounted(el) {
|
|
50
|
+
const target = resolveVueElement(el) as MaskedElement | null;
|
|
51
|
+
target?.inputmask?.remove?.();
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Without this, @vue/server-renderer warns about an unhandled custom
|
|
55
|
+
// directive. There is nothing to render server-side.
|
|
56
|
+
getSSRProps: () => ({}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default vMaskInput;
|
package/src/vue/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { default as vMaskInput } from './directive';
|
|
2
|
+
export { default as useMaskInput } from './useMaskInput';
|
|
3
|
+
|
|
4
|
+
export { formatWithMask, unformatWithMask } from '../core';
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
Mask,
|
|
8
|
+
MaskRefTarget,
|
|
9
|
+
Options,
|
|
10
|
+
UseMaskInputReturn,
|
|
11
|
+
VueMaskBinding,
|
|
12
|
+
VueMaskConfig,
|
|
13
|
+
} from './types';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { findInputElement, isHTMLElement } from '../core/elementResolver';
|
|
2
|
+
|
|
3
|
+
import type { MaskRefTarget } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves whatever Vue hands us into the element that should carry the mask.
|
|
7
|
+
*
|
|
8
|
+
* A directive always receives a DOM element. A `:ref` on a component receives
|
|
9
|
+
* the component's public instance instead, so the element has to be dug out of
|
|
10
|
+
* `$el` first. Either way the existing `findInputElement` does the final
|
|
11
|
+
* wrapper search, which is the same code path that makes Ant Design work on the
|
|
12
|
+
* React side.
|
|
13
|
+
*
|
|
14
|
+
* Fragment-root components resolve to `null` on purpose: their `$el` is a text
|
|
15
|
+
* anchor node, and Vue itself refuses this case for directives
|
|
16
|
+
* ("Runtime directive used on component with non-element root node"). Walking
|
|
17
|
+
* up to `parentElement` could just as easily find a sibling input as the
|
|
18
|
+
* intended one.
|
|
19
|
+
*
|
|
20
|
+
* @param target - Element, component public instance, or null
|
|
21
|
+
* @returns The element to mask, or null if there isn't one
|
|
22
|
+
*/
|
|
23
|
+
export default function resolveVueElement(target: MaskRefTarget): HTMLElement | null {
|
|
24
|
+
if (!target) return null;
|
|
25
|
+
|
|
26
|
+
if (isHTMLElement(target)) {
|
|
27
|
+
return findInputElement(target);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { $el } = target as { $el?: unknown };
|
|
31
|
+
if (!isHTMLElement($el)) return null;
|
|
32
|
+
|
|
33
|
+
return findInputElement($el);
|
|
34
|
+
}
|
package/src/vue/types.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Mask, Options } from '../types/mask';
|
|
2
|
+
|
|
3
|
+
export type { Mask, Options } from '../types/mask';
|
|
4
|
+
|
|
5
|
+
/** Object form of the directive binding, for when options are needed. */
|
|
6
|
+
export interface VueMaskConfig {
|
|
7
|
+
mask: Mask;
|
|
8
|
+
options?: Options;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Everything `v-mask-input` accepts. A bare mask covers the common case;
|
|
13
|
+
* the object form adds options.
|
|
14
|
+
*/
|
|
15
|
+
export type VueMaskBinding = Mask | VueMaskConfig;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What Vue hands a `:ref` callback: a DOM element for a native tag, or the
|
|
19
|
+
* component's public instance for a component. `$el` is typed loosely because
|
|
20
|
+
* a fragment-root component exposes a text anchor node there, not an element.
|
|
21
|
+
*/
|
|
22
|
+
export type MaskRefTarget = Element | { $el?: unknown } | null;
|
|
23
|
+
|
|
24
|
+
export interface UseMaskInputReturn {
|
|
25
|
+
/** Bind with `:ref="maskRef"`. */
|
|
26
|
+
maskRef: (target: MaskRefTarget) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Current unmasked value. Call this from event handlers and imperative code.
|
|
29
|
+
*
|
|
30
|
+
* NOT reactive: `{{ unmaskedValue() }}` in a template renders once and never
|
|
31
|
+
* updates, because reading the DOM registers no reactive dependency. For a
|
|
32
|
+
* value the template should track, bind `v-model` with `autoUnmask: true`.
|
|
33
|
+
*/
|
|
34
|
+
unmaskedValue: () => string;
|
|
35
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import applyMask from './applyMask';
|
|
2
|
+
import resolveVueElement from './resolveVueElement';
|
|
3
|
+
import { getUnmaskedValue } from '../utils';
|
|
4
|
+
import isServer from '../utils/isServer';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
MaskRefTarget, Mask, Options, UseMaskInputReturn,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Composable form, for imperative use and for reading the raw value when
|
|
12
|
+
* `autoUnmask` is off.
|
|
13
|
+
*
|
|
14
|
+
* Narrower than the directive on purpose: wrapper components (PrimeVue,
|
|
15
|
+
* Element Plus, Ant Design Vue) are already covered by `v-mask-input`, because
|
|
16
|
+
* Vue applies directives to a component's root element and the mask engine
|
|
17
|
+
* searches inside it. Reach for this when you need `unmaskedValue()` or a ref
|
|
18
|
+
* you can hold.
|
|
19
|
+
*
|
|
20
|
+
* @param mask - The mask pattern or alias
|
|
21
|
+
* @param options - Optional mask configuration options
|
|
22
|
+
* @returns A ref callback to bind, and an unmasked-value accessor
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```vue
|
|
26
|
+
* <script setup>
|
|
27
|
+
* import { useMaskInput } from 'use-mask-input/vue'
|
|
28
|
+
* const { maskRef, unmaskedValue } = useMaskInput('cpf')
|
|
29
|
+
* function submit() { console.log(unmaskedValue()) }
|
|
30
|
+
* </script>
|
|
31
|
+
*
|
|
32
|
+
* <template><input :ref="maskRef" /></template>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export default function useMaskInput(mask: Mask, options?: Options): UseMaskInputReturn {
|
|
36
|
+
if (isServer) {
|
|
37
|
+
return {
|
|
38
|
+
maskRef: () => {
|
|
39
|
+
// server doesn't have dom, so just do nothing
|
|
40
|
+
},
|
|
41
|
+
unmaskedValue: () => '',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let element: HTMLElement | null = null;
|
|
46
|
+
|
|
47
|
+
const maskRef = (target: MaskRefTarget): void => {
|
|
48
|
+
element = resolveVueElement(target);
|
|
49
|
+
applyMask(element, { mask, options });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
maskRef,
|
|
54
|
+
unmaskedValue: () => getUnmaskedValue(element),
|
|
55
|
+
};
|
|
56
|
+
}
|