2
0
mirror of https://github.com/tenrok/maska.git synced 2026-06-11 18:02:27 +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)
}
}
}