maskarajs 1.0.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/LICENSE +21 -0
- package/README.md +338 -0
- package/mask.cjs.js +874 -0
- package/mask.d.ts +291 -0
- package/mask.js +874 -0
- package/mask.mjs +874 -0
- package/package.json +31 -0
package/mask.cjs.js
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mask.js — engine de máscaras com padrão declarativo
|
|
3
|
+
*
|
|
4
|
+
* Sintaxe do padrão:
|
|
5
|
+
* # → qualquer dígito (0–9)
|
|
6
|
+
* @ → qualquer letra (a–z, A–Z, acentuados)
|
|
7
|
+
* * → qualquer caractere
|
|
8
|
+
* [texto] → literal fixo (inserido/removido automaticamente)
|
|
9
|
+
* {expr} → slot livre: testa UM caractere contra a expressão
|
|
10
|
+
*
|
|
11
|
+
* Sintaxe de {expr} — três formas, resolvidas nesta ordem:
|
|
12
|
+
* 1. Regex explícita — contém \, [, ^, (, | → new RegExp(expr)
|
|
13
|
+
* {\d} → /\d/.test(ch)
|
|
14
|
+
* {[0-4]} → /[0-4]/.test(ch)
|
|
15
|
+
* {[^aeiou]} → qualquer consoante
|
|
16
|
+
* 2. Intervalo — exatamente "x-y" (3 chars com hífen no meio)
|
|
17
|
+
* {0-4} → ch >= '0' && ch <= '4'
|
|
18
|
+
* {a-f} → ch >= 'a' && ch <= 'f'
|
|
19
|
+
* 3. Conjunto literal — qualquer outra coisa
|
|
20
|
+
* {4} → só o char '4'
|
|
21
|
+
* {013} → '0', '1' ou '3'
|
|
22
|
+
* {SP} → 'S' ou 'P'
|
|
23
|
+
*
|
|
24
|
+
* Exemplos reais:
|
|
25
|
+
* CEP: '#####[-]###'
|
|
26
|
+
* Telefone: ['[(]##[)] ####[-]####', '[(]##[)] #####[-]####']
|
|
27
|
+
* CPF: '###[.]###[.]###[-]##'
|
|
28
|
+
* CNPJ: '##[.]###[.]###[/]####[-]##'
|
|
29
|
+
* Data: '##[/]##[/]####'
|
|
30
|
+
* Mês: '{0-1}#' + validate incremental para aceitar só 01–12
|
|
31
|
+
* Cartão: '#### #### #### ####'
|
|
32
|
+
* Visa: '{4}### #### #### ####'
|
|
33
|
+
* Mês válido: '{0-1}{0-9}[/]{0-3}{0-9}[/]####'
|
|
34
|
+
* Hex color: '{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}'
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// ─── Cache do parser ───────────────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// parse() compila regex e aloca tokens a cada chamada.
|
|
40
|
+
// Como o mesmo padrão é usado em extractInputChars + applyTokens + inputCount
|
|
41
|
+
// a cada keystroke, cachear elimina recompilações redundantes.
|
|
42
|
+
//
|
|
43
|
+
// Chave: string do padrão → Valor: array de tokens imutáveis
|
|
44
|
+
|
|
45
|
+
const _parseCache = new Map()
|
|
46
|
+
|
|
47
|
+
// ─── Parser de padrão ──────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve {expr} para um predicado de teste (ch: string) => boolean.
|
|
51
|
+
*
|
|
52
|
+
* Ordem de resolução:
|
|
53
|
+
* 1. Regex explícita — expr contém \ [ ^ ( |
|
|
54
|
+
* 2. Intervalo — exatamente "x-y"
|
|
55
|
+
* 3. Conjunto literal
|
|
56
|
+
*/
|
|
57
|
+
function resolveExpr(expr) {
|
|
58
|
+
const isRegex = /[\\[^(|]/.test(expr) || expr.startsWith('\\')
|
|
59
|
+
if (isRegex) {
|
|
60
|
+
let re
|
|
61
|
+
try { re = new RegExp(expr) } catch (e) {
|
|
62
|
+
throw new Error(`mask: expressão inválida "{${expr}}" — ${e.message}`)
|
|
63
|
+
}
|
|
64
|
+
return { test: ch => re.test(ch), constraint: expr }
|
|
65
|
+
}
|
|
66
|
+
if (expr.length === 3 && expr[1] === '-') {
|
|
67
|
+
const lo = expr[0], hi = expr[2]
|
|
68
|
+
return { test: ch => ch >= lo && ch <= hi, constraint: expr }
|
|
69
|
+
}
|
|
70
|
+
return { test: ch => expr.includes(ch), constraint: expr }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Predicados padrão para os tokens base sem modificador */
|
|
74
|
+
const DEFAULT_SLOTS = Object.freeze({
|
|
75
|
+
'#': { test: ch => /\d/.test(ch), hint: '0' },
|
|
76
|
+
'@': { test: ch => /[A-Za-zÀ-ÿ]/.test(ch), hint: 'A' },
|
|
77
|
+
'*': { test: () => true, hint: '_' },
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const globalSlots = { ...DEFAULT_SLOTS }
|
|
81
|
+
let globalSlotsVersion = 0
|
|
82
|
+
let slotLanguageSeq = 0
|
|
83
|
+
|
|
84
|
+
function normalizeSlot(symbol, definition) {
|
|
85
|
+
if (typeof symbol !== 'string' || Array.from(symbol).length !== 1) {
|
|
86
|
+
throw new Error('mask.defineSlot: o símbolo precisa ter exatamente 1 caractere')
|
|
87
|
+
}
|
|
88
|
+
if (symbol === '[' || symbol === ']' || symbol === '{' || symbol === '}') {
|
|
89
|
+
throw new Error(`mask.defineSlot: "${symbol}" é reservado pela sintaxe de pattern`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof definition === 'function') {
|
|
93
|
+
return { test: definition, hint: symbol }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (definition instanceof RegExp) {
|
|
97
|
+
const flags = definition.flags.replace(/[gy]/g, '')
|
|
98
|
+
const re = new RegExp(definition.source, flags)
|
|
99
|
+
return { test: ch => re.test(ch), hint: symbol }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!definition || typeof definition.test !== 'function') {
|
|
103
|
+
throw new Error(`mask.defineSlot: "${symbol}" precisa de uma função, RegExp ou { test, hint }`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
test: definition.test,
|
|
108
|
+
hint: typeof definition.hint === 'string' && definition.hint ? Array.from(definition.hint)[0] : symbol,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function defineSlot(slots, symbol, definition) {
|
|
113
|
+
slots[symbol] = normalizeSlot(symbol, definition)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Converte um padrão string em tokens — resultado cacheado por padrão.
|
|
118
|
+
*
|
|
119
|
+
* Token sem modificador:
|
|
120
|
+
* '#' → { type:'input', base:'#', test: /\d/.test }
|
|
121
|
+
*
|
|
122
|
+
* Token {expr}:
|
|
123
|
+
* '{0-4}' → { type:'input', base:'{expr}', test: ch>=0&&ch<=4, constraint:'0-4' }
|
|
124
|
+
* '{[^0]}'→ { type:'input', base:'{expr}', test: /[^0]/.test, constraint:'[^0]' }
|
|
125
|
+
*/
|
|
126
|
+
function parse(pattern, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
|
|
127
|
+
const cacheKey = `${cacheId}:${slotsVersion}:${pattern}`
|
|
128
|
+
if (_parseCache.has(cacheKey)) return _parseCache.get(cacheKey)
|
|
129
|
+
|
|
130
|
+
const tokens = []
|
|
131
|
+
let i = 0
|
|
132
|
+
|
|
133
|
+
while (i < pattern.length) {
|
|
134
|
+
const ch = pattern[i]
|
|
135
|
+
|
|
136
|
+
// ── literal fixo [texto] ──────────────────────────────────────────────
|
|
137
|
+
if (ch === '[') {
|
|
138
|
+
const close = pattern.indexOf(']', i)
|
|
139
|
+
if (close === -1) throw new Error(`mask: colchete não fechado em "${pattern}"`)
|
|
140
|
+
tokens.push({ type: 'literal', value: pattern.slice(i + 1, close) })
|
|
141
|
+
i = close + 1
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── slot livre {expr} ─────────────────────────────────────────────────
|
|
146
|
+
if (ch === '{') {
|
|
147
|
+
const close = pattern.indexOf('}', i)
|
|
148
|
+
if (close === -1) throw new Error(`mask: chave não fechada em "${pattern}"`)
|
|
149
|
+
const expr = pattern.slice(i + 1, close)
|
|
150
|
+
if (!expr) throw new Error(`mask: expressão vazia em "${pattern}"`)
|
|
151
|
+
const { test, constraint } = resolveExpr(expr)
|
|
152
|
+
tokens.push({ type: 'input', base: '{expr}', test, constraint })
|
|
153
|
+
i = close + 1
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── tokens de input configuráveis ─────────────────────────────────────
|
|
158
|
+
if (slots[ch]) {
|
|
159
|
+
tokens.push({ type: 'input', base: ch, ...slots[ch] })
|
|
160
|
+
i++
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── literal implícito ─────────────────────────────────────────────────
|
|
165
|
+
tokens.push({ type: 'literal', value: ch })
|
|
166
|
+
i++
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// congela os tokens para evitar mutação acidental no cache
|
|
170
|
+
Object.freeze(tokens)
|
|
171
|
+
_parseCache.set(cacheKey, tokens)
|
|
172
|
+
return tokens
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Quantos slots de input um padrão tem */
|
|
176
|
+
function inputCount(pattern, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
|
|
177
|
+
return parse(pattern, slots, cacheId, slotsVersion).filter(t => t.type === 'input').length
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Engine de aplicação ───────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Aplica tokens a chars de input já filtrados e validados.
|
|
184
|
+
* Retorna o valor mascarado, sem literais pendurados no final.
|
|
185
|
+
*/
|
|
186
|
+
function applyTokens(tokens, inputChars) {
|
|
187
|
+
let masked = ''
|
|
188
|
+
let ci = 0
|
|
189
|
+
|
|
190
|
+
for (const token of tokens) {
|
|
191
|
+
if (ci >= inputChars.length) break
|
|
192
|
+
|
|
193
|
+
if (token.type === 'literal') {
|
|
194
|
+
masked += token.value
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (token.test(inputChars[ci])) {
|
|
199
|
+
masked += inputChars[ci]
|
|
200
|
+
ci++
|
|
201
|
+
} else {
|
|
202
|
+
break
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// remove literais pendurados no final sem input depois deles
|
|
207
|
+
let trimmed = masked
|
|
208
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
209
|
+
const t = tokens[i]
|
|
210
|
+
if (t.type === 'literal') {
|
|
211
|
+
if (trimmed.endsWith(t.value)) trimmed = trimmed.slice(0, -t.value.length)
|
|
212
|
+
} else {
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return trimmed
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── Seleção de padrão dinâmico ────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Dado um array de padrões e os chars de input,
|
|
224
|
+
* escolhe o menor padrão que ainda comporta todos os chars.
|
|
225
|
+
* Se nenhum comporta, usa o maior.
|
|
226
|
+
*/
|
|
227
|
+
function selectPattern(patterns, chars, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
|
|
228
|
+
const sorted = [...patterns].sort((a, b) => inputCount(a, slots, cacheId, slotsVersion) - inputCount(b, slots, cacheId, slotsVersion))
|
|
229
|
+
for (const p of sorted) {
|
|
230
|
+
if (inputCount(p, slots, cacheId, slotsVersion) >= chars.length) return p
|
|
231
|
+
}
|
|
232
|
+
return sorted[sorted.length - 1]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Extração e validação de chars de input ────────────────────────────────
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extrai os chars de input de um valor (bruto ou mascarado),
|
|
239
|
+
* filtra literais, valida cada char contra o predicado do slot correspondente,
|
|
240
|
+
* e trunca ao limite do padrão escolhido.
|
|
241
|
+
*
|
|
242
|
+
* FIX: chars inválidos (ex: letras em campo #) são descartados antes de
|
|
243
|
+
* chegar ao applyTokens — garante raw correto mesmo em paste.
|
|
244
|
+
*/
|
|
245
|
+
function defaultValidate() {
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function fullLength(pattern, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
|
|
250
|
+
return parse(pattern, slots, cacheId, slotsVersion).reduce((total, token) => {
|
|
251
|
+
if (token.type === 'literal') return total + token.value.length
|
|
252
|
+
return total + 1
|
|
253
|
+
}, 0)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function extractInputChars(value, patterns, validate = defaultValidate, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
|
|
257
|
+
const patList = Array.isArray(patterns) ? patterns : [patterns]
|
|
258
|
+
|
|
259
|
+
// 1. coleta todos os literais para remover do valor
|
|
260
|
+
const allLiterals = new Set()
|
|
261
|
+
for (const p of patList) {
|
|
262
|
+
for (const t of parse(p, slots, cacheId, slotsVersion)) {
|
|
263
|
+
if (t.type === 'literal') {
|
|
264
|
+
for (const ch of t.value) allLiterals.add(ch)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 2. remove literais — chars candidatos a input
|
|
270
|
+
const candidates = Array.from(String(value)).filter(ch => !allLiterals.has(ch))
|
|
271
|
+
|
|
272
|
+
// 3. escolhe o padrão para estes candidatos (antes de validar)
|
|
273
|
+
const chosen = selectPattern(patList, candidates, slots, cacheId, slotsVersion)
|
|
274
|
+
const inputTokens = parse(chosen, slots, cacheId, slotsVersion).filter(t => t.type === 'input')
|
|
275
|
+
|
|
276
|
+
// 4. valida cada char contra o predicado do slot correspondente
|
|
277
|
+
// chars que não passam no teste são descartados (não apenas bloqueados)
|
|
278
|
+
// isso garante que paste com chars inválidos produza raw correto
|
|
279
|
+
const valid = []
|
|
280
|
+
let ti = 0
|
|
281
|
+
for (const ch of candidates) {
|
|
282
|
+
if (ti >= inputTokens.length) break
|
|
283
|
+
if (inputTokens[ti].test(ch)) {
|
|
284
|
+
const next = [...valid, ch]
|
|
285
|
+
const nextRaw = next.join('')
|
|
286
|
+
const nextMasked = applyTokens(parse(chosen, slots, cacheId, slotsVersion), next)
|
|
287
|
+
const complete = next.length >= inputCount(chosen, slots, cacheId, slotsVersion)
|
|
288
|
+
if (!validate(nextRaw, nextMasked, complete)) break
|
|
289
|
+
valid.push(ch)
|
|
290
|
+
ti++
|
|
291
|
+
}
|
|
292
|
+
// char inválido para este slot → descartado silenciosamente
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 5. trunca ao limite do padrão (fonte da verdade do tamanho máximo)
|
|
296
|
+
return valid.slice(0, inputCount(chosen, slots, cacheId, slotsVersion))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Registro de máscaras nomeadas ────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
const registry = new Map()
|
|
302
|
+
|
|
303
|
+
// ─── API pública ───────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* mask(pattern, value) — aplica máscara, retorna string formatada (display)
|
|
307
|
+
*
|
|
308
|
+
* @param {string | string[]} pattern padrão único, array dinâmico, ou nome registrado
|
|
309
|
+
* @param {string} value valor bruto ou já mascarado
|
|
310
|
+
* @returns {string}
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* mask('##[/]##[/]####', '01012025') // → '01/01/2025'
|
|
314
|
+
* mask(['[(]##[)] ####[-]####',
|
|
315
|
+
* '[(]##[)] #####[-]####'], '11987654321') // → '(11) 98765-4321'
|
|
316
|
+
* mask('date', '01012025') // → '01/01/2025' (nome registrado)
|
|
317
|
+
*/
|
|
318
|
+
function mask(pattern, value) {
|
|
319
|
+
if (value == null) return ''
|
|
320
|
+
let validate
|
|
321
|
+
if (typeof pattern === 'string' && registry.has(pattern)) {
|
|
322
|
+
const entry = registry.get(pattern)
|
|
323
|
+
validate = entry.validate
|
|
324
|
+
pattern = entry.pattern
|
|
325
|
+
}
|
|
326
|
+
const str = String(value)
|
|
327
|
+
const patterns = Array.isArray(pattern) ? pattern : [pattern]
|
|
328
|
+
const chars = extractInputChars(str, patterns, validate, globalSlots, 'global', globalSlotsVersion)
|
|
329
|
+
const chosen = selectPattern(patterns, chars, globalSlots, 'global', globalSlotsVersion)
|
|
330
|
+
return applyTokens(parse(chosen, globalSlots, 'global', globalSlotsVersion), chars)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* mask.raw(pattern, value) — devolve o raw passado pelo transform
|
|
335
|
+
*
|
|
336
|
+
* O raw É o transform. Não são dois valores — são um só.
|
|
337
|
+
*
|
|
338
|
+
* Sem transform: devolve a string crua (chars sem literais), sempre.
|
|
339
|
+
* Com transform: devolve exatamente o que transform retornar —
|
|
340
|
+
* parcial ou completo. O transform recebe (raw, masked, complete).
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* mask.raw('##[/]##[/]####', '01/01/2025') // → '01012025'
|
|
344
|
+
* mask.raw('date', '01/01/2025') // → Date(2025-01-01) (com transform)
|
|
345
|
+
* mask.raw('date', '01/01') // → null (transform decidiu)
|
|
346
|
+
*/
|
|
347
|
+
mask.raw = function (pattern, value) {
|
|
348
|
+
if (value == null) return ''
|
|
349
|
+
|
|
350
|
+
let transform
|
|
351
|
+
let validate
|
|
352
|
+
if (typeof pattern === 'string' && registry.has(pattern)) {
|
|
353
|
+
const entry = registry.get(pattern)
|
|
354
|
+
transform = entry.transform
|
|
355
|
+
validate = entry.validate
|
|
356
|
+
pattern = entry.pattern
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const patterns = Array.isArray(pattern) ? pattern : [pattern]
|
|
360
|
+
const raw = extractInputChars(String(value), patterns, validate, globalSlots, 'global', globalSlotsVersion).join('')
|
|
361
|
+
|
|
362
|
+
if (!transform) return raw
|
|
363
|
+
|
|
364
|
+
const complete = patterns.some(p => raw.length >= inputCount(p, globalSlots, 'global', globalSlotsVersion))
|
|
365
|
+
return transform(raw, mask(pattern, value), complete)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* mask.is(pattern, value) — verifica se o valor preenche o padrão completamente
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* mask.is('##[/]##[/]####', '01/01/2025') // → true
|
|
373
|
+
* mask.is('##[/]##[/]####', '01/01') // → false
|
|
374
|
+
*/
|
|
375
|
+
mask.is = function (pattern, value) {
|
|
376
|
+
if (value == null) return false
|
|
377
|
+
let validate
|
|
378
|
+
if (typeof pattern === 'string' && registry.has(pattern)) {
|
|
379
|
+
const entry = registry.get(pattern)
|
|
380
|
+
validate = entry.validate
|
|
381
|
+
pattern = entry.pattern
|
|
382
|
+
}
|
|
383
|
+
const patterns = Array.isArray(pattern) ? pattern : [pattern]
|
|
384
|
+
const chars = extractInputChars(String(value), patterns, validate, globalSlots, 'global', globalSlotsVersion)
|
|
385
|
+
return patterns.some(p => chars.length >= inputCount(p, globalSlots, 'global', globalSlotsVersion))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* mask.hint(pattern) — placeholder legível para o campo
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* mask.hint('##[/]##[/]####') // → '00/00/0000'
|
|
393
|
+
* mask.hint('[(]##[)] #####[-]####') // → '(00) 00000-0000'
|
|
394
|
+
* mask.hint('{[0-4]}###') // → '0###' (primeiro char do range)
|
|
395
|
+
*/
|
|
396
|
+
mask.hint = function (pattern) {
|
|
397
|
+
if (typeof pattern === 'string' && registry.has(pattern)) {
|
|
398
|
+
pattern = registry.get(pattern).pattern
|
|
399
|
+
}
|
|
400
|
+
const p = Array.isArray(pattern) ? pattern[pattern.length - 1] : pattern
|
|
401
|
+
return parse(p, globalSlots, 'global', globalSlotsVersion)
|
|
402
|
+
.map(t => {
|
|
403
|
+
if (t.type === 'literal') return t.value
|
|
404
|
+
if (t.constraint) {
|
|
405
|
+
if (t.constraint.length === 3 && t.constraint[1] === '-') return t.constraint[0]
|
|
406
|
+
if (/[\\[^(|]/.test(t.constraint) || t.constraint.startsWith('\\')) return '_'
|
|
407
|
+
return t.constraint[0]
|
|
408
|
+
}
|
|
409
|
+
return t.hint || '_'
|
|
410
|
+
})
|
|
411
|
+
.join('')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* mask.rawLength(pattern, value) — comprimento do raw atual (chars de input preenchidos)
|
|
416
|
+
*
|
|
417
|
+
* Equivale a mask.raw(pattern, value).length mas sem passar pelo transform —
|
|
418
|
+
* sempre retorna um número, independente do transform definido.
|
|
419
|
+
*
|
|
420
|
+
* Útil para: progress bars, validação incremental, contadores de caracteres.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* mask.rawLength('##[/]##[/]####', '01/01') // → 4 (digitando)
|
|
424
|
+
* mask.rawLength('##[/]##[/]####', '01/01/2025') // → 8 (completo)
|
|
425
|
+
* mask.rawLength('date', '01/01/2025') // → 8 (funciona com nome registrado)
|
|
426
|
+
*
|
|
427
|
+
* // Progress bar:
|
|
428
|
+
* const pct = mask.rawLength('cpf', value) / mask.patternLength('cpf') * 100
|
|
429
|
+
*/
|
|
430
|
+
mask.rawLength = function (pattern, value) {
|
|
431
|
+
if (value == null) return 0
|
|
432
|
+
let p = pattern
|
|
433
|
+
let validate
|
|
434
|
+
if (typeof p === 'string' && registry.has(p)) {
|
|
435
|
+
const entry = registry.get(p)
|
|
436
|
+
validate = entry.validate
|
|
437
|
+
p = entry.pattern
|
|
438
|
+
}
|
|
439
|
+
const patterns = Array.isArray(p) ? p : [p]
|
|
440
|
+
// Aplica a máscara e conta os chars do resultado — fonte da verdade
|
|
441
|
+
// é o valor mascarado, não o valor bruto (que pode ter chars não validados)
|
|
442
|
+
const masked = mask(p, String(value))
|
|
443
|
+
return extractInputChars(masked, patterns, validate, globalSlots, 'global', globalSlotsVersion).length
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* mask.patternLength(pattern) — comprimento total do valor mascarado completo
|
|
448
|
+
*
|
|
449
|
+
* Conta slots de input como 1 caractere e literais pelo seu tamanho real.
|
|
450
|
+
* Para padrões dinâmicos (array), retorna o maior comprimento mascarado.
|
|
451
|
+
*
|
|
452
|
+
* Útil para: limites de caracteres, progress bars, pré-alocação de buffers.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* mask.patternLength('##[/]##[/]####') // → 10
|
|
456
|
+
* mask.patternLength('###[.]###[.]###[-]##') // → 14 (CPF)
|
|
457
|
+
* mask.patternLength(['[(]##[)] ####[-]####',
|
|
458
|
+
* '[(]##[)] #####[-]####']) // → 15 (maior padrão)
|
|
459
|
+
* mask.patternLength('date') // → 10 (nome registrado)
|
|
460
|
+
*/
|
|
461
|
+
mask.patternLength = function (pattern) {
|
|
462
|
+
if (typeof pattern === 'string' && registry.has(pattern)) {
|
|
463
|
+
pattern = registry.get(pattern).pattern
|
|
464
|
+
}
|
|
465
|
+
const patterns = Array.isArray(pattern) ? pattern : [pattern]
|
|
466
|
+
return Math.max(...patterns.map(p => fullLength(p, globalSlots, 'global', globalSlotsVersion)))
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* mask.format(pattern, rawValue) — formata um valor vindo da API (sem máscara)
|
|
471
|
+
*
|
|
472
|
+
* Alias semântico de mask() — deixa clara a intenção de "formatar para exibição"
|
|
473
|
+
* versus "aplicar máscara enquanto o usuário digita".
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* mask.format('cpf', user.cpf) // → '123.456.789-09'
|
|
477
|
+
* mask.format('phone', contact.phone) // → '(11) 98765-4321'
|
|
478
|
+
* mask.format('date', '01012025') // → '01/01/2025'
|
|
479
|
+
*/
|
|
480
|
+
mask.format = function (pattern, value) {
|
|
481
|
+
return mask(pattern, value)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* mask.define(name, definition) — registra uma máscara nomeada
|
|
486
|
+
*
|
|
487
|
+
* @param {string} name
|
|
488
|
+
* @param {{ pattern: string | string[], transform?: (raw, masked, complete) => any, validate?: (raw, masked, complete) => boolean }} definition
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* mask.define('date', {
|
|
492
|
+
* pattern: '##[/]##[/]####',
|
|
493
|
+
* transform: (raw, masked, complete) => {
|
|
494
|
+
* if (!complete) return null
|
|
495
|
+
* const dt = new Date(`${raw.slice(4,8)}-${raw.slice(2,4)}-${raw.slice(0,2)}`)
|
|
496
|
+
* return isNaN(dt) ? null : dt
|
|
497
|
+
* },
|
|
498
|
+
* })
|
|
499
|
+
*
|
|
500
|
+
* mask.define('money', {
|
|
501
|
+
* pattern: '########[,]##',
|
|
502
|
+
* transform: raw => parseInt(raw || '0', 10) / 100,
|
|
503
|
+
* })
|
|
504
|
+
*
|
|
505
|
+
* mask.define('month', {
|
|
506
|
+
* pattern: '{0-1}#',
|
|
507
|
+
* validate: (raw, masked, complete) => !complete || Number(raw) >= 1 && Number(raw) <= 12,
|
|
508
|
+
* })
|
|
509
|
+
*/
|
|
510
|
+
mask.define = function (name, definition) {
|
|
511
|
+
if (!definition?.pattern) throw new Error(`mask.define: "${name}" precisa de um pattern`)
|
|
512
|
+
if (definition.validate && typeof definition.validate !== 'function') {
|
|
513
|
+
throw new Error(`mask.define: "${name}" validate precisa ser uma função`)
|
|
514
|
+
}
|
|
515
|
+
registry.set(name, definition)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* mask.undefine(name) — remove uma máscara do registro
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* mask.undefine('date')
|
|
523
|
+
* mask.is('date', '01/01/2025') // throws — 'date' não existe mais
|
|
524
|
+
*/
|
|
525
|
+
mask.undefine = function (name) {
|
|
526
|
+
registry.delete(name)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* mask.names() — lista todas as máscaras registradas
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* mask.names() // → ['cpf', 'phone', 'date', 'money', ...]
|
|
534
|
+
*/
|
|
535
|
+
mask.names = function () {
|
|
536
|
+
return Array.from(registry.keys())
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* mask.defineSlot(symbol, definition) — cria ou sobrescreve um token de input.
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* mask.defineSlot('N', { test: ch => /\d/.test(ch), hint: '0' })
|
|
544
|
+
* mask('NNN[-]NN', '12345') // → '123-45'
|
|
545
|
+
*/
|
|
546
|
+
mask.defineSlot = function (symbol, definition) {
|
|
547
|
+
defineSlot(globalSlots, symbol, definition)
|
|
548
|
+
globalSlotsVersion++
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* mask.undefineSlot(symbol) — remove um token customizado.
|
|
553
|
+
* Tokens padrão (#, @, *) voltam ao comportamento original.
|
|
554
|
+
*/
|
|
555
|
+
mask.undefineSlot = function (symbol) {
|
|
556
|
+
if (DEFAULT_SLOTS[symbol]) globalSlots[symbol] = DEFAULT_SLOTS[symbol]
|
|
557
|
+
else delete globalSlots[symbol]
|
|
558
|
+
globalSlotsVersion++
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* mask.slots() — lista os símbolos disponíveis na linguagem atual.
|
|
563
|
+
*/
|
|
564
|
+
mask.slots = function () {
|
|
565
|
+
return Object.keys(globalSlots)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* mask.on(input, pattern, options?) — vincula máscara a um input DOM
|
|
570
|
+
*
|
|
571
|
+
* Framework-agnostic. Retorna função de cleanup.
|
|
572
|
+
*
|
|
573
|
+
* options:
|
|
574
|
+
* onValue(raw) — valor limpo / transformado a cada mudança
|
|
575
|
+
* onMasked(v) — valor mascarado a cada mudança
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* // Vanilla
|
|
579
|
+
* const off = mask.on(el, 'cpf', { onValue: v => setState(v) })
|
|
580
|
+
* off() // remove listeners
|
|
581
|
+
*
|
|
582
|
+
* // React
|
|
583
|
+
* useEffect(() => mask.on(ref.current, 'date', {
|
|
584
|
+
* onValue: date => setValue(date), // recebe Date | null
|
|
585
|
+
* }), [])
|
|
586
|
+
*
|
|
587
|
+
* // Vue
|
|
588
|
+
* onMounted(() => mask.on(inputRef.value, 'phone', {
|
|
589
|
+
* onValue: v => emit('update:modelValue', v),
|
|
590
|
+
* }))
|
|
591
|
+
*/
|
|
592
|
+
mask.on = function (input, pattern, options = {}) {
|
|
593
|
+
const { onValue, onMasked } = options
|
|
594
|
+
|
|
595
|
+
function resolvePatterns() {
|
|
596
|
+
let p = pattern
|
|
597
|
+
if (typeof p === 'string' && registry.has(p)) p = registry.get(p).pattern
|
|
598
|
+
return Array.isArray(p) ? p : [p]
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Bloqueia keydown quando já no limite máximo —
|
|
602
|
+
// evita o flash de um frame com char excedente antes do handler corrigir
|
|
603
|
+
function onKeydown(e) {
|
|
604
|
+
if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return
|
|
605
|
+
const patterns = resolvePatterns()
|
|
606
|
+
let validate
|
|
607
|
+
if (typeof pattern === 'string' && registry.has(pattern)) validate = registry.get(pattern).validate
|
|
608
|
+
const chars = extractInputChars(e.target.value, patterns, validate, globalSlots, 'global', globalSlotsVersion)
|
|
609
|
+
const maxLimit = Math.max(...patterns.map(p => inputCount(p, globalSlots, 'global', globalSlotsVersion)))
|
|
610
|
+
if (chars.length >= maxLimit) e.preventDefault()
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Handler de input: aplica máscara, preserva cursor, dispara callbacks
|
|
614
|
+
function handler(e) {
|
|
615
|
+
const el = e.target
|
|
616
|
+
const raw = el.value
|
|
617
|
+
const masked = mask(pattern, raw)
|
|
618
|
+
const cursor = el.selectionStart ?? masked.length
|
|
619
|
+
const diff = masked.length - raw.length
|
|
620
|
+
|
|
621
|
+
el.value = masked
|
|
622
|
+
|
|
623
|
+
requestAnimationFrame(() => {
|
|
624
|
+
const pos = Math.max(0, cursor + diff)
|
|
625
|
+
el.setSelectionRange(pos, pos)
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
onMasked?.(masked)
|
|
629
|
+
onValue?.(mask.raw(pattern, masked))
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
input.addEventListener('keydown', onKeydown)
|
|
633
|
+
input.addEventListener('input', handler)
|
|
634
|
+
|
|
635
|
+
return () => {
|
|
636
|
+
input.removeEventListener('keydown', onKeydown)
|
|
637
|
+
input.removeEventListener('input', handler)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ─── Instância isolada ────────────────────────────────────────────────────
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Cria uma instância independente do engine com registry próprio
|
|
645
|
+
* e presets opcionais — sem compartilhar estado com a instância global.
|
|
646
|
+
*
|
|
647
|
+
* Útil para:
|
|
648
|
+
* - Múltiplos contextos num mesmo projeto (BR + US, admin + cliente)
|
|
649
|
+
* - Bibliotecas que não querem poluir o registry global
|
|
650
|
+
* - Testes com estado isolado
|
|
651
|
+
* - Presets reutilizáveis distribuídos como pacote
|
|
652
|
+
*
|
|
653
|
+
* @param {Record<string, MaskDefinition>} presets máscaras pré-configuradas
|
|
654
|
+
* @returns instância com a mesma API de mask, mas registry isolado
|
|
655
|
+
*
|
|
656
|
+
* @example
|
|
657
|
+
* // Instância com presets brasileiros
|
|
658
|
+
* export const maskBR = mask.create({
|
|
659
|
+
* cpf: { pattern: '###[.]###[.]###[-]##' },
|
|
660
|
+
* cnpj: { pattern: '##[.]###[.]###[/]####[-]##' },
|
|
661
|
+
* phone: { pattern: ['[(]##[)] ####[-]####', '[(]##[)] #####[-]####'] },
|
|
662
|
+
* cep: { pattern: '#####[-]###', transform: (r,m,c) => c ? r : null },
|
|
663
|
+
* date: {
|
|
664
|
+
* pattern: '##[/]##[/]####',
|
|
665
|
+
* transform: (raw, masked, complete) => {
|
|
666
|
+
* if (!complete) return null
|
|
667
|
+
* const dt = new Date(`${raw.slice(4,8)}-${raw.slice(2,4)}-${raw.slice(0,2)}T12:00:00`)
|
|
668
|
+
* return isNaN(dt) ? null : dt
|
|
669
|
+
* },
|
|
670
|
+
* },
|
|
671
|
+
* money: {
|
|
672
|
+
* pattern: '########[,]##',
|
|
673
|
+
* transform: raw => parseInt(raw || '0', 10) / 100,
|
|
674
|
+
* },
|
|
675
|
+
* })
|
|
676
|
+
*
|
|
677
|
+
* // Instância com presets americanos
|
|
678
|
+
* export const maskUS = mask.create({
|
|
679
|
+
* ssn: { pattern: '###[-]##[-]####' },
|
|
680
|
+
* zip: { pattern: '#####[-]####' },
|
|
681
|
+
* phone: { pattern: '({0-9}{0-9}{0-9}) ###[-]####' },
|
|
682
|
+
* date: { pattern: '##{/}##[/]####' },
|
|
683
|
+
* })
|
|
684
|
+
*
|
|
685
|
+
* // Uso — mesma API, registry isolado
|
|
686
|
+
* maskBR('cpf', '12345678909') // → '123.456.789-09'
|
|
687
|
+
* maskBR.raw('date', '01/01/2025') // → Date(2025-01-01)
|
|
688
|
+
* maskBR.is('phone', '(11) 98765-4321')// → true
|
|
689
|
+
* maskBR.on(el, 'cep', { onValue: v => setCep(v) })
|
|
690
|
+
*
|
|
691
|
+
* // Registros da instância não afetam a global nem outras instâncias
|
|
692
|
+
* maskBR.define('rg', { pattern: '##[.]###[.]###[-]#' })
|
|
693
|
+
* maskUS.names() // → ['ssn', 'zip', 'phone', 'date'] (sem 'rg')
|
|
694
|
+
* mask.names() // → [] (global intocada)
|
|
695
|
+
*/
|
|
696
|
+
mask.create = function (presets = {}) {
|
|
697
|
+
// registry isolado para esta instância
|
|
698
|
+
const localRegistry = new Map()
|
|
699
|
+
const localSlots = { ...globalSlots }
|
|
700
|
+
const localCacheId = `local-${++slotLanguageSeq}`
|
|
701
|
+
let localSlotsVersion = globalSlotsVersion
|
|
702
|
+
|
|
703
|
+
// registra presets iniciais
|
|
704
|
+
for (const [name, def] of Object.entries(presets)) {
|
|
705
|
+
if (!def?.pattern) throw new Error(`mask.create: "${name}" precisa de um pattern`)
|
|
706
|
+
localRegistry.set(name, def)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// resolve pattern de um nome neste registry local
|
|
710
|
+
function resolveLocal(pattern) {
|
|
711
|
+
if (typeof pattern === 'string' && localRegistry.has(pattern)) {
|
|
712
|
+
return localRegistry.get(pattern).pattern
|
|
713
|
+
}
|
|
714
|
+
return pattern
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function resolveLocalEntry(pattern) {
|
|
718
|
+
if (typeof pattern === 'string' && localRegistry.has(pattern)) {
|
|
719
|
+
return localRegistry.get(pattern)
|
|
720
|
+
}
|
|
721
|
+
return undefined
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ── API da instância — espelha mask.* usando o registry local ─────────
|
|
725
|
+
|
|
726
|
+
function instance(pattern, value) {
|
|
727
|
+
if (value == null) return ''
|
|
728
|
+
const p = resolveLocal(pattern)
|
|
729
|
+
const pats = Array.isArray(p) ? p : [p]
|
|
730
|
+
const chars = extractInputChars(String(value), pats, resolveLocalEntry(pattern)?.validate, localSlots, localCacheId, localSlotsVersion)
|
|
731
|
+
const chosen = selectPattern(pats, chars, localSlots, localCacheId, localSlotsVersion)
|
|
732
|
+
return applyTokens(parse(chosen, localSlots, localCacheId, localSlotsVersion), chars)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
instance.raw = function (pattern, value) {
|
|
736
|
+
if (value == null) return ''
|
|
737
|
+
let transform
|
|
738
|
+
let validate
|
|
739
|
+
if (typeof pattern === 'string' && localRegistry.has(pattern)) {
|
|
740
|
+
const entry = localRegistry.get(pattern)
|
|
741
|
+
transform = entry.transform
|
|
742
|
+
validate = entry.validate
|
|
743
|
+
pattern = entry.pattern
|
|
744
|
+
}
|
|
745
|
+
const pats = Array.isArray(pattern) ? pattern : [pattern]
|
|
746
|
+
const raw = extractInputChars(String(value), pats, validate, localSlots, localCacheId, localSlotsVersion).join('')
|
|
747
|
+
if (!transform) return raw
|
|
748
|
+
const complete = pats.some(p => raw.length >= inputCount(p, localSlots, localCacheId, localSlotsVersion))
|
|
749
|
+
return transform(raw, instance(pattern, value), complete)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
instance.is = function (pattern, value) {
|
|
753
|
+
if (value == null) return false
|
|
754
|
+
const p = resolveLocal(pattern)
|
|
755
|
+
const pats = Array.isArray(p) ? p : [p]
|
|
756
|
+
const chars = extractInputChars(String(value), pats, resolveLocalEntry(pattern)?.validate, localSlots, localCacheId, localSlotsVersion)
|
|
757
|
+
return pats.some(pt => chars.length >= inputCount(pt, localSlots, localCacheId, localSlotsVersion))
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
instance.hint = function (pattern) {
|
|
761
|
+
const p = resolveLocal(pattern)
|
|
762
|
+
const pat = Array.isArray(p) ? p[p.length - 1] : p
|
|
763
|
+
return parse(pat, localSlots, localCacheId, localSlotsVersion).map(t => {
|
|
764
|
+
if (t.type === 'literal') return t.value
|
|
765
|
+
if (t.constraint) {
|
|
766
|
+
if (t.constraint.length === 3 && t.constraint[1] === '-') return t.constraint[0]
|
|
767
|
+
if (/[\\[^(|]/.test(t.constraint) || t.constraint.startsWith('\\')) return '_'
|
|
768
|
+
return t.constraint[0]
|
|
769
|
+
}
|
|
770
|
+
return t.hint || '_'
|
|
771
|
+
}).join('')
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
instance.format = function (pattern, value) {
|
|
775
|
+
return instance(pattern, value)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
instance.define = function (name, definition) {
|
|
779
|
+
if (!definition?.pattern) throw new Error(`mask.define: "${name}" precisa de um pattern`)
|
|
780
|
+
if (definition.validate && typeof definition.validate !== 'function') {
|
|
781
|
+
throw new Error(`mask.define: "${name}" validate precisa ser uma função`)
|
|
782
|
+
}
|
|
783
|
+
localRegistry.set(name, definition)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
instance.undefine = function (name) {
|
|
787
|
+
localRegistry.delete(name)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
instance.names = function () {
|
|
791
|
+
return Array.from(localRegistry.keys())
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
instance.rawLength = function (pattern, value) {
|
|
795
|
+
if (value == null) return 0
|
|
796
|
+
const p = resolveLocal(pattern)
|
|
797
|
+
const pats = Array.isArray(p) ? p : [p]
|
|
798
|
+
const masked = instance(pattern, String(value))
|
|
799
|
+
return extractInputChars(masked, pats, resolveLocalEntry(pattern)?.validate, localSlots, localCacheId, localSlotsVersion).length
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
instance.patternLength = function (pattern) {
|
|
803
|
+
const p = resolveLocal(pattern)
|
|
804
|
+
const pats = Array.isArray(p) ? p : [p]
|
|
805
|
+
return Math.max(...pats.map(p => fullLength(p, localSlots, localCacheId, localSlotsVersion)))
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
instance.on = function (input, pattern, options = {}) {
|
|
809
|
+
const { onValue, onMasked } = options
|
|
810
|
+
|
|
811
|
+
function resolvePatterns() {
|
|
812
|
+
const p = resolveLocal(pattern)
|
|
813
|
+
return Array.isArray(p) ? p : [p]
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function onKeydown(e) {
|
|
817
|
+
if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return
|
|
818
|
+
const pats = resolvePatterns()
|
|
819
|
+
const chars = extractInputChars(e.target.value, pats, resolveLocalEntry(pattern)?.validate, localSlots, localCacheId, localSlotsVersion)
|
|
820
|
+
const maxLimit = Math.max(...pats.map(p => inputCount(p, localSlots, localCacheId, localSlotsVersion)))
|
|
821
|
+
if (chars.length >= maxLimit) e.preventDefault()
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function handler(e) {
|
|
825
|
+
const el = e.target
|
|
826
|
+
const raw = el.value
|
|
827
|
+
const masked = instance(pattern, raw)
|
|
828
|
+
const cursor = el.selectionStart ?? masked.length
|
|
829
|
+
const diff = masked.length - raw.length
|
|
830
|
+
el.value = masked
|
|
831
|
+
requestAnimationFrame(() => {
|
|
832
|
+
const pos = Math.max(0, cursor + diff)
|
|
833
|
+
el.setSelectionRange(pos, pos)
|
|
834
|
+
})
|
|
835
|
+
onMasked?.(masked)
|
|
836
|
+
onValue?.(instance.raw(pattern, masked))
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
input.addEventListener('keydown', onKeydown)
|
|
840
|
+
input.addEventListener('input', handler)
|
|
841
|
+
|
|
842
|
+
return () => {
|
|
843
|
+
input.removeEventListener('keydown', onKeydown)
|
|
844
|
+
input.removeEventListener('input', handler)
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
instance.defineSlot = function (symbol, definition) {
|
|
849
|
+
defineSlot(localSlots, symbol, definition)
|
|
850
|
+
localSlotsVersion++
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
instance.undefineSlot = function (symbol) {
|
|
854
|
+
if (DEFAULT_SLOTS[symbol]) localSlots[symbol] = DEFAULT_SLOTS[symbol]
|
|
855
|
+
else delete localSlots[symbol]
|
|
856
|
+
localSlotsVersion++
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
instance.slots = function () {
|
|
860
|
+
return Object.keys(localSlots)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// permite encadear mais defines após create()
|
|
864
|
+
instance.create = mask.create
|
|
865
|
+
|
|
866
|
+
return instance
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// ─── Export ────────────────────────────────────────────────────────────────
|
|
870
|
+
|
|
871
|
+
// export { mask }
|
|
872
|
+
// export default mask
|
|
873
|
+
|
|
874
|
+
module.exports = mask
|