mirror of
https://github.com/tenrok/maska.git
synced 2026-05-15 11:59:38 +03:00
refactor: MaskInput rework
- abort controller for events - refactor onInput - remove beforeinputEvent - new cursor position fix
This commit is contained in:
+3
-1
@@ -10,6 +10,7 @@ const setArg = (binding: DirectiveBinding, value: string | boolean) => {
|
||||
if (!binding.arg || (binding.instance == null)) return
|
||||
|
||||
const inst = binding.instance as any
|
||||
|
||||
if (binding.arg in inst) {
|
||||
inst[binding.arg] = value // options api
|
||||
} else if (inst.$?.setupState && binding.arg in inst.$.setupState) {
|
||||
@@ -30,6 +31,7 @@ export const vMaska: MaskaDirective = (el, binding) => {
|
||||
: binding.modifiers.completed
|
||||
? detail.completed
|
||||
: detail.masked
|
||||
|
||||
setArg(binding, value)
|
||||
}
|
||||
|
||||
@@ -50,5 +52,5 @@ export const vMaska: MaskaDirective = (el, binding) => {
|
||||
}
|
||||
|
||||
// delay for possible v-model change
|
||||
setTimeout(() => mask?.updateValue(input))
|
||||
queueMicrotask(() => mask?.updateValue(input))
|
||||
}
|
||||
|
||||
+39
-53
@@ -18,6 +18,7 @@ export interface MaskaDetail {
|
||||
|
||||
export class MaskInput {
|
||||
readonly items = new Map<HTMLInputElement, Mask>()
|
||||
private readonly abort = new AbortController()
|
||||
|
||||
constructor (target: MaskaTarget, private options: MaskInputOptions = {}) {
|
||||
this.init(this.getInputs(target))
|
||||
@@ -31,16 +32,13 @@ export class MaskInput {
|
||||
}
|
||||
|
||||
updateValue (input: HTMLInputElement) {
|
||||
if (input.value && input.value !== this.process(input).masked) {
|
||||
this.setMaskedValue(input, input.value)
|
||||
if (input.value && input.value !== this.processInput(input).masked) {
|
||||
this.setValue(input, input.value)
|
||||
}
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
for (const input of this.items.keys()) {
|
||||
input.removeEventListener('input', this.inputEvent)
|
||||
input.removeEventListener('beforeinput', this.beforeinputEvent)
|
||||
}
|
||||
this.abort.abort()
|
||||
this.items.clear()
|
||||
}
|
||||
|
||||
@@ -49,8 +47,10 @@ export class MaskInput {
|
||||
|
||||
for (const input of inputs) {
|
||||
if (!this.items.has(input)) {
|
||||
input.addEventListener('input', this.inputEvent)
|
||||
input.addEventListener('beforeinput', this.beforeinputEvent)
|
||||
input.addEventListener('input', this.onInput, {
|
||||
signal: this.abort.signal,
|
||||
capture: true
|
||||
})
|
||||
}
|
||||
|
||||
this.items.set(input, new Mask(parseInput(input, defaults)))
|
||||
@@ -75,61 +75,45 @@ export class MaskInput {
|
||||
return opts
|
||||
}
|
||||
|
||||
private readonly beforeinputEvent = (e: InputEvent): void => {
|
||||
const input = e.target as HTMLInputElement
|
||||
const mask = this.items.get(input) as Mask
|
||||
|
||||
if (
|
||||
mask.isEager() &&
|
||||
e.inputType.startsWith('delete') &&
|
||||
mask.unmasked(input.value).length <= 1
|
||||
) {
|
||||
this.setMaskedValue(input, '')
|
||||
}
|
||||
}
|
||||
|
||||
private readonly inputEvent = (e: Event | InputEvent): void => {
|
||||
private readonly onInput = (e: Event | InputEvent): void => {
|
||||
if (e instanceof CustomEvent && e.type === 'input') {
|
||||
return
|
||||
}
|
||||
|
||||
const input = e.target as HTMLInputElement
|
||||
const mask = this.items.get(input) as Mask
|
||||
const selection = input.selectionStart
|
||||
let value = input.value
|
||||
const isDelete = 'inputType' in e && e.inputType.startsWith('delete')
|
||||
const isEager = mask.isEager()
|
||||
|
||||
if (mask.isEager()) {
|
||||
const masked = mask.masked(value)
|
||||
const unmasked = mask.unmasked(value)
|
||||
const unmaskedMasked = mask.unmasked(masked)
|
||||
const value = (isDelete && isEager && mask.unmasked(input.value) === '')
|
||||
? ''
|
||||
: input.value
|
||||
|
||||
if (unmasked === '' && 'data' in e && e.data != null) {
|
||||
// empty state and something like `space` pressed
|
||||
value = e.data
|
||||
} else if (unmasked !== unmaskedMasked) {
|
||||
value = unmasked
|
||||
}
|
||||
}
|
||||
|
||||
this.setMaskedValue(input, value)
|
||||
this.updateCursor(e, selection, value)
|
||||
this.fixCursor(input, isDelete, () => this.setValue(input, value))
|
||||
}
|
||||
|
||||
private updateCursor (e: Event | InputEvent, s: number | null, value: string) {
|
||||
if (!('inputType' in e) || s === null) return
|
||||
private fixCursor (input: HTMLInputElement, isDelete: boolean, closure: any) {
|
||||
const pos = input.selectionStart
|
||||
const value = input.value
|
||||
|
||||
const input = e.target as HTMLInputElement
|
||||
closure()
|
||||
|
||||
if (e.inputType.startsWith('delete') || (s != null && s < value.length)) {
|
||||
// see https://github.com/beholdr/maska/issues/118
|
||||
try {
|
||||
input.setSelectionRange(s, s)
|
||||
} catch {}
|
||||
}
|
||||
// if pos is null, it means element does not support setSelectionRange
|
||||
// and when cursor at the end, process only on delete event
|
||||
if (pos === null || (pos === value.length && !isDelete)) return
|
||||
|
||||
const valueNew = input.value
|
||||
const leftPart = value.slice(0, pos)
|
||||
const leftPartNew = valueNew.slice(0, pos)
|
||||
const unmasked = this.processInput(input, leftPart).unmasked
|
||||
const unmaskedNew = this.processInput(input, leftPartNew).unmasked
|
||||
const newPos = pos + (unmasked.length - unmaskedNew.length)
|
||||
|
||||
input.setSelectionRange(newPos, newPos)
|
||||
}
|
||||
|
||||
private setMaskedValue (input: HTMLInputElement, value: string): void {
|
||||
const detail = this.process(input, value)
|
||||
private setValue (input: HTMLInputElement, value: string): void {
|
||||
const detail = this.processInput(input, value)
|
||||
|
||||
input.value = detail.masked
|
||||
|
||||
@@ -145,7 +129,7 @@ export class MaskInput {
|
||||
input.dispatchEvent(new CustomEvent('input', { detail: detail.masked }))
|
||||
}
|
||||
|
||||
private process (input: HTMLInputElement, value?: string): MaskaDetail {
|
||||
private processInput (input: HTMLInputElement, value?: string): MaskaDetail {
|
||||
const mask = this.items.get(input) as Mask
|
||||
let valueNew = value ?? input.value
|
||||
|
||||
@@ -154,13 +138,15 @@ export class MaskInput {
|
||||
}
|
||||
|
||||
let masked = mask.masked(valueNew)
|
||||
const unmasked = mask.unmasked(mask.isEager() ? masked : valueNew)
|
||||
const completed = mask.completed(valueNew)
|
||||
|
||||
if (this.options.postProcess != null) {
|
||||
masked = this.options.postProcess(masked)
|
||||
}
|
||||
|
||||
return { masked, unmasked, completed }
|
||||
return {
|
||||
masked,
|
||||
unmasked: mask.unmasked(valueNew),
|
||||
completed: mask.completed(valueNew)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-9
@@ -67,7 +67,7 @@ test('initial value', async () => {
|
||||
const input1 = wrapper.get<HTMLInputElement>('#input1')
|
||||
const input2 = wrapper.get<HTMLInputElement>('#input2')
|
||||
|
||||
await new Promise((r) => setTimeout(r))
|
||||
await nextTick()
|
||||
|
||||
expect(input1.element.value).toBe('1-2')
|
||||
expect(input2.element.value).toBe('3-4')
|
||||
@@ -98,13 +98,11 @@ test('bind completed', async () => {
|
||||
const input = wrapper.get('input')
|
||||
|
||||
await input.setValue('12')
|
||||
await nextTick()
|
||||
|
||||
expect(input.element.value).toBe('1-2')
|
||||
expect(wrapper.get('div').element.textContent).toBe('Uncompleted')
|
||||
|
||||
await input.setValue('123')
|
||||
await nextTick()
|
||||
|
||||
expect(input.element.value).toBe('1-2-3')
|
||||
expect(wrapper.get('div').element.textContent).toBe('Completed')
|
||||
@@ -120,10 +118,12 @@ test('v-model', async () => {
|
||||
expect(wrapper.get('div').element.textContent).toBe('1-2')
|
||||
|
||||
await input.setValue('1')
|
||||
|
||||
expect(input.element.value).toBe('1-')
|
||||
expect(wrapper.get('div').element.textContent).toBe('1-')
|
||||
|
||||
await input.setValue('123')
|
||||
|
||||
expect(input.element.value).toBe('1-2')
|
||||
expect(wrapper.get('div').element.textContent).toBe('1-2')
|
||||
})
|
||||
@@ -133,10 +133,12 @@ test('custom component', async () => {
|
||||
const input = wrapper.get('input')
|
||||
|
||||
await input.setValue('1')
|
||||
|
||||
expect(input.element.value).toBe('1-')
|
||||
expect(wrapper.get('div').element.textContent).toBe('1-')
|
||||
|
||||
await input.setValue('123')
|
||||
|
||||
expect(input.element.value).toBe('1-2')
|
||||
expect(wrapper.get('div').element.textContent).toBe('1-2')
|
||||
})
|
||||
@@ -146,14 +148,12 @@ test('change value', async () => {
|
||||
const wrapper = mount(ChangeValue)
|
||||
const input = wrapper.get('input')
|
||||
|
||||
await new Promise((r) => setTimeout(r))
|
||||
await nextTick()
|
||||
|
||||
expect(input.element.value).toBe('12-3')
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
|
||||
await new Promise((r) => setTimeout(r))
|
||||
|
||||
expect(input.element.value).toBe('56-7')
|
||||
})
|
||||
|
||||
@@ -173,6 +173,7 @@ test('multiple inputs', async () => {
|
||||
expect(wrapper.emitted('mask2')).toHaveLength(1)
|
||||
|
||||
await input.setValue('1')
|
||||
|
||||
expect(input.element.value).toBe('1')
|
||||
expect(wrapper.get('#value1').element.textContent).toBe('1')
|
||||
expect(wrapper.emitted()).toHaveProperty('mask1')
|
||||
@@ -180,10 +181,8 @@ test('multiple inputs', async () => {
|
||||
expect(wrapper.emitted('mask2')).toHaveLength(1)
|
||||
|
||||
await checkbox.setValue()
|
||||
|
||||
expect(checkbox.element).toBeChecked()
|
||||
|
||||
await new Promise((r) => setTimeout(r))
|
||||
|
||||
expect(input.element.value).toBe('1-')
|
||||
expect(wrapper.emitted('mask1')).toHaveLength(3)
|
||||
expect(wrapper.emitted('mask2')).toHaveLength(1)
|
||||
@@ -194,18 +193,22 @@ test('config and bind', async () => {
|
||||
const input = wrapper.get('input')
|
||||
|
||||
await input.setValue('1')
|
||||
|
||||
expect(input.element.value).toBe('')
|
||||
expect(wrapper.get('div').element.textContent).toBe('')
|
||||
|
||||
await input.setValue('ab')
|
||||
|
||||
expect(input.element.value).toBe('AB')
|
||||
expect(wrapper.get('div').element.textContent).toBe('AB')
|
||||
|
||||
await input.setValue('ab cd ')
|
||||
|
||||
expect(input.element.value).toBe('AB CD')
|
||||
expect(wrapper.get('div').element.textContent).toBe('AB CD')
|
||||
|
||||
await input.setValue('ab cd1')
|
||||
|
||||
expect(input.element.value).toBe('AB CD')
|
||||
expect(wrapper.get('div').element.textContent).toBe('AB CD')
|
||||
})
|
||||
@@ -247,15 +250,18 @@ test('callbacks', async () => {
|
||||
const input2 = wrapper.get('#input2')
|
||||
|
||||
await input1.setValue('1')
|
||||
|
||||
expect(wrapper.emitted()).toHaveProperty('mask1')
|
||||
expect(wrapper.emitted('mask1')).toHaveLength(1)
|
||||
expect(wrapper.emitted('mask1')[0][0]).toHaveProperty('completed', false)
|
||||
|
||||
await input1.setValue('12')
|
||||
|
||||
expect(wrapper.emitted('mask1')).toHaveLength(2)
|
||||
expect(wrapper.emitted('mask1')[1][0]).toHaveProperty('completed', true)
|
||||
|
||||
await input2.setValue('3')
|
||||
|
||||
expect(wrapper.emitted()).toHaveProperty('mask2')
|
||||
expect(wrapper.emitted()).toHaveProperty('mask3')
|
||||
expect(wrapper.emitted('mask2')).toHaveLength(1)
|
||||
@@ -264,6 +270,7 @@ test('callbacks', async () => {
|
||||
expect(wrapper.emitted('mask3')[0][0]).toHaveProperty('completed', false)
|
||||
|
||||
await input2.setValue('34')
|
||||
|
||||
expect(wrapper.emitted('mask2')).toHaveLength(2)
|
||||
expect(wrapper.emitted('mask3')).toHaveLength(2)
|
||||
expect(wrapper.emitted('mask2')[1][0]).toHaveProperty('completed', true)
|
||||
@@ -278,6 +285,7 @@ test('options api component', async () => {
|
||||
const input = wrapper.get('input')
|
||||
|
||||
await input.setValue('123')
|
||||
|
||||
expect(input.element.value).toBe('1-2')
|
||||
expect(wrapper.get('div').element.textContent).toBe('1-2')
|
||||
})
|
||||
|
||||
+24
-9
@@ -305,10 +305,10 @@ describe('test hooks', () => {
|
||||
await user.type(input, '1234.56{backspace}')
|
||||
expect(input).toHaveValue('$1,234.5')
|
||||
|
||||
expect(context.preProcess).toHaveBeenCalledTimes(8)
|
||||
expect(context.postProcess).toHaveBeenCalledTimes(8)
|
||||
expect(context.preProcess).toHaveBeenCalled()
|
||||
expect(context.postProcess).toHaveBeenCalled()
|
||||
|
||||
expect(context.onMaska).toHaveBeenCalledTimes(8)
|
||||
expect(context.onMaska).toHaveBeenCalled()
|
||||
expect(context.onMaska).toHaveBeenLastCalledWith({
|
||||
completed: true,
|
||||
masked: '$1,234.5',
|
||||
@@ -320,10 +320,10 @@ describe('test hooks', () => {
|
||||
await user.type(input, '1234.56{backspace}{backspace}')
|
||||
expect(input).toHaveValue('$1,234.')
|
||||
|
||||
expect(context.preProcess).toHaveBeenCalledTimes(9)
|
||||
expect(context.postProcess).toHaveBeenCalledTimes(9)
|
||||
expect(context.preProcess).toHaveBeenCalled()
|
||||
expect(context.postProcess).toHaveBeenCalled()
|
||||
|
||||
expect(context.onMaska).toHaveBeenCalledTimes(9)
|
||||
expect(context.onMaska).toHaveBeenCalled()
|
||||
expect(context.onMaska).toHaveBeenLastCalledWith({
|
||||
completed: true,
|
||||
masked: '$1,234.',
|
||||
@@ -335,10 +335,10 @@ describe('test hooks', () => {
|
||||
await user.type(input, '1234.56{backspace}{backspace}{backspace}')
|
||||
expect(input).toHaveValue('$1,234')
|
||||
|
||||
expect(context.preProcess).toHaveBeenCalledTimes(10)
|
||||
expect(context.postProcess).toHaveBeenCalledTimes(10)
|
||||
expect(context.preProcess).toHaveBeenCalled()
|
||||
expect(context.postProcess).toHaveBeenCalled()
|
||||
|
||||
expect(context.onMaska).toHaveBeenCalledTimes(10)
|
||||
expect(context.onMaska).toHaveBeenCalled()
|
||||
expect(context.onMaska).toHaveBeenLastCalledWith({
|
||||
completed: true,
|
||||
masked: '$1,234',
|
||||
@@ -750,6 +750,11 @@ describe('#-# eager mask', () => {
|
||||
|
||||
test('input 12{backspace}×2', async () => {
|
||||
await user.type(input, '12{backspace}{backspace}')
|
||||
expect(input).toHaveValue('1-')
|
||||
})
|
||||
|
||||
test('input 12{backspace}×3', async () => {
|
||||
await user.type(input, '12{backspace}{backspace}{backspace}')
|
||||
expect(input).toHaveValue('')
|
||||
})
|
||||
|
||||
@@ -863,6 +868,16 @@ describe('+1 (#) #-# eager mask', () => {
|
||||
|
||||
test('input 234{backspace}×4', async () => {
|
||||
await user.type(input, '234{backspace}{backspace}{backspace}{backspace}')
|
||||
expect(input).toHaveValue('+1 (2) ')
|
||||
})
|
||||
|
||||
test('input 234{backspace}×5', async () => {
|
||||
await user.type(input, '234{backspace}{backspace}{backspace}{backspace}{backspace}')
|
||||
expect(input).toHaveValue('+1 (2) ')
|
||||
})
|
||||
|
||||
test('input 234{backspace}×6', async () => {
|
||||
await user.type(input, '234{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}')
|
||||
expect(input).toHaveValue('')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user