mirror of
https://github.com/tenrok/maska.git
synced 2026-05-21 13:24:06 +03:00
feat!: rework for v3
- new directive format: pass options as value, bound as argument with modifiers - update and checkValue methods in MaskInput
This commit is contained in:
+34
-30
@@ -1,50 +1,54 @@
|
||||
import { Directive } from 'vue'
|
||||
import { Directive, DirectiveBinding } from 'vue'
|
||||
import { MaskaDetail, MaskInput, MaskInputOptions } from './mask-input'
|
||||
|
||||
type MaskaDirective = Directive<HTMLElement, MaskaDetail | undefined>
|
||||
type MaskaDirective = Directive<HTMLElement, MaskInputOptions | undefined>
|
||||
|
||||
const masks = new WeakMap<HTMLInputElement, MaskInput>()
|
||||
|
||||
const checkValue = (input: HTMLInputElement): void => {
|
||||
setTimeout(() => {
|
||||
if (masks.get(input)?.needUpdateValue(input) === true) {
|
||||
input.dispatchEvent(new CustomEvent('input'))
|
||||
}
|
||||
})
|
||||
// hacky way to update binding.arg without using defineExposed
|
||||
const setArg = (binding: DirectiveBinding, value: string | boolean) => {
|
||||
if (!binding.arg || !binding.instance) 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) {
|
||||
inst.$.setupState[binding.arg] = value // composition api
|
||||
}
|
||||
}
|
||||
|
||||
export const vMaska: MaskaDirective = (el, binding) => {
|
||||
const input = el instanceof HTMLInputElement ? el : el.querySelector('input')
|
||||
const opts = { ...(binding.arg as MaskInputOptions) } ?? {}
|
||||
const opts = { ...binding.value } ?? {}
|
||||
|
||||
if (input == null || input?.type === 'file') return
|
||||
|
||||
checkValue(input)
|
||||
|
||||
const existed = masks.get(input)
|
||||
if (existed != null) {
|
||||
if (!existed.needUpdateOptions(input, opts)) {
|
||||
return
|
||||
}
|
||||
|
||||
existed.destroy()
|
||||
}
|
||||
|
||||
if (binding.value != null) {
|
||||
const bound = binding.value
|
||||
const onMaska = (detail: MaskaDetail): void => {
|
||||
bound.masked = detail.masked
|
||||
bound.unmasked = detail.unmasked
|
||||
bound.completed = detail.completed
|
||||
if (binding.arg) {
|
||||
const updateArg = (detail: MaskaDetail) => {
|
||||
const value = binding.modifiers.unmasked
|
||||
? detail.unmasked
|
||||
: binding.modifiers.completed
|
||||
? detail.completed
|
||||
: detail.masked
|
||||
setArg(binding, value)
|
||||
}
|
||||
|
||||
opts.onMaska =
|
||||
opts.onMaska == null
|
||||
? onMaska
|
||||
? updateArg
|
||||
: Array.isArray(opts.onMaska)
|
||||
? [...opts.onMaska, onMaska]
|
||||
: [opts.onMaska, onMaska]
|
||||
? [...opts.onMaska, updateArg]
|
||||
: [opts.onMaska, updateArg]
|
||||
}
|
||||
|
||||
masks.set(input, new MaskInput(input, opts))
|
||||
let mask = masks.get(input)
|
||||
if (!mask) {
|
||||
mask = new MaskInput(input, opts)
|
||||
masks.set(input, mask)
|
||||
} else {
|
||||
mask.update(opts)
|
||||
}
|
||||
|
||||
// delay init to wait for v-model value
|
||||
setTimeout(() => mask?.checkValue(input))
|
||||
}
|
||||
|
||||
+74
-87
@@ -2,6 +2,7 @@ import { Mask, MaskOptions } from './mask'
|
||||
import { parseInput } from './parser'
|
||||
|
||||
type OnMaskaType = (detail: MaskaDetail) => void
|
||||
type MaskaTarget = string | NodeListOf<HTMLInputElement> | HTMLInputElement
|
||||
|
||||
export interface MaskInputOptions extends MaskOptions {
|
||||
onMaska?: OnMaskaType | OnMaskaType[]
|
||||
@@ -18,24 +19,21 @@ export interface MaskaDetail {
|
||||
export class MaskInput {
|
||||
readonly items = new Map<HTMLInputElement, Mask>()
|
||||
|
||||
constructor (
|
||||
target: string | NodeListOf<HTMLInputElement> | HTMLInputElement,
|
||||
readonly options: MaskInputOptions = {}
|
||||
) {
|
||||
if (typeof target === 'string') {
|
||||
this.init(
|
||||
Array.from(document.querySelectorAll(target)),
|
||||
this.getMaskOpts(options)
|
||||
)
|
||||
} else {
|
||||
this.init(
|
||||
'length' in target ? Array.from(target) : [target],
|
||||
this.getMaskOpts(options)
|
||||
)
|
||||
constructor(target: MaskaTarget, readonly options: MaskInputOptions = {}) {
|
||||
this.init(this.getInputs(target), this.getOptions(options))
|
||||
}
|
||||
|
||||
update(options: MaskInputOptions = {}): void {
|
||||
this.init(Array.from(this.items.keys()), this.getOptions(options))
|
||||
}
|
||||
|
||||
checkValue(input: HTMLInputElement) {
|
||||
if (input.value && input.value !== this.process(input).masked) {
|
||||
this.setMaskedValue(input, input.value)
|
||||
}
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
destroy(): void {
|
||||
for (const input of this.items.keys()) {
|
||||
input.removeEventListener('input', this.inputEvent)
|
||||
input.removeEventListener('beforeinput', this.beforeinputEvent)
|
||||
@@ -43,50 +41,40 @@ export class MaskInput {
|
||||
this.items.clear()
|
||||
}
|
||||
|
||||
needUpdateOptions (input: HTMLInputElement, opts: MaskInputOptions): boolean {
|
||||
const mask = this.items.get(input) as Mask
|
||||
const maskNew = new Mask(parseInput(input, this.getMaskOpts(opts)))
|
||||
private init(inputs: HTMLInputElement[], defaults: MaskOptions): void {
|
||||
for (const input of inputs) {
|
||||
const inited = this.items.has(input)
|
||||
|
||||
return JSON.stringify(mask.opts) !== JSON.stringify(maskNew.opts)
|
||||
this.items.set(input, new Mask(parseInput(input, defaults)))
|
||||
|
||||
if (!inited) {
|
||||
input.addEventListener('input', this.inputEvent)
|
||||
input.addEventListener('beforeinput', this.beforeinputEvent)
|
||||
this.checkValue(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
needUpdateValue (input: HTMLInputElement): boolean {
|
||||
const value = input.dataset.maskaValue
|
||||
private getInputs(target: MaskaTarget): HTMLInputElement[] {
|
||||
if (typeof target === 'string') {
|
||||
return Array.from(document.querySelectorAll(target))
|
||||
}
|
||||
|
||||
return (
|
||||
(value == null && input.value !== '') ||
|
||||
(value != null && value !== input.value)
|
||||
)
|
||||
return 'length' in target ? Array.from(target) : [target]
|
||||
}
|
||||
|
||||
private getMaskOpts (options: MaskInputOptions): MaskOptions {
|
||||
private getOptions(options: MaskInputOptions): MaskOptions {
|
||||
const { onMaska, preProcess, postProcess, ...opts } = options
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
private init (inputs: HTMLInputElement[], defaults: MaskOptions): void {
|
||||
for (const input of inputs) {
|
||||
const mask = new Mask(parseInput(input, defaults))
|
||||
this.items.set(input, mask)
|
||||
|
||||
if (input.value !== '') {
|
||||
this.setMaskedValue(input, input.value)
|
||||
}
|
||||
|
||||
input.addEventListener('input', this.inputEvent)
|
||||
input.addEventListener('beforeinput', this.beforeinputEvent)
|
||||
}
|
||||
}
|
||||
|
||||
private readonly beforeinputEvent = (e: Event | InputEvent): void => {
|
||||
private readonly beforeinputEvent = (e: InputEvent): void => {
|
||||
const input = e.target as HTMLInputElement
|
||||
const mask = this.items.get(input) as Mask
|
||||
|
||||
// delete first character in eager mask when it's the only left
|
||||
if (
|
||||
mask.isEager() &&
|
||||
'inputType' in e &&
|
||||
e.inputType.startsWith('delete') &&
|
||||
mask.unmasked(input.value).length <= 1
|
||||
) {
|
||||
@@ -95,71 +83,50 @@ export class MaskInput {
|
||||
}
|
||||
|
||||
private readonly inputEvent = (e: Event | InputEvent): void => {
|
||||
if (
|
||||
e instanceof CustomEvent &&
|
||||
e.type === 'input' &&
|
||||
e.detail != null &&
|
||||
typeof e.detail === 'object' &&
|
||||
'masked' in e.detail
|
||||
) {
|
||||
if (e instanceof CustomEvent && e.type === 'input') {
|
||||
return
|
||||
}
|
||||
|
||||
const input = e.target as HTMLInputElement
|
||||
const mask = this.items.get(input) as Mask
|
||||
const valueOld = input.value
|
||||
const ss = input.selectionStart
|
||||
const se = input.selectionEnd
|
||||
let value = valueOld
|
||||
const selection = input.selectionStart
|
||||
let value = input.value
|
||||
|
||||
if (mask.isEager()) {
|
||||
const masked = mask.masked(valueOld)
|
||||
const unmasked = mask.unmasked(valueOld)
|
||||
const masked = mask.masked(value)
|
||||
const unmasked = mask.unmasked(value)
|
||||
const unmaskedMasked = mask.unmasked(masked)
|
||||
|
||||
if (unmasked === '' && 'data' in e && e.data != null) {
|
||||
// empty state and something like `space` pressed
|
||||
value = e.data
|
||||
} else if (unmasked !== mask.unmasked(masked)) {
|
||||
} else if (unmasked !== unmaskedMasked) {
|
||||
value = unmasked
|
||||
}
|
||||
}
|
||||
|
||||
this.setMaskedValue(input, value)
|
||||
this.updateCursor(e, selection, value)
|
||||
}
|
||||
|
||||
// set caret position
|
||||
if ('inputType' in e) {
|
||||
if (
|
||||
e.inputType.startsWith('delete') ||
|
||||
(ss != null && ss < valueOld.length)
|
||||
) {
|
||||
try {
|
||||
// see https://github.com/beholdr/maska/issues/118
|
||||
input.setSelectionRange(ss, se)
|
||||
} catch {}
|
||||
private updateCursor(e: Event | InputEvent, s: number | null, value: string) {
|
||||
if (!('inputType' in e) || s === null) return
|
||||
|
||||
const input = e.target as HTMLInputElement
|
||||
|
||||
if (e.inputType.startsWith('delete') || (s != null && s < value.length)) {
|
||||
try {
|
||||
input.setSelectionRange(s, s)
|
||||
} catch {
|
||||
// see https://github.com/beholdr/maska/issues/118
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setMaskedValue (input: HTMLInputElement, value: string): void {
|
||||
const mask = this.items.get(input) as Mask
|
||||
private setMaskedValue(input: HTMLInputElement, value: string): void {
|
||||
const detail = this.process(input, value)
|
||||
|
||||
if (this.options.preProcess != null) {
|
||||
value = this.options.preProcess(value)
|
||||
}
|
||||
|
||||
const masked = mask.masked(value)
|
||||
const unmasked = mask.unmasked(mask.isEager() ? masked : value)
|
||||
const completed = mask.completed(value)
|
||||
const detail = { masked, unmasked, completed }
|
||||
|
||||
value = masked
|
||||
|
||||
if (this.options.postProcess != null) {
|
||||
value = this.options.postProcess(value)
|
||||
}
|
||||
|
||||
input.value = value
|
||||
input.dataset.maskaValue = value
|
||||
input.value = detail.masked
|
||||
|
||||
if (this.options.onMaska != null) {
|
||||
if (Array.isArray(this.options.onMaska)) {
|
||||
@@ -168,7 +135,27 @@ export class MaskInput {
|
||||
this.options.onMaska(detail)
|
||||
}
|
||||
}
|
||||
|
||||
input.dispatchEvent(new CustomEvent<MaskaDetail>('maska', { detail }))
|
||||
input.dispatchEvent(new CustomEvent<MaskaDetail>('input', { detail }))
|
||||
input.dispatchEvent(new CustomEvent('input', { detail: detail.masked }))
|
||||
}
|
||||
|
||||
private process(input: HTMLInputElement, value?: string): MaskaDetail {
|
||||
const mask = this.items.get(input) as Mask
|
||||
let valueNew = value ?? input.value
|
||||
|
||||
if (this.options.preProcess != null) {
|
||||
valueNew = this.options.preProcess(valueNew)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user