maskarajs 1.0.0 → 1.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 +424 -171
- package/package.json +50 -13
- package/src/adapters/react/index.cjs.js +89 -0
- package/src/adapters/react/index.d.ts +57 -0
- package/src/adapters/react/index.mjs +80 -0
- package/src/adapters/vue/index.cjs.js +151 -0
- package/src/adapters/vue/index.d.ts +46 -0
- package/src/adapters/vue/index.mjs +147 -0
- package/{mask.cjs.js → src/core/mask.cjs.js} +176 -45
- package/{mask.d.ts → src/core/mask.d.ts} +50 -8
- package/{mask.mjs → src/core/mask.mjs} +173 -45
- package/src/presets/br.cjs.js +45 -0
- package/src/presets/br.d.ts +23 -0
- package/src/presets/br.mjs +40 -0
- package/mask.js +0 -874
package/README.md
CHANGED
|
@@ -1,94 +1,226 @@
|
|
|
1
|
-
#
|
|
1
|
+
# maskarajs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Declarative input mask engine for JavaScript. Framework-agnostic, zero dependencies, and small enough to live close to your form fields.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Syntax
|
|
6
6
|
|
|
7
|
-
| Token |
|
|
7
|
+
| Token | Accepts |
|
|
8
8
|
|---|---|
|
|
9
|
-
| `#` |
|
|
10
|
-
| `@` |
|
|
11
|
-
| `*` |
|
|
12
|
-
| `[
|
|
13
|
-
| `{expr}` | slot
|
|
9
|
+
| `#` | any digit (`0-9`) |
|
|
10
|
+
| `@` | any letter (`a-z`, `A-Z`, accented letters) |
|
|
11
|
+
| `*` | any character |
|
|
12
|
+
| `[text]` | fixed literal text, inserted and removed automatically |
|
|
13
|
+
| `{expr}` | free slot: tests one character against an expression |
|
|
14
|
+
|
|
15
|
+
### `{expr}` modifier
|
|
16
|
+
|
|
17
|
+
```txt
|
|
18
|
+
{4} -> only the char "4"
|
|
19
|
+
{0-4} -> range: ch >= "0" && ch <= "4"
|
|
20
|
+
{013} -> set: "0", "1" or "3"
|
|
21
|
+
{[0-9a-f]} -> regex: any hexadecimal char
|
|
22
|
+
{\d} -> regex: any digit
|
|
23
|
+
{[^aeiou]} -> regex: any consonant
|
|
24
|
+
```
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
## Install
|
|
16
27
|
|
|
28
|
+
```bash
|
|
29
|
+
npm install maskarajs
|
|
17
30
|
```
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{[0-9a-f]} → regex: qualquer char hex
|
|
22
|
-
{\d} → regex: qualquer dígito
|
|
23
|
-
{[^aeiou]} → regex: consoante
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import maskara from 'maskarajs'
|
|
24
34
|
```
|
|
25
35
|
|
|
26
36
|
## API
|
|
27
37
|
|
|
28
38
|
```js
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
maskara(pattern, value) // apply mask -> formatted string
|
|
40
|
+
maskara.raw(pattern, value) // clean value / transform result
|
|
41
|
+
maskara.is(pattern, value) // complete pattern? -> boolean
|
|
42
|
+
maskara.hint(pattern) // readable placeholder -> string
|
|
43
|
+
maskara.format(pattern, value) // semantic alias for maskara()
|
|
44
|
+
maskara.rawLength(pattern, value) // filled input chars -> number
|
|
45
|
+
maskara.patternLength(pattern) // full formatted length -> number
|
|
46
|
+
maskara.define(name, definition) // register a named mask
|
|
47
|
+
maskara.undefine(name) // remove a named mask
|
|
48
|
+
maskara.names() // list registered names -> string[]
|
|
49
|
+
maskara.defineSlot(symbol, definition) // create or override an input token
|
|
50
|
+
maskara.undefineSlot(symbol) // remove a custom token
|
|
51
|
+
maskara.slots() // list available input tokens
|
|
52
|
+
maskara.on(input, pattern, options) // bind to a DOM input -> cleanup()
|
|
53
|
+
maskara.create(presets) // isolated instance with its own registry
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## React adapter
|
|
57
|
+
|
|
58
|
+
The React adapter is optional and lives in `maskarajs/react`. Use `useMaskara` directly for small forms, or wrap your app with `MaskaraProvider` when you have an isolated engine created with `maskara.create()`.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import maskara from 'maskarajs'
|
|
62
|
+
import { MaskaraProvider, useMaskara } from 'maskarajs/react'
|
|
63
|
+
|
|
64
|
+
const appMaskara = maskara.create({
|
|
65
|
+
cpf: { pattern: '###[.]###[.]###[-]##' },
|
|
66
|
+
phone: { pattern: ['[(]##[)] ####[-]####', '[(]##[)] #####[-]####'] },
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
function CPFInput() {
|
|
70
|
+
const cpf = useMaskara('cpf')
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<input
|
|
74
|
+
{...cpf.inputProps({ inputMode: 'numeric' })}
|
|
75
|
+
aria-label="CPF"
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function App() {
|
|
81
|
+
return (
|
|
82
|
+
<MaskaraProvider engine={appMaskara}>
|
|
83
|
+
<CPFInput />
|
|
84
|
+
</MaskaraProvider>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`useMaskara` still accepts an `engine` option when a single field needs a specific instance:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
const cpf = useMaskara('cpf', { engine: appMaskara })
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The hook stores field state. The engine stores mask configuration.
|
|
96
|
+
|
|
97
|
+
## Vue 3 adapter
|
|
98
|
+
|
|
99
|
+
The Vue adapter is optional and lives in `maskarajs/vue`. Its default export is a Vue 3 directive ready for `v-maskara`. Use it locally in a component or register it once as a plugin.
|
|
100
|
+
|
|
101
|
+
### Local directive
|
|
102
|
+
|
|
103
|
+
```vue
|
|
104
|
+
<script setup lang="ts">
|
|
105
|
+
import { maskaraDirective as vMaskara } from 'maskarajs/vue'
|
|
106
|
+
|
|
107
|
+
const pattern = '###[.]###[.]###[-]##'
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<input v-maskara="pattern" inputmode="numeric" />
|
|
112
|
+
</template>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### With `v-model` and raw value callback
|
|
116
|
+
|
|
117
|
+
```vue
|
|
118
|
+
<script setup lang="ts">
|
|
119
|
+
import { ref } from 'vue'
|
|
120
|
+
import { maskaraDirective as vMaskara } from 'maskarajs/vue'
|
|
121
|
+
|
|
122
|
+
const document = ref('')
|
|
123
|
+
const rawDocument = ref('')
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<template>
|
|
127
|
+
<input
|
|
128
|
+
v-model="document"
|
|
129
|
+
v-maskara="{
|
|
130
|
+
pattern: '###[.]###[.]###[-]##',
|
|
131
|
+
onValue: value => rawDocument = value
|
|
132
|
+
}"
|
|
133
|
+
inputmode="numeric"
|
|
134
|
+
/>
|
|
135
|
+
</template>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The directive listens during the capture phase, so Vue's `v-model` receives the masked value during the same input event.
|
|
139
|
+
|
|
140
|
+
### Register globally
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { createApp } from 'vue'
|
|
144
|
+
import { createMaskaraPlugin } from 'maskarajs/vue'
|
|
145
|
+
import App from './App.vue'
|
|
146
|
+
|
|
147
|
+
createApp(App)
|
|
148
|
+
.use(createMaskaraPlugin())
|
|
149
|
+
.mount('#app')
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Then use it anywhere:
|
|
153
|
+
|
|
154
|
+
```vue
|
|
155
|
+
<input v-maskara="'#####[-]###'" />
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Use an isolated engine
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import maskara from 'maskarajs'
|
|
162
|
+
import { createMaskaraPlugin } from 'maskarajs/vue'
|
|
163
|
+
|
|
164
|
+
const br = maskara.create({
|
|
165
|
+
cpf: { pattern: '###[.]###[.]###[-]##' },
|
|
166
|
+
phone: { pattern: ['[(]##[)] ####[-]####', '[(]##[)] #####[-]####'] },
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
app.use(createMaskaraPlugin({ engine: br }))
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
You can also create a directive instance manually:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { createMaskaraDirective } from 'maskarajs/vue'
|
|
176
|
+
|
|
177
|
+
const vMaskara = createMaskaraDirective({ engine: br })
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Examples
|
|
49
181
|
|
|
50
182
|
```js
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
//
|
|
183
|
+
// Single pattern
|
|
184
|
+
maskara('###[.]###[.]###[-]##', '12345678909')
|
|
185
|
+
// -> '123.456.789-09'
|
|
54
186
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
//
|
|
187
|
+
// Dynamic pattern array: chooses by input size
|
|
188
|
+
maskara(['[(]##[)] ####[-]####', '[(]##[)] #####[-]####'], '11987654321')
|
|
189
|
+
// -> '(11) 98765-4321'
|
|
58
190
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
//
|
|
191
|
+
// Restricted slot
|
|
192
|
+
maskara('{4}### #### #### ####', '4111111111111111')
|
|
193
|
+
// -> '4111 1111 1111 1111'
|
|
62
194
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
//
|
|
195
|
+
// Regex slot
|
|
196
|
+
maskara('{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}', '1a2b3c')
|
|
197
|
+
// -> '1a2b3c'
|
|
66
198
|
```
|
|
67
199
|
|
|
68
|
-
###
|
|
200
|
+
### Common cases
|
|
69
201
|
|
|
70
202
|
```js
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
//
|
|
203
|
+
// Brazilian ZIP code
|
|
204
|
+
maskara('#####[-]###', '01310930')
|
|
205
|
+
// -> '01310-930'
|
|
74
206
|
|
|
75
207
|
// CNPJ
|
|
76
|
-
|
|
77
|
-
//
|
|
208
|
+
maskara('##[.]###[.]###[/]####[-]##', '11222333000181')
|
|
209
|
+
// -> '11.222.333/0001-81'
|
|
78
210
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
//
|
|
211
|
+
// Visa card
|
|
212
|
+
maskara('{4}### #### #### ####', '5111111111111111')
|
|
213
|
+
// -> '' (the first char did not pass)
|
|
82
214
|
|
|
83
|
-
// Hex color
|
|
84
|
-
|
|
85
|
-
//
|
|
215
|
+
// Hex color with dirty paste
|
|
216
|
+
maskara('{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}', '1z2b3c')
|
|
217
|
+
// -> '12b3c'
|
|
86
218
|
```
|
|
87
219
|
|
|
88
|
-
##
|
|
220
|
+
## `maskara.define` with `transform`
|
|
89
221
|
|
|
90
222
|
```js
|
|
91
|
-
|
|
223
|
+
maskara.define('date', {
|
|
92
224
|
pattern: '##[/]##[/]####',
|
|
93
225
|
validate: (raw, masked, complete) => {
|
|
94
226
|
if (raw.length < 4) return true
|
|
@@ -102,23 +234,21 @@ mask.define('date', {
|
|
|
102
234
|
},
|
|
103
235
|
})
|
|
104
236
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
237
|
+
maskara('date', '01012025') // -> '01/01/2025'
|
|
238
|
+
maskara.raw('date', '01/01/2025') // -> Date(2025-01-01)
|
|
239
|
+
maskara.raw('date', '01/01') // -> null
|
|
240
|
+
maskara.is('date', '01/01/2025') // -> true
|
|
241
|
+
maskara.hint('date') // -> '00/00/0000'
|
|
242
|
+
maskara.rawLength('date', '01/01') // -> 4
|
|
243
|
+
maskara.patternLength('date') // -> 10
|
|
112
244
|
```
|
|
113
245
|
|
|
114
|
-
## validate
|
|
246
|
+
## `validate`: incremental validation
|
|
115
247
|
|
|
116
|
-
Use `validate`
|
|
117
|
-
Isso resolve casos onde a sintaxe caractere a caractere não é suficiente, como mês
|
|
118
|
-
entre `01` e `12`.
|
|
248
|
+
Use `validate` on named masks when the rule depends on what has already been typed. This covers cases where character-by-character syntax is not enough, such as accepting only months from `01` to `12`.
|
|
119
249
|
|
|
120
250
|
```js
|
|
121
|
-
|
|
251
|
+
maskara.define('month', {
|
|
122
252
|
pattern: '{0-1}#',
|
|
123
253
|
validate: (raw, masked, complete) => {
|
|
124
254
|
if (!complete) return true
|
|
@@ -127,98 +257,129 @@ mask.define('month', {
|
|
|
127
257
|
},
|
|
128
258
|
})
|
|
129
259
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
260
|
+
maskara('month', '12') // -> '12'
|
|
261
|
+
maskara('month', '19') // -> '1'
|
|
262
|
+
maskara.is('month', '12') // -> true
|
|
263
|
+
maskara.is('month', '19') // -> false
|
|
134
264
|
```
|
|
135
265
|
|
|
136
|
-
##
|
|
266
|
+
## Conditional masks
|
|
137
267
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
268
|
+
Use `patterns` + `select` on a named mask when the mask must change because of the typed value, not only because of length. Arrays of strings keep the old behavior and still choose the smallest pattern that fits the input. Conditional masks live inside `define` or `create`, where product rules already belong.
|
|
269
|
+
|
|
270
|
+
`select(raw, value)` receives the value without literals from all possible patterns and returns a key from `patterns`.
|
|
141
271
|
|
|
142
272
|
```js
|
|
143
|
-
|
|
273
|
+
const cpf = '###[.]###[.]###[-]##'
|
|
274
|
+
const cnpj = '##[.]###[.]###[/]####[-]##'
|
|
275
|
+
|
|
276
|
+
maskara.define('smartDocument', {
|
|
277
|
+
patterns: { cpf, cnpj },
|
|
278
|
+
select: raw => raw.includes('123') ? 'cnpj' : 'cpf',
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
maskara('smartDocument', '98765432100')
|
|
282
|
+
// -> '987.654.321-00'
|
|
283
|
+
|
|
284
|
+
maskara('smartDocument', '12345678000199')
|
|
285
|
+
// -> '12.345.678/0001-99'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
It also works with isolated instances:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
const forms = maskara.create({
|
|
292
|
+
smartDocument: {
|
|
293
|
+
patterns: { cpf, cnpj },
|
|
294
|
+
select: raw => raw.startsWith('9') ? 'cpf' : 'cnpj',
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Prefer conditional masks for rules that depend on the value itself: prefixes, country codes, card BINs, product codes or any domain rule. Prefer a simple string array when the only difference is input size, such as phone numbers with 10 or 11 digits.
|
|
300
|
+
|
|
301
|
+
## Custom slots: create your own pattern language
|
|
302
|
+
|
|
303
|
+
Besides `#`, `@`, `*` and `{expr}`, you can create symbols that make sense for your team. This is useful when a project needs a shorter, more expressive language aligned with its product domain.
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
maskara.defineSlot('N', {
|
|
144
307
|
test: ch => /\d/.test(ch),
|
|
145
308
|
hint: '0',
|
|
146
309
|
})
|
|
147
310
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
311
|
+
maskara('NNN[-]NN', '12345') // -> '123-45'
|
|
312
|
+
maskara.hint('NNN[-]NN') // -> '000-00'
|
|
313
|
+
maskara.slots() // -> ['#', '@', '*', 'N']
|
|
151
314
|
```
|
|
152
315
|
|
|
153
|
-
|
|
316
|
+
You can also pass a `RegExp` or a function directly:
|
|
154
317
|
|
|
155
318
|
```js
|
|
156
|
-
|
|
157
|
-
|
|
319
|
+
maskara.defineSlot('H', /[0-9a-f]/i)
|
|
320
|
+
maskara.defineSlot('V', ch => 'AEIOUaeiou'.includes(ch))
|
|
158
321
|
|
|
159
|
-
|
|
160
|
-
|
|
322
|
+
maskara('HHHHHH', '1a2b3c') // -> '1a2b3c'
|
|
323
|
+
maskara('VVV', 'mask') // -> 'a'
|
|
161
324
|
```
|
|
162
325
|
|
|
163
|
-
|
|
164
|
-
produtos com regras próprias, prefira uma instância isolada:
|
|
326
|
+
Global slots affect the global engine. For libraries, design systems or products with their own rules, prefer an isolated instance:
|
|
165
327
|
|
|
166
328
|
```js
|
|
167
|
-
const forge =
|
|
329
|
+
const forge = maskara.create()
|
|
168
330
|
|
|
169
331
|
forge.defineSlot('N', { test: ch => /\d/.test(ch), hint: '0' })
|
|
170
332
|
forge.defineSlot('#', { test: ch => /[1-9]/.test(ch), hint: '1' })
|
|
171
333
|
|
|
172
|
-
forge('NNN[-]NN', '12345') //
|
|
173
|
-
forge('#', '0') //
|
|
174
|
-
|
|
334
|
+
forge('NNN[-]NN', '12345') // -> '123-45'
|
|
335
|
+
forge('#', '0') // -> ''
|
|
336
|
+
maskara('#', '0') // -> '0' (global stays unchanged)
|
|
175
337
|
```
|
|
176
338
|
|
|
177
|
-
|
|
178
|
-
colchetes:
|
|
339
|
+
If a registered symbol must appear as fixed text, escape it with brackets:
|
|
179
340
|
|
|
180
341
|
```js
|
|
181
|
-
|
|
342
|
+
maskara.defineSlot('N', /\d/)
|
|
182
343
|
|
|
183
|
-
|
|
184
|
-
|
|
344
|
+
maskara('[N]##', '45') // -> 'N45'
|
|
345
|
+
maskara('N##', '145') // -> '145'
|
|
185
346
|
```
|
|
186
347
|
|
|
187
|
-
##
|
|
348
|
+
## `maskara.on`: any framework
|
|
188
349
|
|
|
189
350
|
```js
|
|
190
351
|
// Vanilla JS
|
|
191
|
-
const off =
|
|
192
|
-
onValue:
|
|
193
|
-
|
|
352
|
+
const off = maskara.on(inputEl, 'cpf', {
|
|
353
|
+
onValue: raw => setState(raw),
|
|
354
|
+
onMaskara: masked => setLabel(masked),
|
|
194
355
|
})
|
|
195
|
-
off()
|
|
356
|
+
off()
|
|
196
357
|
|
|
197
358
|
// React
|
|
198
359
|
useEffect(() => {
|
|
199
|
-
return
|
|
200
|
-
onValue:
|
|
360
|
+
return maskara.on(ref.current, 'date', {
|
|
361
|
+
onValue: date => setValue(date), // Date | null
|
|
201
362
|
})
|
|
202
363
|
}, [])
|
|
203
364
|
|
|
204
365
|
// Vue
|
|
205
366
|
onMounted(() => {
|
|
206
|
-
|
|
207
|
-
onValue:
|
|
367
|
+
maskara.on(inputRef.value, 'phone', {
|
|
368
|
+
onValue: value => emit('update:modelValue', value),
|
|
208
369
|
})
|
|
209
370
|
})
|
|
210
371
|
|
|
211
372
|
// Svelte action
|
|
212
373
|
function maskAction(node, pattern) {
|
|
213
|
-
const off =
|
|
374
|
+
const off = maskara.on(node, pattern)
|
|
214
375
|
return { destroy: off }
|
|
215
376
|
}
|
|
216
377
|
```
|
|
217
378
|
|
|
218
|
-
##
|
|
379
|
+
## `maskara.create`: isolated instances
|
|
219
380
|
|
|
220
381
|
```js
|
|
221
|
-
export const
|
|
382
|
+
export const maskaraBR = maskara.create({
|
|
222
383
|
cpf: { pattern: '###[.]###[.]###[-]##' },
|
|
223
384
|
cnpj: { pattern: '##[.]###[.]###[/]####[-]##' },
|
|
224
385
|
phone: { pattern: ['[(]##[)] ####[-]####', '[(]##[)] #####[-]####'] },
|
|
@@ -231,53 +392,78 @@ export const maskBR = mask.create({
|
|
|
231
392
|
return isNaN(dt) ? null : dt
|
|
232
393
|
},
|
|
233
394
|
},
|
|
234
|
-
money: {
|
|
235
|
-
pattern: '########[,]##',
|
|
236
|
-
transform: raw => parseInt(raw || '0', 10) / 100,
|
|
237
|
-
},
|
|
238
395
|
})
|
|
239
396
|
|
|
240
|
-
export const
|
|
397
|
+
export const maskaraUS = maskara.create({
|
|
241
398
|
ssn: { pattern: '###[-]##[-]####' },
|
|
242
399
|
zip: { pattern: '#####[-]####' },
|
|
243
400
|
phone: { pattern: '[(]###[)] ###[-]####' },
|
|
244
401
|
})
|
|
245
402
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
403
|
+
maskaraBR('cpf', '12345678909') // -> '123.456.789-09'
|
|
404
|
+
maskaraBR.raw('date', '01/01/2025') // -> Date
|
|
405
|
+
maskaraBR.names() // -> ['cpf', 'cnpj', 'phone', 'cep', 'date']
|
|
406
|
+
maskaraUS.names() // -> ['ssn', 'zip', 'phone']
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Official Brazilian presets
|
|
410
|
+
|
|
411
|
+
If you want common Brazilian masks ready to use, import `maskarajs/presets/br` and create an isolated engine from it.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
import maskara from 'maskarajs'
|
|
415
|
+
import { br, type BrazilPresetRegistry } from 'maskarajs/presets/br'
|
|
416
|
+
|
|
417
|
+
const maskaraBR = maskara.create<BrazilPresetRegistry>(br)
|
|
418
|
+
|
|
419
|
+
maskaraBR('cpf', '12345678909') // -> '123.456.789-09'
|
|
420
|
+
maskaraBR('cnpj', '11222333000181') // -> '11.222.333/0001-81'
|
|
421
|
+
maskaraBR('phone', '11987654321') // -> '(11) 98765-4321'
|
|
422
|
+
maskaraBR.raw('cep', '01310-930') // -> '01310930'
|
|
423
|
+
maskaraBR.raw('date', '01/12/2025') // -> Date
|
|
424
|
+
maskaraBR.raw('money', '1299,90') // -> 1299.9
|
|
251
425
|
```
|
|
252
426
|
|
|
253
|
-
|
|
427
|
+
The preset includes:
|
|
428
|
+
|
|
429
|
+
| Name | Pattern / behavior |
|
|
430
|
+
|---|---|
|
|
431
|
+
| `cpf` | `000.000.000-00` |
|
|
432
|
+
| `cnpj` | `00.000.000/0000-00` |
|
|
433
|
+
| `cep` | `00000-000`, returns `null` until complete |
|
|
434
|
+
| `phone` | landline or mobile phone |
|
|
435
|
+
| `date` | `DD/MM/YYYY`, rejects invalid months and returns `Date \| null` |
|
|
436
|
+
| `month` | accepts only `01` to `12` |
|
|
437
|
+
| `money` | decimal money value from cents |
|
|
438
|
+
|
|
439
|
+
## `rawLength` and `patternLength`
|
|
254
440
|
|
|
255
441
|
```js
|
|
256
|
-
const filled =
|
|
257
|
-
const total
|
|
442
|
+
const filled = maskara.rawLength('cpf', value)
|
|
443
|
+
const total = maskara.patternLength('cpf')
|
|
258
444
|
|
|
259
|
-
const pct
|
|
260
|
-
const ready
|
|
261
|
-
const label
|
|
445
|
+
const pct = maskara('cpf', value).length / total * 100
|
|
446
|
+
const ready = maskara.is('cpf', value)
|
|
447
|
+
const label = `${filled} raw chars`
|
|
262
448
|
```
|
|
263
449
|
|
|
264
|
-
`patternLength`
|
|
450
|
+
`patternLength` counts the final formatted length, including literals:
|
|
265
451
|
|
|
266
452
|
```js
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
453
|
+
maskara.patternLength('##[/]##[/]####') // -> 10
|
|
454
|
+
maskara.patternLength('###[.]###[.]###[-]##') // -> 14
|
|
455
|
+
maskara.patternLength('{4}### #### #### ####') // -> 19
|
|
270
456
|
```
|
|
271
457
|
|
|
272
|
-
##
|
|
458
|
+
## Visual showcase
|
|
273
459
|
|
|
274
|
-
|
|
460
|
+
The `maskforge-showcase` project demonstrates the library with:
|
|
275
461
|
|
|
276
|
-
- playground
|
|
277
|
-
-
|
|
278
|
-
-
|
|
279
|
-
-
|
|
280
|
-
-
|
|
462
|
+
- a playground that applies the mask while the user types;
|
|
463
|
+
- a visual pattern map with slot, literal and expression blocks;
|
|
464
|
+
- examples for React, Vue and Vanilla JavaScript;
|
|
465
|
+
- interactive recipes for `validate`, `define`, `defineSlot` and `create`;
|
|
466
|
+
- local benchmark notes.
|
|
281
467
|
|
|
282
468
|
```bash
|
|
283
469
|
cd maskforge-showcase
|
|
@@ -285,54 +471,121 @@ npm install
|
|
|
285
471
|
npm run dev
|
|
286
472
|
```
|
|
287
473
|
|
|
288
|
-
##
|
|
474
|
+
## Local benchmark
|
|
289
475
|
|
|
290
|
-
|
|
291
|
-
Os valores variam por máquina, mas ajudam a orientar expectativas de uso em input.
|
|
476
|
+
Simple benchmark executed in Node/WSL with 200,000 iterations per case after warmup. Numbers vary by machine, but they help set expectations for input-level usage.
|
|
292
477
|
|
|
293
|
-
|
|
|
478
|
+
| Case | Result |
|
|
294
479
|
|---|---:|
|
|
295
|
-
| CPF format | 39
|
|
296
|
-
| Phone dynamic | 32
|
|
297
|
-
| Date validate | 64
|
|
298
|
-
| Raw extraction | 44
|
|
480
|
+
| CPF format | 39,705 ops/s |
|
|
481
|
+
| Phone dynamic | 32,455 ops/s |
|
|
482
|
+
| Date validate | 64,572 ops/s |
|
|
483
|
+
| Raw extraction | 44,729 ops/s |
|
|
299
484
|
|
|
300
485
|
```js
|
|
301
486
|
const iterations = 200000
|
|
302
487
|
for (let i = 0; i < iterations; i++) {
|
|
303
|
-
|
|
488
|
+
maskara('###[.]###[.]###[-]##', '12345678909')
|
|
304
489
|
}
|
|
305
490
|
```
|
|
306
491
|
|
|
307
|
-
## transform
|
|
492
|
+
## `transform` contract
|
|
308
493
|
|
|
309
494
|
```js
|
|
310
495
|
// transform(raw, masked, complete) => T
|
|
311
496
|
// validate(raw, masked, complete) => boolean
|
|
312
497
|
//
|
|
313
|
-
// raw
|
|
314
|
-
// masked
|
|
315
|
-
// complete
|
|
498
|
+
// raw -> input chars without literals
|
|
499
|
+
// masked -> formatted string
|
|
500
|
+
// complete -> true when every slot is filled
|
|
316
501
|
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
mask.define('money', {
|
|
322
|
-
pattern: '########[,]##',
|
|
323
|
-
transform: raw => parseInt(raw || '0', 10) / 100,
|
|
324
|
-
// retorna number sempre — mesmo parcial
|
|
325
|
-
})
|
|
502
|
+
// Without transform -> maskara.raw() always returns the raw string
|
|
503
|
+
// With transform -> maskara.raw() always returns whatever transform returns
|
|
504
|
+
```
|
|
326
505
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
506
|
+
## License
|
|
507
|
+
|
|
508
|
+
MIT
|
|
509
|
+
|
|
510
|
+
## React hook
|
|
511
|
+
|
|
512
|
+
`maskarajs/react` exports a small `useMaskara` hook. It lives in a separate entrypoint, so the core package stays framework-agnostic for users who do not use React.
|
|
513
|
+
|
|
514
|
+
```tsx
|
|
515
|
+
import { useMaskara } from 'maskarajs/react'
|
|
516
|
+
|
|
517
|
+
export function CPFField() {
|
|
518
|
+
const cpf = useMaskara('###[.]###[.]###[-]##')
|
|
519
|
+
|
|
520
|
+
return <input {...cpf.inputProps({ inputMode: 'numeric' })} />
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### React Hook Form
|
|
525
|
+
|
|
526
|
+
Use `onValue` to send the raw value to React Hook Form while the input keeps the masked value on screen.
|
|
527
|
+
|
|
528
|
+
```tsx
|
|
529
|
+
import { Controller, useForm } from 'react-hook-form'
|
|
530
|
+
import { useMaskara } from 'maskarajs/react'
|
|
531
|
+
|
|
532
|
+
type FormValues = {
|
|
533
|
+
cpf: string
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function CPFController({ field }) {
|
|
537
|
+
const cpf = useMaskara('###[.]###[.]###[-]##', {
|
|
538
|
+
value: field.value,
|
|
539
|
+
onValue: field.onChange,
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<input
|
|
544
|
+
{...cpf.inputProps({
|
|
545
|
+
name: field.name,
|
|
546
|
+
onBlur: field.onBlur,
|
|
547
|
+
ref: field.ref,
|
|
548
|
+
inputMode: 'numeric',
|
|
549
|
+
})}
|
|
550
|
+
/>
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function Form() {
|
|
555
|
+
const { control, handleSubmit } = useForm<FormValues>({
|
|
556
|
+
defaultValues: { cpf: '' },
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
return (
|
|
560
|
+
<form onSubmit={handleSubmit(console.log)}>
|
|
561
|
+
<Controller
|
|
562
|
+
name="cpf"
|
|
563
|
+
control={control}
|
|
564
|
+
render={({ field }) => <CPFController field={field} />}
|
|
565
|
+
/>
|
|
566
|
+
</form>
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Zod
|
|
572
|
+
|
|
573
|
+
Keep the form value raw and validate it normally.
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
import { z } from 'zod'
|
|
577
|
+
|
|
578
|
+
export const schema = z.object({
|
|
579
|
+
cpf: z.string().length(11, 'CPF must contain 11 digits'),
|
|
333
580
|
})
|
|
334
581
|
```
|
|
335
582
|
|
|
336
|
-
|
|
583
|
+
### Yup
|
|
337
584
|
|
|
338
|
-
|
|
585
|
+
```ts
|
|
586
|
+
import * as yup from 'yup'
|
|
587
|
+
|
|
588
|
+
export const schema = yup.object({
|
|
589
|
+
cpf: yup.string().length(11, 'CPF must contain 11 digits').required(),
|
|
590
|
+
})
|
|
591
|
+
```
|