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.
- package/CHANGELOG.md +55 -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 -2
- package/dist/antd.d.mts +1 -2
- package/dist/antd.mjs +1 -1
- package/dist/antd.mjs.map +1 -1
- package/dist/core-BopfTGvA.cjs +2 -0
- package/dist/{withMask-BkTzstzt.cjs.map → core-BopfTGvA.cjs.map} +1 -1
- package/dist/core-CNGYEIpO.mjs +2 -0
- package/dist/{withMask-C-wAMt0W.mjs.map → core-CNGYEIpO.mjs.map} +1 -1
- package/dist/{index-Do9euvWK.d.cts → index-CqBLkcNO.d.cts} +393 -385
- package/dist/{index-Do9euvWK.d.mts → index-CqBLkcNO.d.mts} +393 -385
- 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 -24
- package/dist/index.d.mts +2 -24
- 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 +56 -12
- package/src/@types/inputmask.d.ts +6 -0
- package/src/api/useHookFormMask.ts +20 -5
- package/src/api/withMask.ts +2 -0
- package/src/core/maskEngine.ts +56 -27
- 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-BkTzstzt.cjs +0 -2
- 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.
|
|
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": {
|
|
@@ -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": "^
|
|
89
|
+
"@types/node": "^26.1.1",
|
|
66
90
|
"@types/react": "^19.2.17",
|
|
67
91
|
"@types/react-dom": "^19.2.3",
|
|
68
|
-
"@vitest/
|
|
69
|
-
"
|
|
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.
|
|
73
|
-
"
|
|
74
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
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);
|
package/src/api/withMask.ts
CHANGED
|
@@ -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
|
|
package/src/core/maskEngine.ts
CHANGED
|
@@ -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
|
+
}
|
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
|
+
}
|