mirror of
https://github.com/tenrok/maska.git
synced 2026-06-23 20:40:35 +03:00
Refactor Mask options
Allow to pass null mask for disable masking
This commit is contained in:
+2
-2
@@ -76,7 +76,7 @@ export class MaskInput {
|
|||||||
|
|
||||||
// delete first character in eager mask when it's the only left
|
// delete first character in eager mask when it's the only left
|
||||||
if (
|
if (
|
||||||
mask.eager &&
|
mask.isEager() &&
|
||||||
'inputType' in e &&
|
'inputType' in e &&
|
||||||
e.inputType.startsWith('delete') &&
|
e.inputType.startsWith('delete') &&
|
||||||
mask.unmasked(input.value).length <= 1
|
mask.unmasked(input.value).length <= 1
|
||||||
@@ -94,7 +94,7 @@ export class MaskInput {
|
|||||||
const se = input.selectionEnd
|
const se = input.selectionEnd
|
||||||
let value = valueOld
|
let value = valueOld
|
||||||
|
|
||||||
if (mask.eager) {
|
if (mask.isEager()) {
|
||||||
const unmasked = mask.unmasked(valueOld)
|
const unmasked = mask.unmasked(valueOld)
|
||||||
const maskedUnmasked = mask.masked(unmasked)
|
const maskedUnmasked = mask.masked(unmasked)
|
||||||
|
|
||||||
|
|||||||
+65
-42
@@ -1,6 +1,6 @@
|
|||||||
import { MaskTokens, tokens } from './tokens'
|
import { MaskTokens, tokens } from './tokens'
|
||||||
|
|
||||||
export type MaskType = string | string[] | ((input: string) => string)
|
export type MaskType = string | string[] | ((input: string) => string) | null
|
||||||
|
|
||||||
export interface MaskOptions {
|
export interface MaskOptions {
|
||||||
mask?: MaskType
|
mask?: MaskType
|
||||||
@@ -11,13 +11,12 @@ export interface MaskOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Mask {
|
export class Mask {
|
||||||
readonly mask: MaskType = ''
|
readonly opts: MaskOptions = {}
|
||||||
readonly tokens = tokens
|
|
||||||
readonly eager = false
|
|
||||||
readonly reversed = false
|
|
||||||
private readonly memo = new Map()
|
private readonly memo = new Map()
|
||||||
|
|
||||||
constructor (opts: MaskOptions = {}) {
|
constructor (defaults: MaskOptions = {}) {
|
||||||
|
const opts = { ...defaults }
|
||||||
|
|
||||||
if (opts.tokens != null) {
|
if (opts.tokens != null) {
|
||||||
opts.tokens = (opts.tokensReplace as boolean)
|
opts.tokens = (opts.tokensReplace as boolean)
|
||||||
? { ...opts.tokens }
|
? { ...opts.tokens }
|
||||||
@@ -28,19 +27,22 @@ export class Mask {
|
|||||||
token.pattern = new RegExp(token.pattern)
|
token.pattern = new RegExp(token.pattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
opts.tokens = tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.mask == null) {
|
if (Array.isArray(opts.mask)) {
|
||||||
opts.mask = ''
|
|
||||||
} else if (typeof opts.mask === 'object') {
|
|
||||||
if (opts.mask.length > 1) {
|
if (opts.mask.length > 1) {
|
||||||
opts.mask.sort((a, b) => a.length - b.length)
|
opts.mask.sort((a, b) => a.length - b.length)
|
||||||
} else {
|
} else {
|
||||||
opts.mask = opts.mask[0] ?? ''
|
opts.mask = opts.mask[0] ?? ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (opts.mask === '') {
|
||||||
|
opts.mask = null
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this, opts)
|
this.opts = opts
|
||||||
}
|
}
|
||||||
|
|
||||||
masked (value: string): string {
|
masked (value: string): string {
|
||||||
@@ -51,33 +53,47 @@ export class Mask {
|
|||||||
return this.process(value, this.findMask(value), false)
|
return this.process(value, this.findMask(value), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
completed (value: string): boolean {
|
isEager (): boolean {
|
||||||
const length = this.process(value, this.findMask(value)).length
|
return this.opts.eager === true
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof this.mask === 'string') {
|
isReversed (): boolean {
|
||||||
return length >= this.mask.length
|
return this.opts.reversed === true
|
||||||
} else if (typeof this.mask === 'function') {
|
}
|
||||||
return length >= this.findMask(value).length
|
|
||||||
|
completed (value: string): boolean {
|
||||||
|
const mask = this.findMask(value)
|
||||||
|
if (this.opts.mask == null || mask == null) return false
|
||||||
|
|
||||||
|
const length = this.process(value, mask).length
|
||||||
|
|
||||||
|
if (typeof this.opts.mask === 'string') {
|
||||||
|
return length >= this.opts.mask.length
|
||||||
|
} else if (typeof this.opts.mask === 'function') {
|
||||||
|
return length >= mask.length
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
this.mask.filter((m) => length >= m.length).length === this.mask.length
|
this.opts.mask.filter((m) => length >= m.length).length ===
|
||||||
|
this.opts.mask.length
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private findMask (value: string): string {
|
private findMask (value: string): string | null {
|
||||||
if (typeof this.mask === 'string') {
|
const mask = this.opts.mask
|
||||||
return this.mask
|
if (mask == null) {
|
||||||
} else if (typeof this.mask === 'function') {
|
return null
|
||||||
return this.mask(value)
|
} else if (typeof mask === 'string') {
|
||||||
|
return mask
|
||||||
|
} else if (typeof mask === 'function') {
|
||||||
|
return mask(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const last = this.process(value, this.mask.slice(-1).pop() ?? '', false)
|
const last = this.process(value, mask.slice(-1).pop() ?? '', false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.mask.find(
|
mask.find((el) => this.process(value, el, false).length >= last.length) ??
|
||||||
(mask) => this.process(value, mask, false).length >= last.length
|
''
|
||||||
) ?? ''
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,32 +114,39 @@ export class Mask {
|
|||||||
return { mask: chars.join(''), escaped }
|
return { mask: chars.join(''), escaped }
|
||||||
}
|
}
|
||||||
|
|
||||||
private process (value: string, maskRaw: string, masked = true): string {
|
private process (
|
||||||
|
value: string,
|
||||||
|
maskRaw: string | null,
|
||||||
|
masked = true
|
||||||
|
): string {
|
||||||
|
if (maskRaw == null) return value
|
||||||
|
|
||||||
const key = `value=${value},mask=${maskRaw},masked=${masked ? 1 : 0}`
|
const key = `value=${value},mask=${maskRaw},masked=${masked ? 1 : 0}`
|
||||||
if (this.memo.has(key)) return this.memo.get(key)
|
if (this.memo.has(key)) return this.memo.get(key)
|
||||||
|
|
||||||
const { mask, escaped } = this.escapeMask(maskRaw)
|
const { mask, escaped } = this.escapeMask(maskRaw)
|
||||||
const result: string[] = []
|
const result: string[] = []
|
||||||
const offset = this.reversed ? -1 : 1
|
const tokens = this.opts.tokens != null ? this.opts.tokens : {}
|
||||||
const method = this.reversed ? 'unshift' : 'push'
|
const offset = this.isReversed() ? -1 : 1
|
||||||
const lastMaskChar = this.reversed ? 0 : mask.length - 1
|
const method = this.isReversed() ? 'unshift' : 'push'
|
||||||
|
const lastMaskChar = this.isReversed() ? 0 : mask.length - 1
|
||||||
|
|
||||||
const check = this.reversed
|
const check = this.isReversed()
|
||||||
? () => m > -1 && v > -1
|
? () => m > -1 && v > -1
|
||||||
: () => m < mask.length && v < value.length
|
: () => m < mask.length && v < value.length
|
||||||
|
|
||||||
const notLastMaskChar = (m: number): boolean =>
|
const notLastMaskChar = (m: number): boolean =>
|
||||||
(!this.reversed && m <= lastMaskChar) ||
|
(!this.isReversed() && m <= lastMaskChar) ||
|
||||||
(this.reversed && m >= lastMaskChar)
|
(this.isReversed() && m >= lastMaskChar)
|
||||||
|
|
||||||
let lastRawMaskChar
|
let lastRawMaskChar
|
||||||
let repeatedPos = -1
|
let repeatedPos = -1
|
||||||
let m = this.reversed ? mask.length - 1 : 0
|
let m = this.isReversed() ? mask.length - 1 : 0
|
||||||
let v = this.reversed ? value.length - 1 : 0
|
let v = this.isReversed() ? value.length - 1 : 0
|
||||||
|
|
||||||
while (check()) {
|
while (check()) {
|
||||||
const maskChar = mask.charAt(m)
|
const maskChar = mask.charAt(m)
|
||||||
const token = this.tokens[maskChar]
|
const token = tokens[maskChar]
|
||||||
const valueChar =
|
const valueChar =
|
||||||
token?.transform != null
|
token?.transform != null
|
||||||
? token.transform(value.charAt(v))
|
? token.transform(value.charAt(v))
|
||||||
@@ -151,7 +174,7 @@ export class Mask {
|
|||||||
} else if (token.multiple as boolean) {
|
} else if (token.multiple as boolean) {
|
||||||
const hasValue = result[v - offset]?.match(token.pattern) != null
|
const hasValue = result[v - offset]?.match(token.pattern) != null
|
||||||
const nextMask = mask.charAt(m + offset)
|
const nextMask = mask.charAt(m + offset)
|
||||||
if (hasValue && nextMask !== '' && this.tokens[nextMask] == null) {
|
if (hasValue && nextMask !== '' && tokens[nextMask] == null) {
|
||||||
m += offset
|
m += offset
|
||||||
v -= offset
|
v -= offset
|
||||||
} else {
|
} else {
|
||||||
@@ -171,25 +194,25 @@ export class Mask {
|
|||||||
|
|
||||||
v += offset
|
v += offset
|
||||||
} else {
|
} else {
|
||||||
if (masked && !this.eager) {
|
if (masked && !this.isEager()) {
|
||||||
result[method](maskChar)
|
result[method](maskChar)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valueChar === maskChar && !this.eager) {
|
if (valueChar === maskChar && !this.isEager()) {
|
||||||
v += offset
|
v += offset
|
||||||
} else {
|
} else {
|
||||||
lastRawMaskChar = maskChar
|
lastRawMaskChar = maskChar
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.eager) {
|
if (!this.isEager()) {
|
||||||
m += offset
|
m += offset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.eager) {
|
if (this.isEager()) {
|
||||||
while (
|
while (
|
||||||
notLastMaskChar(m) &&
|
notLastMaskChar(m) &&
|
||||||
(this.tokens[mask.charAt(m)] == null || escaped.includes(m))
|
(tokens[mask.charAt(m)] == null || escaped.includes(m))
|
||||||
) {
|
) {
|
||||||
if (masked) {
|
if (masked) {
|
||||||
result[method](mask.charAt(m))
|
result[method](mask.charAt(m))
|
||||||
|
|||||||
+20
-20
@@ -41,8 +41,8 @@ describe('test init', () => {
|
|||||||
`
|
`
|
||||||
const mask = new MaskInput('[data-maska]')
|
const mask = new MaskInput('[data-maska]')
|
||||||
|
|
||||||
expect([...mask.items][0][1].eager).toBe(true)
|
expect([...mask.items][0][1].isEager()).toBe(true)
|
||||||
expect([...mask.items][1][1].eager).toBe(false)
|
expect([...mask.items][1][1].isEager()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('test callback', async () => {
|
test('test callback', async () => {
|
||||||
@@ -126,8 +126,8 @@ describe('test init', () => {
|
|||||||
const input = <HTMLInputElement>document.getElementById('input')
|
const input = <HTMLInputElement>document.getElementById('input')
|
||||||
new MaskInput('#input')
|
new MaskInput('#input')
|
||||||
|
|
||||||
await user.type(input, '1')
|
await user.type(input, '1a')
|
||||||
expect(input).toHaveValue('')
|
expect(input).toHaveValue('1a')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('no mask param', async () => {
|
test('no mask param', async () => {
|
||||||
@@ -135,8 +135,8 @@ describe('test init', () => {
|
|||||||
const input = <HTMLInputElement>document.getElementById('input')
|
const input = <HTMLInputElement>document.getElementById('input')
|
||||||
new MaskInput(input)
|
new MaskInput(input)
|
||||||
|
|
||||||
await user.type(input, '1')
|
await user.type(input, '1a')
|
||||||
expect(input).toHaveValue('')
|
expect(input).toHaveValue('1a')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -149,74 +149,74 @@ describe('test data-attr', () => {
|
|||||||
|
|
||||||
test('empty mask', () => {
|
test('empty mask', () => {
|
||||||
const mask = prepareMaskWithHtml(`<input id="input" data-maska>`)
|
const mask = prepareMaskWithHtml(`<input id="input" data-maska>`)
|
||||||
expect([...mask.items][0][1].mask).toBe('')
|
expect([...mask.items][0][1].opts.mask).toBe(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('simple mask', () => {
|
test('simple mask', () => {
|
||||||
const mask = prepareMaskWithHtml(`<input id="input" data-maska="#-#">`)
|
const mask = prepareMaskWithHtml(`<input id="input" data-maska="#-#">`)
|
||||||
expect([...mask.items][0][1].mask).toBe('#-#')
|
expect([...mask.items][0][1].opts.mask).toBe('#-#')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dynamic mask', () => {
|
test('dynamic mask', () => {
|
||||||
const mask = prepareMaskWithHtml(
|
const mask = prepareMaskWithHtml(
|
||||||
`<input id="input" data-maska="['#--#', '#-#--#']">`
|
`<input id="input" data-maska="['#--#', '#-#--#']">`
|
||||||
)
|
)
|
||||||
expect([...mask.items][0][1].mask.length).toBe(2)
|
expect([...mask.items][0][1].opts.mask?.length).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('eager mask', () => {
|
test('eager mask', () => {
|
||||||
const mask = prepareMaskWithHtml(`<input id="input" data-maska-eager>`)
|
const mask = prepareMaskWithHtml(`<input id="input" data-maska-eager>`)
|
||||||
expect([...mask.items][0][1].eager).toBe(true)
|
expect([...mask.items][0][1].isEager()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('eager mask true', () => {
|
test('eager mask true', () => {
|
||||||
const mask = prepareMaskWithHtml(
|
const mask = prepareMaskWithHtml(
|
||||||
`<input id="input" data-maska-eager="true">`
|
`<input id="input" data-maska-eager="true">`
|
||||||
)
|
)
|
||||||
expect([...mask.items][0][1].eager).toBe(true)
|
expect([...mask.items][0][1].isEager()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('eager mask false', () => {
|
test('eager mask false', () => {
|
||||||
const mask = prepareMaskWithHtml(
|
const mask = prepareMaskWithHtml(
|
||||||
`<input id="input" data-maska-eager="false">`
|
`<input id="input" data-maska-eager="false">`
|
||||||
)
|
)
|
||||||
expect([...mask.items][0][1].eager).toBe(false)
|
expect([...mask.items][0][1].isEager()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('reversed mask', () => {
|
test('reversed mask', () => {
|
||||||
const mask = prepareMaskWithHtml(`<input id="input" data-maska-reversed>`)
|
const mask = prepareMaskWithHtml(`<input id="input" data-maska-reversed>`)
|
||||||
expect([...mask.items][0][1].reversed).toBe(true)
|
expect([...mask.items][0][1].opts.reversed).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('custom tokens mask', () => {
|
test('custom tokens mask', () => {
|
||||||
const mask = prepareMaskWithHtml(
|
const mask = prepareMaskWithHtml(
|
||||||
`<input id="input" data-maska-tokens='{ "Z": { "pattern": "[0-9]" } }'>`
|
`<input id="input" data-maska-tokens='{ "Z": { "pattern": "[0-9]" } }'>`
|
||||||
)
|
)
|
||||||
expect([...mask.items][0][1].tokens).toHaveProperty('#.pattern')
|
expect([...mask.items][0][1].opts.tokens).toHaveProperty('#.pattern')
|
||||||
expect([...mask.items][0][1].tokens).toHaveProperty('Z.pattern')
|
expect([...mask.items][0][1].opts.tokens).toHaveProperty('Z.pattern')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('replace tokens mask', () => {
|
test('replace tokens mask', () => {
|
||||||
const mask = prepareMaskWithHtml(
|
const mask = prepareMaskWithHtml(
|
||||||
`<input id="input" data-maska-tokens='{ "Z": { "pattern": "[0-9]" } }' data-maska-tokens-replace>`
|
`<input id="input" data-maska-tokens='{ "Z": { "pattern": "[0-9]" } }' data-maska-tokens-replace>`
|
||||||
)
|
)
|
||||||
expect([...mask.items][0][1].tokens).toHaveProperty('Z.pattern')
|
expect([...mask.items][0][1].opts.tokens).toHaveProperty('Z.pattern')
|
||||||
expect([...mask.items][0][1].tokens).not.toHaveProperty('#.pattern')
|
expect([...mask.items][0][1].opts.tokens).not.toHaveProperty('#.pattern')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('single quotes tokens mask', () => {
|
test('single quotes tokens mask', () => {
|
||||||
const mask = prepareMaskWithHtml(
|
const mask = prepareMaskWithHtml(
|
||||||
`<input id="input" data-maska-tokens="{ 'Z': { 'pattern': '[0-9]' } }">`
|
`<input id="input" data-maska-tokens="{ 'Z': { 'pattern': '[0-9]' } }">`
|
||||||
)
|
)
|
||||||
expect([...mask.items][0][1].tokens).toHaveProperty('Z.pattern')
|
expect([...mask.items][0][1].opts.tokens).toHaveProperty('Z.pattern')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('simple tokens mask', () => {
|
test('simple tokens mask', () => {
|
||||||
const mask = prepareMaskWithHtml(
|
const mask = prepareMaskWithHtml(
|
||||||
`<input id="input" data-maska-tokens="Z:[0-9]|X:[0-9]:optional">`
|
`<input id="input" data-maska-tokens="Z:[0-9]|X:[0-9]:optional">`
|
||||||
)
|
)
|
||||||
expect([...mask.items][0][1].tokens).toHaveProperty('Z.optional', false)
|
expect([...mask.items][0][1].opts.tokens).toHaveProperty('Z.optional', false)
|
||||||
expect([...mask.items][0][1].tokens).toHaveProperty('X.optional', true)
|
expect([...mask.items][0][1].opts.tokens).toHaveProperty('X.optional', true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+9
-4
@@ -3,16 +3,21 @@ import { expect, test } from 'vitest'
|
|||||||
import { Mask } from '../src/mask'
|
import { Mask } from '../src/mask'
|
||||||
|
|
||||||
test('null mask', () => {
|
test('null mask', () => {
|
||||||
// @ts-ignore
|
|
||||||
const mask = new Mask({ mask: null })
|
const mask = new Mask({ mask: null })
|
||||||
|
|
||||||
expect(mask.masked('1a')).toBe('')
|
expect(mask.masked('1a')).toBe('1a')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('empty string mask', () => {
|
||||||
|
const mask = new Mask({ mask: '' })
|
||||||
|
|
||||||
|
expect(mask.masked('1a')).toBe('1a')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('undefined mask', () => {
|
test('undefined mask', () => {
|
||||||
const mask = new Mask({ mask: undefined })
|
const mask = new Mask({ mask: undefined })
|
||||||
|
|
||||||
expect(mask.masked('1a')).toBe('')
|
expect(mask.masked('1a')).toBe('1a')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('@ @ mask', () => {
|
test('@ @ mask', () => {
|
||||||
@@ -721,7 +726,7 @@ test('multiple letters mask', () => {
|
|||||||
test('dynamic empty mask', () => {
|
test('dynamic empty mask', () => {
|
||||||
const mask = new Mask({ mask: [] })
|
const mask = new Mask({ mask: [] })
|
||||||
|
|
||||||
expect(mask.masked('1')).toBe('')
|
expect(mask.masked('1')).toBe('1')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dynamic single mask', () => {
|
test('dynamic single mask', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user