diff --git a/src/mask-input.ts b/src/mask-input.ts index 5840db7..8857f98 100644 --- a/src/mask-input.ts +++ b/src/mask-input.ts @@ -76,7 +76,7 @@ export class MaskInput { // delete first character in eager mask when it's the only left if ( - mask.eager && + mask.isEager() && 'inputType' in e && e.inputType.startsWith('delete') && mask.unmasked(input.value).length <= 1 @@ -94,7 +94,7 @@ export class MaskInput { const se = input.selectionEnd let value = valueOld - if (mask.eager) { + if (mask.isEager()) { const unmasked = mask.unmasked(valueOld) const maskedUnmasked = mask.masked(unmasked) diff --git a/src/mask.ts b/src/mask.ts index 57f6333..4cbcb82 100644 --- a/src/mask.ts +++ b/src/mask.ts @@ -1,6 +1,6 @@ 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 { mask?: MaskType @@ -11,13 +11,12 @@ export interface MaskOptions { } export class Mask { - readonly mask: MaskType = '' - readonly tokens = tokens - readonly eager = false - readonly reversed = false + readonly opts: MaskOptions = {} private readonly memo = new Map() - constructor (opts: MaskOptions = {}) { + constructor (defaults: MaskOptions = {}) { + const opts = { ...defaults } + if (opts.tokens != null) { opts.tokens = (opts.tokensReplace as boolean) ? { ...opts.tokens } @@ -28,19 +27,22 @@ export class Mask { token.pattern = new RegExp(token.pattern) } } + } else { + opts.tokens = tokens } - if (opts.mask == null) { - opts.mask = '' - } else if (typeof opts.mask === 'object') { + if (Array.isArray(opts.mask)) { if (opts.mask.length > 1) { opts.mask.sort((a, b) => a.length - b.length) } else { opts.mask = opts.mask[0] ?? '' } } + if (opts.mask === '') { + opts.mask = null + } - Object.assign(this, opts) + this.opts = opts } masked (value: string): string { @@ -51,33 +53,47 @@ export class Mask { return this.process(value, this.findMask(value), false) } - completed (value: string): boolean { - const length = this.process(value, this.findMask(value)).length + isEager (): boolean { + return this.opts.eager === true + } - if (typeof this.mask === 'string') { - return length >= this.mask.length - } else if (typeof this.mask === 'function') { - return length >= this.findMask(value).length + isReversed (): boolean { + return this.opts.reversed === true + } + + 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 { 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 { - if (typeof this.mask === 'string') { - return this.mask - } else if (typeof this.mask === 'function') { - return this.mask(value) + private findMask (value: string): string | null { + const mask = this.opts.mask + if (mask == null) { + return null + } 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 ( - this.mask.find( - (mask) => this.process(value, mask, false).length >= last.length - ) ?? '' + mask.find((el) => this.process(value, el, false).length >= last.length) ?? + '' ) } @@ -98,32 +114,39 @@ export class Mask { 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}` if (this.memo.has(key)) return this.memo.get(key) const { mask, escaped } = this.escapeMask(maskRaw) const result: string[] = [] - const offset = this.reversed ? -1 : 1 - const method = this.reversed ? 'unshift' : 'push' - const lastMaskChar = this.reversed ? 0 : mask.length - 1 + const tokens = this.opts.tokens != null ? this.opts.tokens : {} + const offset = this.isReversed() ? -1 : 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 < mask.length && v < value.length const notLastMaskChar = (m: number): boolean => - (!this.reversed && m <= lastMaskChar) || - (this.reversed && m >= lastMaskChar) + (!this.isReversed() && m <= lastMaskChar) || + (this.isReversed() && m >= lastMaskChar) let lastRawMaskChar let repeatedPos = -1 - let m = this.reversed ? mask.length - 1 : 0 - let v = this.reversed ? value.length - 1 : 0 + let m = this.isReversed() ? mask.length - 1 : 0 + let v = this.isReversed() ? value.length - 1 : 0 while (check()) { const maskChar = mask.charAt(m) - const token = this.tokens[maskChar] + const token = tokens[maskChar] const valueChar = token?.transform != null ? token.transform(value.charAt(v)) @@ -151,7 +174,7 @@ export class Mask { } else if (token.multiple as boolean) { const hasValue = result[v - offset]?.match(token.pattern) != null const nextMask = mask.charAt(m + offset) - if (hasValue && nextMask !== '' && this.tokens[nextMask] == null) { + if (hasValue && nextMask !== '' && tokens[nextMask] == null) { m += offset v -= offset } else { @@ -171,25 +194,25 @@ export class Mask { v += offset } else { - if (masked && !this.eager) { + if (masked && !this.isEager()) { result[method](maskChar) } - if (valueChar === maskChar && !this.eager) { + if (valueChar === maskChar && !this.isEager()) { v += offset } else { lastRawMaskChar = maskChar } - if (!this.eager) { + if (!this.isEager()) { m += offset } } - if (this.eager) { + if (this.isEager()) { while ( notLastMaskChar(m) && - (this.tokens[mask.charAt(m)] == null || escaped.includes(m)) + (tokens[mask.charAt(m)] == null || escaped.includes(m)) ) { if (masked) { result[method](mask.charAt(m)) diff --git a/test/mask-input.test.ts b/test/mask-input.test.ts index 19a2c92..7dece3b 100644 --- a/test/mask-input.test.ts +++ b/test/mask-input.test.ts @@ -41,8 +41,8 @@ describe('test init', () => { ` const mask = new MaskInput('[data-maska]') - expect([...mask.items][0][1].eager).toBe(true) - expect([...mask.items][1][1].eager).toBe(false) + expect([...mask.items][0][1].isEager()).toBe(true) + expect([...mask.items][1][1].isEager()).toBe(false) }) test('test callback', async () => { @@ -126,8 +126,8 @@ describe('test init', () => { const input = document.getElementById('input') new MaskInput('#input') - await user.type(input, '1') - expect(input).toHaveValue('') + await user.type(input, '1a') + expect(input).toHaveValue('1a') }) test('no mask param', async () => { @@ -135,8 +135,8 @@ describe('test init', () => { const input = document.getElementById('input') new MaskInput(input) - await user.type(input, '1') - expect(input).toHaveValue('') + await user.type(input, '1a') + expect(input).toHaveValue('1a') }) }) @@ -149,74 +149,74 @@ describe('test data-attr', () => { test('empty mask', () => { const mask = prepareMaskWithHtml(``) - expect([...mask.items][0][1].mask).toBe('') + expect([...mask.items][0][1].opts.mask).toBe(undefined) }) test('simple mask', () => { const mask = prepareMaskWithHtml(``) - expect([...mask.items][0][1].mask).toBe('#-#') + expect([...mask.items][0][1].opts.mask).toBe('#-#') }) test('dynamic mask', () => { const mask = prepareMaskWithHtml( `` ) - expect([...mask.items][0][1].mask.length).toBe(2) + expect([...mask.items][0][1].opts.mask?.length).toBe(2) }) test('eager mask', () => { const mask = prepareMaskWithHtml(``) - expect([...mask.items][0][1].eager).toBe(true) + expect([...mask.items][0][1].isEager()).toBe(true) }) test('eager mask true', () => { const mask = prepareMaskWithHtml( `` ) - expect([...mask.items][0][1].eager).toBe(true) + expect([...mask.items][0][1].isEager()).toBe(true) }) test('eager mask false', () => { const mask = prepareMaskWithHtml( `` ) - expect([...mask.items][0][1].eager).toBe(false) + expect([...mask.items][0][1].isEager()).toBe(false) }) test('reversed mask', () => { const mask = prepareMaskWithHtml(``) - expect([...mask.items][0][1].reversed).toBe(true) + expect([...mask.items][0][1].opts.reversed).toBe(true) }) test('custom tokens mask', () => { const mask = prepareMaskWithHtml( `` ) - expect([...mask.items][0][1].tokens).toHaveProperty('#.pattern') - expect([...mask.items][0][1].tokens).toHaveProperty('Z.pattern') + expect([...mask.items][0][1].opts.tokens).toHaveProperty('#.pattern') + expect([...mask.items][0][1].opts.tokens).toHaveProperty('Z.pattern') }) test('replace tokens mask', () => { const mask = prepareMaskWithHtml( `` ) - expect([...mask.items][0][1].tokens).toHaveProperty('Z.pattern') - expect([...mask.items][0][1].tokens).not.toHaveProperty('#.pattern') + expect([...mask.items][0][1].opts.tokens).toHaveProperty('Z.pattern') + expect([...mask.items][0][1].opts.tokens).not.toHaveProperty('#.pattern') }) test('single quotes tokens mask', () => { const mask = prepareMaskWithHtml( `` ) - expect([...mask.items][0][1].tokens).toHaveProperty('Z.pattern') + expect([...mask.items][0][1].opts.tokens).toHaveProperty('Z.pattern') }) test('simple tokens mask', () => { const mask = prepareMaskWithHtml( `` ) - expect([...mask.items][0][1].tokens).toHaveProperty('Z.optional', false) - expect([...mask.items][0][1].tokens).toHaveProperty('X.optional', true) + expect([...mask.items][0][1].opts.tokens).toHaveProperty('Z.optional', false) + expect([...mask.items][0][1].opts.tokens).toHaveProperty('X.optional', true) }) }) diff --git a/test/mask.test.ts b/test/mask.test.ts index e1adca1..9a4de18 100644 --- a/test/mask.test.ts +++ b/test/mask.test.ts @@ -3,16 +3,21 @@ import { expect, test } from 'vitest' import { Mask } from '../src/mask' test('null mask', () => { - // @ts-ignore 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', () => { const mask = new Mask({ mask: undefined }) - expect(mask.masked('1a')).toBe('') + expect(mask.masked('1a')).toBe('1a') }) test('@ @ mask', () => { @@ -721,7 +726,7 @@ test('multiple letters mask', () => { test('dynamic empty mask', () => { const mask = new Mask({ mask: [] }) - expect(mask.masked('1')).toBe('') + expect(mask.masked('1')).toBe('1') }) test('dynamic single mask', () => {