use-mask-input 3.11.0 → 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +102 -1
  3. package/dist/antd.cjs +1 -1
  4. package/dist/antd.cjs.map +1 -1
  5. package/dist/antd.d.cts +1 -2
  6. package/dist/antd.d.mts +1 -2
  7. package/dist/antd.mjs +1 -1
  8. package/dist/antd.mjs.map +1 -1
  9. package/dist/core-BopfTGvA.cjs +2 -0
  10. package/dist/{withMask-BkTzstzt.cjs.map → core-BopfTGvA.cjs.map} +1 -1
  11. package/dist/core-CNGYEIpO.mjs +2 -0
  12. package/dist/{withMask-C-wAMt0W.mjs.map → core-CNGYEIpO.mjs.map} +1 -1
  13. package/dist/{index-Do9euvWK.d.cts → index-CqBLkcNO.d.cts} +393 -385
  14. package/dist/{index-Do9euvWK.d.mts → index-CqBLkcNO.d.mts} +393 -385
  15. package/dist/index-DAfluDSv.d.cts +25 -0
  16. package/dist/index-DGAwRzYx.d.mts +25 -0
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +2 -24
  20. package/dist/index.d.mts +2 -24
  21. package/dist/index.mjs +1 -1
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/vue.cjs +2 -0
  24. package/dist/vue.cjs.map +1 -0
  25. package/dist/vue.d.cts +92 -0
  26. package/dist/vue.d.mts +92 -0
  27. package/dist/vue.mjs +2 -0
  28. package/dist/vue.mjs.map +1 -0
  29. package/dist/withMask--iSmiRbB.cjs +2 -0
  30. package/dist/withMask--iSmiRbB.cjs.map +1 -0
  31. package/dist/withMask-DOmJHTJ4.mjs +2 -0
  32. package/dist/withMask-DOmJHTJ4.mjs.map +1 -0
  33. package/package.json +56 -12
  34. package/src/@types/inputmask.d.ts +6 -0
  35. package/src/api/useHookFormMask.ts +20 -5
  36. package/src/api/withMask.ts +2 -0
  37. package/src/core/maskEngine.ts +56 -27
  38. package/src/types/index.ts +6 -26
  39. package/src/types/mask.ts +37 -0
  40. package/src/vue/applyMask.ts +99 -0
  41. package/src/vue/directive.ts +59 -0
  42. package/src/vue/index.ts +13 -0
  43. package/src/vue/resolveVueElement.ts +34 -0
  44. package/src/vue/types.ts +35 -0
  45. package/src/vue/useMaskInput.ts +56 -0
  46. package/dist/withMask-BkTzstzt.cjs +0 -2
  47. package/dist/withMask-C-wAMt0W.mjs +0 -2
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "use-mask-input",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "private": false,
5
- "description": "A react Hook for build elegant input masks. Compatible with React Hook Form",
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": {
@@ -62,26 +86,46 @@
62
86
  "@testing-library/dom": "^10.4.1",
63
87
  "@testing-library/react": "^16.3.2",
64
88
  "@types/inputmask": "5.0.7",
65
- "@types/node": "^25.9.3",
89
+ "@types/node": "^26.1.1",
66
90
  "@types/react": "^19.2.17",
67
91
  "@types/react-dom": "^19.2.3",
68
- "@vitest/coverage-v8": "4.1.7",
69
- "antd": "^6.4.4",
92
+ "@vitest/browser": "4.1.10",
93
+ "@vitest/browser-playwright": "^4.1.10",
94
+ "@vitest/coverage-v8": "4.1.10",
95
+ "@vue/server-renderer": "^3.5.40",
96
+ "@vue/test-utils": "^2.4.11",
97
+ "antd": "^6.5.1",
70
98
  "inputmask": "5.0.10-beta.61",
71
99
  "jsdom": "^29.1.1",
72
- "oxlint": "1.66.0",
73
- "react-hook-form": "7.75.0",
74
- "tsdown": "^0.22.2",
100
+ "oxlint": "1.74.0",
101
+ "playwright": "^1.62.0",
102
+ "react-hook-form": "7.82.0",
103
+ "tsdown": "^0.22.9",
75
104
  "typescript": "~6.0.3",
76
- "vitest": "4.1.7"
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 {
@@ -79,11 +79,26 @@ export default function useHookFormMask<
79
79
  return _ref;
80
80
  };
81
81
 
82
- nextEntry.stableRef = (
83
- nextEntry.latestRHFRef
84
- ? flow(applyMaskToRef, (_ref: HTMLElement | null) => nextEntry.latestRHFRef?.(_ref))
85
- : applyMaskToRef
86
- ) as RefCallback<HTMLElement | null>;
82
+ // RHF must run first: it writes the field's predefined value onto the
83
+ // element. If we masked before that, Inputmask would initialise with an
84
+ // empty buffer and RHF's later value assignment would leave its internal
85
+ // state out of sync with the DOM, swallowing the first onChange after a
86
+ // select-all + delete (#193). Masking after RHF seeds Inputmask with the
87
+ // current value, keeping its buffer in sync from the start.
88
+ const syncRHFRef = (_ref: HTMLElement | null) => {
89
+ nextEntry.latestRHFRef?.(_ref);
90
+ return _ref;
91
+ };
92
+
93
+ const composedRef = nextEntry.latestRHFRef
94
+ ? flow(syncRHFRef, applyMaskToRef)
95
+ : applyMaskToRef;
96
+
97
+ // React 19 treats a ref callback's return value as a cleanup function,
98
+ // so the composed callback must return void — never the resolved element.
99
+ nextEntry.stableRef = ((_ref: HTMLElement | null): void => {
100
+ composedRef(_ref);
101
+ }) as RefCallback<HTMLElement | null>;
87
102
 
88
103
  entry = nextEntry;
89
104
  entryCacheRef.current.set(cacheKey, nextEntry);
@@ -2,6 +2,7 @@
2
2
  import inputmask from '../core/inputmask';
3
3
 
4
4
  import { getMaskOptions } from '../core/maskConfig';
5
+ import { stripMaxLength } from '../core/maskEngine';
5
6
  import { getUnmaskedValue, makeMaskCacheKey, setUnmaskedValue } from '../utils';
6
7
  import isServer from '../utils/isServer';
7
8
  import interopDefaultSync from '../utils/moduleInterop';
@@ -34,6 +35,7 @@ export default function withMask(mask: Mask, options?: Options): UseMaskInputRet
34
35
 
35
36
  currentInput = input;
36
37
  const maskInput = interopDefaultSync(inputmask)(getMaskOptions(mask, options));
38
+ stripMaxLength(input, mask, options);
37
39
  maskInput.mask(input as HTMLElement);
38
40
  }) as UseMaskInputReturn;
39
41
 
@@ -2,6 +2,7 @@
2
2
  import inputmask from './inputmask';
3
3
 
4
4
  import { getMaskOptions } from './maskConfig';
5
+ import { isHTMLElement } from './elementResolver';
5
6
  import { moduleInterop } from '../utils';
6
7
 
7
8
  import type { Mask, Options } from '../types';
@@ -19,33 +20,6 @@ export function createMaskInstance(mask: Mask, options?: Options): ReturnType<ty
19
20
  return inputmaskInstance(getMaskOptions(mask, options));
20
21
  }
21
22
 
22
- /**
23
- * Applies a mask to an input element.
24
- * If it's not a direct input, searches inside.
25
- *
26
- * @param element - The element to apply mask to
27
- * @param mask - The mask pattern
28
- * @param options - Optional configuration options
29
- */
30
- export function applyMaskToElement(
31
- element: HTMLElement | null,
32
- mask: Mask,
33
- options?: Options,
34
- ): void {
35
- if (!element) return;
36
-
37
- const maskInstance = createMaskInstance(mask, options);
38
- const inputElement = element.nodeName === 'INPUT'
39
- ? element
40
- : (element.querySelector('input') as HTMLElement);
41
-
42
- if (inputElement) {
43
- maskInstance.mask(inputElement);
44
- } else {
45
- maskInstance.mask(element);
46
- }
47
- }
48
-
49
23
  /**
50
24
  * Formats a raw value using the given mask, without needing a mounted element.
51
25
  * Useful for displaying already-persisted (unmasked) values in the UI.
@@ -73,3 +47,58 @@ export function unformatWithMask(value: string, mask: Mask, options?: Options):
73
47
  const inputmaskInstance = moduleInterop(inputmask);
74
48
  return inputmaskInstance.unmask(value, getMaskOptions(mask, options));
75
49
  }
50
+
51
+ /**
52
+ * Drops the native `maxlength` when the mask renders characters the user never types.
53
+ *
54
+ * A mask like `999.999.999-99` keeps its literals and placeholder in the value, so
55
+ * `maxlength` counts characters nobody typed: the browser stops accepting keystrokes,
56
+ * and inputmask copies the attribute into a validator that rejects any buffer longer
57
+ * than it. Either way the field takes no input at all.
58
+ *
59
+ * Open-ended masks (`numeric`, `integer`, `decimal`) render nothing while empty, so
60
+ * their value only ever holds what was typed and `maxlength` still caps what it says
61
+ * it caps. Those keep the attribute. Rendering the empty value is what tells the two
62
+ * apart, since aliases hide their real pattern.
63
+ *
64
+ * ponytail: the empty render is a proxy, not a proof. A mask that starts empty but
65
+ * grows a literal later (`br-bank-agency` gains a `-` at the 6th digit) still loses
66
+ * that many characters off the cap. Sharpen this only if someone hits it.
67
+ *
68
+ * @see https://github.com/eduardoborges/use-mask-input/issues/191
69
+ * @param element - The element about to be masked
70
+ * @param mask - The mask pattern about to be applied
71
+ * @param options - Optional configuration options
72
+ */
73
+ export function stripMaxLength(element: unknown, mask: Mask, options?: Options): void {
74
+ if (!isHTMLElement(element) || !element.hasAttribute('maxlength')) return;
75
+ if (formatWithMask('', mask, options).length === 0) return;
76
+
77
+ element.removeAttribute('maxlength');
78
+ }
79
+
80
+ /**
81
+ * Applies a mask to an input element.
82
+ * If it's not a direct input, searches inside.
83
+ *
84
+ * @param element - The element to apply mask to
85
+ * @param mask - The mask pattern
86
+ * @param options - Optional configuration options
87
+ */
88
+ export function applyMaskToElement(
89
+ element: HTMLElement | null,
90
+ mask: Mask,
91
+ options?: Options,
92
+ ): void {
93
+ if (!element) return;
94
+
95
+ const maskInstance = createMaskInstance(mask, options);
96
+ const inputElement = element.nodeName === 'INPUT'
97
+ ? element
98
+ : (element.querySelector('input') as HTMLElement);
99
+
100
+ const target = inputElement ?? element;
101
+
102
+ stripMaxLength(target, mask, options);
103
+ maskInstance.mask(target);
104
+ }
@@ -4,35 +4,15 @@ import type {
4
4
  UseFormRegisterReturn,
5
5
  } from 'react-hook-form';
6
6
 
7
- import type { Options as MaskOptions } from './inputmask.types';
7
+ import type { UnmaskedValueApi } from './mask';
8
8
 
9
9
  export type { UseFormRegister, UseFormRegisterReturn } from 'react-hook-form';
10
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
- 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;
@@ -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
+ }
@@ -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
+ }