2
0
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:
Alexander Shabunevich
2024-04-06 22:43:56 +03:00
parent 243b8445a2
commit 1e50a2a259
4 changed files with 83 additions and 72 deletions
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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('')
})
})