mirror of
https://github.com/tenrok/maska.git
synced 2026-06-11 18:02:27 +03:00
New version code prepare
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { VueLive } from 'vue-live'
|
||||
import 'vue-live/style.css'
|
||||
|
||||
const examples = [
|
||||
{
|
||||
label: 'Simple mask',
|
||||
code: `<input v-maska data-maska="#-#" value="12">`
|
||||
},
|
||||
{
|
||||
label: 'Phone mask',
|
||||
code: `<input v-maska data-maska="+1 ### ###-##-##">`
|
||||
},
|
||||
{
|
||||
label: 'HEX-color',
|
||||
code: `<input\n v-maska\n data-maska="!#HHHHHH"\n data-maska-tokens="H:[0-9a-fA-F]"\n>`
|
||||
},
|
||||
{
|
||||
label: 'IP address with optional digits',
|
||||
code: `<input\n v-maska\n data-maska="100.100.100.100"\n data-maska-tokens="1:[0-2]|0:[0-9]:optional"\n>`
|
||||
},
|
||||
{
|
||||
label: 'Dynamic mask: CPF/CNPJ',
|
||||
code: `<input\n v-maska\n data-maska="[\n '###.###.###-##',\n '##.###.###/####-##'\n ]"\n>`
|
||||
},
|
||||
{
|
||||
label: 'Cardholder name: via hook',
|
||||
code: `const options = {\n preProcess: (val) => val.toUpperCase()\n}\n\n<input\n v-maska:[options]\n data-maska="A A"\n data-maska-tokens="A:[A-Z]:multiple"\n>`
|
||||
},
|
||||
{
|
||||
label: 'Cardholder name: via token transform',
|
||||
code: `const options = {\n tokens: {\n A: {\n pattern: /[A-Z]/,\n multiple: true,\n transform: (chr) => chr.toUpperCase()\n }\n }\n}\n\n<input v-maska:[options] data-maska="A A">`
|
||||
},
|
||||
{
|
||||
label: 'Year: with current year as a limit',
|
||||
code: `const options = {\n postProcess: (val) => {\n const max = "" + new Date().getFullYear()\n return val > max ? max : val\n }\n}\n\n<input v-maska:[options] data-maska="####">`
|
||||
},
|
||||
{
|
||||
label: 'Money format: repeated and reversed',
|
||||
code: `<input\n v-maska\n data-maska="9 99#,##"\n data-maska-tokens="9:[0-9]:repeated"\n data-maska-reversed\n>`
|
||||
}
|
||||
]
|
||||
|
||||
const selectedExample = ref(0)
|
||||
const code = computed(() => examples[selectedExample.value].code)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="demo-select">
|
||||
<label for="demo-example-select">Choose mask example:</label>
|
||||
<select v-model="selectedExample" id="demo-example-select">
|
||||
<option v-for="(example, idx) in examples" :value="idx">
|
||||
{{ example.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<VueLive :code="code" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.demo-select {
|
||||
border: 1px solid var(--docsifytabs-border-color);
|
||||
padding: 1rem var(--docsifytabs-content-padding) 1.5rem;
|
||||
border-bottom: none;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
.demo-select > label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--search-input-placeholder-color);
|
||||
}
|
||||
.demo-select > select {
|
||||
width: 100%;
|
||||
padding: 7px;
|
||||
border-radius: 4px;
|
||||
color: var(--search-input-color);
|
||||
border: 1px solid var(--search-input-border-color);
|
||||
background-color: var(--search-input-background-color);
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
appearance: none;
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
}
|
||||
|
||||
.VueLive-container {
|
||||
border: 1px solid var(--docsifytabs-border-color);
|
||||
margin: var(--docsifytabs-margin);
|
||||
margin-top: 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
.VueLive-container .VueLive-editor {
|
||||
padding: 2.3rem var(--docsifytabs-content-padding);
|
||||
background-color: var(--code-theme-background);
|
||||
border-radius: 0 0 0 5px;
|
||||
width: 85%;
|
||||
position: relative;
|
||||
}
|
||||
.VueLive-container .VueLive-editor::after {
|
||||
content: 'Сode';
|
||||
position: absolute;
|
||||
top: 0.75em;
|
||||
right: 0.75em;
|
||||
opacity: 0.6;
|
||||
color: inherit;
|
||||
font-size: var(--font-size-s);
|
||||
line-height: 1;
|
||||
}
|
||||
.VueLive-container .VueLive-editor .prism-editor__editor,
|
||||
.VueLive-container .VueLive-editor .prism-editor__textarea {
|
||||
font-family: var(--code-font-family);
|
||||
}
|
||||
.VueLive-container .VueLivePreview {
|
||||
padding: 2rem var(--docsifytabs-content-padding);
|
||||
border-radius: 0 0 5px 0;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
.VueLive-container .VueLivePreview::after {
|
||||
content: 'Result';
|
||||
position: absolute;
|
||||
top: 0.75em;
|
||||
right: var(--docsifytabs-content-padding);
|
||||
opacity: 0.6;
|
||||
color: inherit;
|
||||
font-size: var(--font-size-s);
|
||||
line-height: 1;
|
||||
}
|
||||
.VueLive-container .VueLivePreview input {
|
||||
font-size: var(--modular-scale-1);
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--search-input-border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--search-input-background-color);
|
||||
color: var(--search-input-color);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.VueLive-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.VueLive-container .VueLive-editor,
|
||||
.VueLive-container .VueLivePreview {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import { vMaska } from '..'
|
||||
import Demo from './Demo.vue'
|
||||
|
||||
createApp(Demo)
|
||||
.directive('maska', vMaska)
|
||||
.mount('#demo-app')
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import Maska from './maska'
|
||||
import { isString } from './utils'
|
||||
|
||||
function getOpts (mask) {
|
||||
const opts = {}
|
||||
|
||||
if (mask.mask) {
|
||||
opts.mask = Array.isArray(mask.mask) ? JSON.stringify(mask.mask) : mask.mask
|
||||
opts.tokens = mask.tokens ? { ...mask.tokens } : {}
|
||||
opts.preprocessor = mask.preprocessor
|
||||
} else {
|
||||
opts.mask = Array.isArray(mask) ? JSON.stringify(mask) : mask
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
function needUpdate (mask) {
|
||||
return !(
|
||||
(isString(mask.value) && mask.value === mask.oldValue) ||
|
||||
(Array.isArray(mask.value) && JSON.stringify(mask.value) === JSON.stringify(mask.oldValue)) ||
|
||||
(mask.value && mask.value.mask && mask.oldValue && mask.oldValue.mask && mask.value.mask === mask.oldValue.mask)
|
||||
)
|
||||
}
|
||||
|
||||
const directive = () => {
|
||||
const state = new WeakMap()
|
||||
|
||||
return (el, mask) => {
|
||||
if (!mask.value) return
|
||||
|
||||
if (state.has(el) && !needUpdate(mask)) {
|
||||
return
|
||||
}
|
||||
|
||||
state.set(el, new Maska(el, getOpts(mask.value)))
|
||||
}
|
||||
}
|
||||
|
||||
export default directive()
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Directive } from 'vue'
|
||||
import { MaskaDetail, MaskInput, MaskInputOptions } from './mask-input'
|
||||
|
||||
type MaskaDirective = Directive<HTMLInputElement, MaskaDetail | undefined>
|
||||
|
||||
const masks = new WeakMap<HTMLInputElement, MaskInput>()
|
||||
|
||||
export const vMaska: MaskaDirective = (el, binding) => {
|
||||
if (masks.get(el) != null) {
|
||||
masks.get(el)?.destroy()
|
||||
}
|
||||
|
||||
const opts = { ...(binding.arg as MaskInputOptions) } ?? {}
|
||||
|
||||
if (binding.value != null) {
|
||||
const binded = binding.value
|
||||
opts.onMaska = (detail: MaskaDetail) => {
|
||||
binded.masked = detail.masked
|
||||
binded.unmasked = detail.unmasked
|
||||
binded.completed = detail.completed
|
||||
}
|
||||
}
|
||||
|
||||
masks.set(el, new MaskInput(el, opts))
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import directive from './directive'
|
||||
import mask from './mask'
|
||||
import Maska from './maska'
|
||||
import tokens from './tokens'
|
||||
|
||||
function install (Vue) {
|
||||
Vue.directive('maska', directive)
|
||||
}
|
||||
// Install by default if included from script tag (only Vue 2)
|
||||
if (typeof window !== 'undefined' && window.Vue && window.Vue.use) {
|
||||
window.Vue.use(install)
|
||||
}
|
||||
|
||||
function create (el, options) {
|
||||
return new Maska(el, options)
|
||||
}
|
||||
|
||||
export default install
|
||||
export {
|
||||
install,
|
||||
create,
|
||||
mask,
|
||||
directive as maska,
|
||||
tokens
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Mask, MaskType, MaskOptions } from './mask'
|
||||
import { MaskInput, MaskInputOptions, MaskaDetail } from './mask-input'
|
||||
import { vMaska } from './directive'
|
||||
import { tokens, MaskTokens } from './tokens'
|
||||
|
||||
export { Mask, MaskInput, tokens, vMaska }
|
||||
export type {
|
||||
MaskaDetail,
|
||||
MaskInputOptions,
|
||||
MaskOptions,
|
||||
MaskTokens,
|
||||
MaskType
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { Mask, MaskOptions } from './mask'
|
||||
import { parseMask, parseOpts, parseTokens } from './parser'
|
||||
|
||||
export interface MaskInputOptions extends MaskOptions {
|
||||
onMaska?: (detail: MaskaDetail) => void
|
||||
preProcess?: (value: string) => string
|
||||
postProcess?: (value: string) => string
|
||||
}
|
||||
|
||||
export interface MaskaDetail {
|
||||
masked: string
|
||||
unmasked: string
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
export class MaskInput {
|
||||
readonly items = new Map<HTMLInputElement, Mask>()
|
||||
|
||||
constructor (
|
||||
target: string | NodeListOf<HTMLInputElement> | HTMLInputElement,
|
||||
readonly options: MaskInputOptions = {}
|
||||
) {
|
||||
const { onMaska, preProcess, postProcess, ...opts } = options
|
||||
|
||||
if (typeof target === 'string') {
|
||||
this.init(Array.from(document.querySelectorAll(target)), opts)
|
||||
} else {
|
||||
this.init('length' in target ? Array.from(target) : [target], opts)
|
||||
}
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
for (const input of this.items.keys()) {
|
||||
input.removeEventListener('input', this.inputEvent)
|
||||
input.removeEventListener('beforeinput', this.beforeinputEvent)
|
||||
}
|
||||
this.items.clear()
|
||||
}
|
||||
|
||||
private init (inputs: HTMLInputElement[], defaults: MaskOptions): void {
|
||||
for (const input of inputs) {
|
||||
const opts = { ...defaults }
|
||||
if (input.dataset.maska != null && input.dataset.maska !== '') {
|
||||
opts.mask = parseMask(input.dataset.maska)
|
||||
}
|
||||
if (input.dataset.maskaEager != null) {
|
||||
opts.eager = parseOpts(input.dataset.maskaEager)
|
||||
}
|
||||
if (input.dataset.maskaReversed != null) {
|
||||
opts.reversed = parseOpts(input.dataset.maskaReversed)
|
||||
}
|
||||
if (input.dataset.maskaTokensReplace != null) {
|
||||
opts.tokensReplace = parseOpts(input.dataset.maskaTokensReplace)
|
||||
}
|
||||
if (input.dataset.maskaTokens != null) {
|
||||
opts.tokens = parseTokens(input.dataset.maskaTokens)
|
||||
}
|
||||
|
||||
const mask = new Mask(opts)
|
||||
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 => {
|
||||
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.eager &&
|
||||
'inputType' in e &&
|
||||
e.inputType.startsWith('delete') &&
|
||||
mask.unmasked(input.value).length <= 1
|
||||
) {
|
||||
this.setMaskedValue(input, '')
|
||||
}
|
||||
}
|
||||
|
||||
private readonly inputEvent = (e: Event | InputEvent): void => {
|
||||
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
|
||||
|
||||
if (mask.eager) {
|
||||
const unmasked = mask.unmasked(valueOld)
|
||||
const maskedUnmasked = mask.masked(unmasked)
|
||||
|
||||
if (unmasked === '' && 'data' in e && e.data != null) {
|
||||
// empty state and something like `space` pressed
|
||||
value = e.data
|
||||
} else if (
|
||||
maskedUnmasked.startsWith(valueOld) ||
|
||||
mask.completed(unmasked)
|
||||
) {
|
||||
value = unmasked
|
||||
}
|
||||
}
|
||||
|
||||
this.setMaskedValue(input, value)
|
||||
|
||||
// set caret position
|
||||
if ('inputType' in e) {
|
||||
if (
|
||||
e.inputType.startsWith('delete') ||
|
||||
(ss != null && ss < valueOld.length)
|
||||
) {
|
||||
input.setSelectionRange(ss, se)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setMaskedValue (input: HTMLInputElement, value: string): void {
|
||||
const mask = this.items.get(input) as Mask
|
||||
|
||||
if (this.options.preProcess != null) {
|
||||
value = this.options.preProcess(value)
|
||||
}
|
||||
|
||||
value = mask.masked(value)
|
||||
|
||||
if (this.options.postProcess != null) {
|
||||
value = this.options.postProcess(value)
|
||||
}
|
||||
|
||||
input.value = value
|
||||
|
||||
const detail = {
|
||||
masked: mask.masked(value),
|
||||
unmasked: mask.unmasked(value),
|
||||
completed: mask.completed(value)
|
||||
}
|
||||
|
||||
if (this.options.onMaska != null) {
|
||||
this.options.onMaska(detail)
|
||||
}
|
||||
input.dispatchEvent(new CustomEvent<MaskaDetail>('maska', { detail }))
|
||||
}
|
||||
}
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
import defaultTokens from './tokens'
|
||||
|
||||
export default function mask (value, mask, tokens = defaultTokens, masked = true) {
|
||||
return (processMask(mask).length > 1)
|
||||
? dynamic(mask)(value, mask, tokens, masked)
|
||||
: process(value, mask, tokens, masked)
|
||||
}
|
||||
|
||||
function processMask (mask) {
|
||||
try {
|
||||
return JSON.parse(mask)
|
||||
} catch {
|
||||
return [mask]
|
||||
}
|
||||
}
|
||||
|
||||
function dynamic (mask) {
|
||||
const masks = processMask(mask).sort((a, b) => a.length - b.length)
|
||||
|
||||
return function (value, mask, tokens, masked = true) {
|
||||
const processed = masks.map(m => process(value, m, tokens, false))
|
||||
const last = processed.pop()
|
||||
|
||||
for (const i in masks) {
|
||||
if (checkMask(last, masks[i], tokens)) {
|
||||
return process(value, masks[i], tokens, masked)
|
||||
}
|
||||
}
|
||||
|
||||
return '' // empty masks
|
||||
}
|
||||
|
||||
function checkMask (variant, mask, tokens) {
|
||||
for (const tok in tokens) {
|
||||
if (tokens[tok].escape) {
|
||||
mask = mask.replace(new RegExp(tok + '.{1}', 'g'), '')
|
||||
}
|
||||
}
|
||||
|
||||
return (mask.split('').filter(el => tokens[el] && tokens[el].pattern).length >= variant.length)
|
||||
}
|
||||
}
|
||||
|
||||
function process (value, mask, tokens, masked = true) {
|
||||
let im = 0
|
||||
let iv = 0
|
||||
let ret = ''
|
||||
let rest = ''
|
||||
|
||||
while (im < mask.length && iv < value.length) {
|
||||
let maskChar = mask[im]
|
||||
const valueChar = value[iv]
|
||||
const token = tokens[maskChar]
|
||||
|
||||
if (token && token.pattern) {
|
||||
if (token.pattern.test(valueChar)) {
|
||||
ret += tokenTransform(valueChar, token)
|
||||
im++
|
||||
// check next char
|
||||
if (masked && mask[im]) {
|
||||
if (!tokens[mask[im]]) {
|
||||
ret += mask[im]
|
||||
im++
|
||||
} else if (tokens[mask[im]] && tokens[mask[im]].escape) {
|
||||
ret += mask[im + 1]
|
||||
im = im + 2
|
||||
}
|
||||
}
|
||||
}
|
||||
iv++
|
||||
} else if (token && token.repeat) {
|
||||
const tokenPrev = tokens[mask[im - 1]]
|
||||
if (tokenPrev && !tokenPrev.pattern.test(valueChar)) {
|
||||
im++
|
||||
} else {
|
||||
im--
|
||||
}
|
||||
} else {
|
||||
if (token && token.escape) {
|
||||
im++
|
||||
maskChar = mask[im]
|
||||
}
|
||||
if (masked) ret += maskChar
|
||||
if (valueChar === maskChar) iv++
|
||||
im++
|
||||
}
|
||||
}
|
||||
|
||||
// fix mask that ends with parenthesis
|
||||
while (masked && im < mask.length) { // eslint-disable-line no-unmodified-loop-condition
|
||||
const maskCharRest = mask[im]
|
||||
if (tokens[maskCharRest]) {
|
||||
rest = ''
|
||||
break
|
||||
}
|
||||
rest += maskCharRest
|
||||
im++
|
||||
}
|
||||
|
||||
return ret + rest
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} value
|
||||
* @param {'uppercase' | 'lowercase' | 'transform'} token
|
||||
*/
|
||||
function tokenTransform (value, token) {
|
||||
if (token.transform) {
|
||||
value = token.transform(value)
|
||||
}
|
||||
|
||||
if (token.uppercase) {
|
||||
return value.toLocaleUpperCase()
|
||||
} else if (token.lowercase) {
|
||||
return value.toLocaleLowerCase()
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import { MaskTokens, tokens } from './tokens'
|
||||
|
||||
export type MaskType = string | string[] | ((input: string) => string)
|
||||
|
||||
export interface MaskOptions {
|
||||
mask?: MaskType
|
||||
tokens?: MaskTokens
|
||||
tokensReplace?: boolean
|
||||
eager?: boolean
|
||||
reversed?: boolean
|
||||
}
|
||||
|
||||
export class Mask {
|
||||
readonly mask: MaskType = ''
|
||||
readonly tokens = tokens
|
||||
readonly eager = false
|
||||
readonly reversed = false
|
||||
private readonly memo = new Map()
|
||||
|
||||
constructor (opts: MaskOptions = {}) {
|
||||
if (opts.tokens != null) {
|
||||
opts.tokens = (opts.tokensReplace as boolean)
|
||||
? { ...opts.tokens }
|
||||
: { ...tokens, ...opts.tokens }
|
||||
|
||||
for (const token of Object.values(opts.tokens)) {
|
||||
if (typeof token.pattern === 'string') {
|
||||
token.pattern = new RegExp(token.pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.mask == null) {
|
||||
opts.mask = ''
|
||||
} else if (typeof opts.mask === 'object') {
|
||||
if (opts.mask.length > 1) {
|
||||
opts.mask.sort((a, b) => a.length - b.length)
|
||||
} else {
|
||||
opts.mask = opts.mask[0] ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(this, opts)
|
||||
}
|
||||
|
||||
masked (value: string): string {
|
||||
return this.process(value, this.findMask(value))
|
||||
}
|
||||
|
||||
unmasked (value: string): string {
|
||||
return this.process(value, this.findMask(value), false)
|
||||
}
|
||||
|
||||
completed (value: string): boolean {
|
||||
const length = this.process(value, this.findMask(value)).length
|
||||
|
||||
if (typeof this.mask === 'string') {
|
||||
return length >= this.mask.length
|
||||
} else if (typeof this.mask === 'function') {
|
||||
return length >= this.findMask(value).length
|
||||
} else {
|
||||
return (
|
||||
this.mask.filter((m) => length >= m.length).length === this.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)
|
||||
}
|
||||
|
||||
const last = this.process(value, this.mask.slice(-1).pop() ?? '', false)
|
||||
|
||||
return (
|
||||
this.mask.find(
|
||||
(mask) => this.process(value, mask, false).length >= last.length
|
||||
) ?? ''
|
||||
)
|
||||
}
|
||||
|
||||
private escapeMask (maskRaw: string): {
|
||||
mask: string
|
||||
escaped: number[]
|
||||
} {
|
||||
const chars: string[] = []
|
||||
const escaped: number[] = []
|
||||
maskRaw.split('').forEach((ch, i) => {
|
||||
if (ch === '!' && maskRaw[i - 1] !== '!') {
|
||||
escaped.push(i - escaped.length)
|
||||
} else {
|
||||
chars.push(ch)
|
||||
}
|
||||
})
|
||||
|
||||
return { mask: chars.join(''), escaped }
|
||||
}
|
||||
|
||||
private process (value: string, maskRaw: string, masked = true): string {
|
||||
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 check = this.reversed
|
||||
? () => m > -1 && v > -1
|
||||
: () => m < mask.length && v < value.length
|
||||
|
||||
const notLastMaskChar = (m: number): boolean =>
|
||||
(!this.reversed && m <= lastMaskChar) ||
|
||||
(this.reversed && m >= lastMaskChar)
|
||||
|
||||
let lastRawMaskChar
|
||||
let repeatedPos = -1
|
||||
let m = this.reversed ? mask.length - 1 : 0
|
||||
let v = this.reversed ? value.length - 1 : 0
|
||||
|
||||
while (check()) {
|
||||
const maskChar = mask.charAt(m)
|
||||
const token = this.tokens[maskChar]
|
||||
const valueChar =
|
||||
token?.transform != null
|
||||
? token.transform(value.charAt(v))
|
||||
: value.charAt(v)
|
||||
|
||||
if (!escaped.includes(m) && token != null) {
|
||||
if (valueChar.match(token.pattern) != null) {
|
||||
result[method](valueChar)
|
||||
|
||||
if (token.repeated as boolean) {
|
||||
if (repeatedPos === -1) {
|
||||
repeatedPos = m
|
||||
} else if (m === lastMaskChar && m !== repeatedPos) {
|
||||
m = repeatedPos - offset
|
||||
}
|
||||
|
||||
if (lastMaskChar === repeatedPos) {
|
||||
m -= offset
|
||||
}
|
||||
} else if (token.multiple as boolean) {
|
||||
m -= offset
|
||||
}
|
||||
|
||||
m += offset
|
||||
} 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) {
|
||||
m += offset
|
||||
v -= offset
|
||||
} else {
|
||||
result[method]('')
|
||||
}
|
||||
} else if (valueChar === lastRawMaskChar) {
|
||||
// matched the last untranslated (raw) mask character that we encountered
|
||||
// likely an insert offset the mask character from the last entry;
|
||||
// fall through and only increment v
|
||||
lastRawMaskChar = undefined
|
||||
} else if (token.optional as boolean) {
|
||||
m += offset
|
||||
v -= offset
|
||||
} else {
|
||||
// invalid input
|
||||
}
|
||||
|
||||
v += offset
|
||||
} else {
|
||||
if (masked && !this.eager) {
|
||||
result[method](maskChar)
|
||||
}
|
||||
|
||||
if (valueChar === maskChar && !this.eager) {
|
||||
v += offset
|
||||
} else {
|
||||
lastRawMaskChar = maskChar
|
||||
}
|
||||
|
||||
if (!this.eager) {
|
||||
m += offset
|
||||
}
|
||||
}
|
||||
|
||||
if (this.eager) {
|
||||
while (
|
||||
notLastMaskChar(m) &&
|
||||
(this.tokens[mask.charAt(m)] == null || escaped.includes(m))
|
||||
) {
|
||||
if (masked) {
|
||||
result[method](mask.charAt(m))
|
||||
} else if (mask.charAt(m) === value.charAt(v)) {
|
||||
v += offset
|
||||
}
|
||||
m += offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.memo.set(key, result.join(''))
|
||||
|
||||
return this.memo.get(key)
|
||||
}
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
import mask from './mask'
|
||||
import tokens from './tokens'
|
||||
import { event, findInputElement, fixInputSelection, isString } from './utils'
|
||||
|
||||
export default class Maska {
|
||||
constructor (el, opts = {}) {
|
||||
if (!el) throw new Error('Maska: no element for mask')
|
||||
|
||||
if (opts.preprocessor != null && typeof opts.preprocessor !== 'function') {
|
||||
throw new Error('Maska: preprocessor must be a function')
|
||||
}
|
||||
|
||||
if (opts.tokens) {
|
||||
for (const i in opts.tokens) {
|
||||
opts.tokens[i] = { ...opts.tokens[i] }
|
||||
if (opts.tokens[i].pattern && isString(opts.tokens[i].pattern)) {
|
||||
opts.tokens[i].pattern = new RegExp(opts.tokens[i].pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._opts = {
|
||||
mask: opts.mask,
|
||||
tokens: { ...tokens, ...opts.tokens },
|
||||
preprocessor: opts.preprocessor
|
||||
}
|
||||
this._el = isString(el) ? document.querySelectorAll(el) : !el.length ? [el] : el
|
||||
this.inputEvent = (e) => this.updateValue(e.target, e)
|
||||
|
||||
this.init()
|
||||
}
|
||||
|
||||
init () {
|
||||
for (let i = 0; i < this._el.length; i++) {
|
||||
const el = findInputElement(this._el[i])
|
||||
if (this._opts.mask && (!el.dataset.mask || el.dataset.mask !== this._opts.mask)) {
|
||||
el.dataset.mask = this._opts.mask
|
||||
}
|
||||
setTimeout(() => this.updateValue(el), 0)
|
||||
if (!el.dataset.maskInited) {
|
||||
el.dataset.maskInited = true
|
||||
el.addEventListener('input', this.inputEvent)
|
||||
el.addEventListener('beforeinput', this.beforeInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
for (let i = 0; i < this._el.length; i++) {
|
||||
const el = findInputElement(this._el[i])
|
||||
el.removeEventListener('input', this.inputEvent)
|
||||
el.removeEventListener('beforeinput', this.beforeInput)
|
||||
delete el.dataset.mask
|
||||
delete el.dataset.maskInited
|
||||
}
|
||||
}
|
||||
|
||||
updateValue (el, evt) {
|
||||
if (!el || !el.type) return
|
||||
|
||||
const wrongNum = el.type.match(/^number$/i) && el.validity.badInput
|
||||
if ((!el.value && !wrongNum) || !el.dataset.mask) {
|
||||
el.dataset.maskRawValue = ''
|
||||
this.dispatch('maska', el, evt)
|
||||
return
|
||||
}
|
||||
|
||||
let position = el.selectionEnd
|
||||
const oldValue = el.value
|
||||
const digit = oldValue[position - 1]
|
||||
|
||||
el.dataset.maskRawValue = mask(el.value, el.dataset.mask, this._opts.tokens, false)
|
||||
let elValue = el.value
|
||||
|
||||
if (this._opts.preprocessor) {
|
||||
elValue = this._opts.preprocessor(elValue)
|
||||
}
|
||||
|
||||
el.value = mask(elValue, el.dataset.mask, this._opts.tokens)
|
||||
|
||||
if (evt && evt.inputType === 'insertText' && position === oldValue.length) {
|
||||
position = el.value.length
|
||||
}
|
||||
fixInputSelection(el, position, digit)
|
||||
|
||||
this.dispatch('maska', el, evt)
|
||||
if (el.value !== oldValue) {
|
||||
this.dispatch('input', el, evt)
|
||||
}
|
||||
}
|
||||
|
||||
beforeInput (e) {
|
||||
if (e && e.target && e.target.type && e.target.type.match(/^number$/i) && e.data && isNaN(e.target.value + e.data)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
dispatch (name, el, evt) {
|
||||
el.dispatchEvent(event(name, (evt && evt.inputType) || null))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MaskType } from './mask'
|
||||
import { MaskTokens } from './tokens'
|
||||
|
||||
const parseJson = (value: string): any => JSON.parse(value.replaceAll("'", '"'))
|
||||
|
||||
export const parseOpts = (value: string): boolean =>
|
||||
value !== '' ? Boolean(JSON.parse(value)) : true
|
||||
|
||||
export const parseMask = (value: string): MaskType =>
|
||||
value.startsWith('[') && value.endsWith(']') ? parseJson(value) : value
|
||||
|
||||
export const parseTokens = (value: string): MaskTokens => {
|
||||
if (value.startsWith('{') && value.endsWith('}')) {
|
||||
return parseJson(value)
|
||||
}
|
||||
|
||||
const tokens: MaskTokens = {}
|
||||
value.split('|').forEach((token) => {
|
||||
const parts = token.split(':')
|
||||
tokens[parts[0]] = {
|
||||
pattern: new RegExp(parts[1]),
|
||||
optional: parts[2] === 'optional',
|
||||
multiple: parts[2] === 'multiple',
|
||||
repeated: parts[2] === 'repeated'
|
||||
}
|
||||
})
|
||||
|
||||
return tokens
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint quote-props: ["error", "consistent"] */
|
||||
export default {
|
||||
'#': { pattern: /[0-9]/ },
|
||||
'X': { pattern: /[0-9a-zA-Z]/ },
|
||||
'S': { pattern: /[a-zA-Z]/ },
|
||||
'A': { pattern: /[a-zA-Z]/, uppercase: true },
|
||||
'a': { pattern: /[a-zA-Z]/, lowercase: true },
|
||||
'!': { escape: true },
|
||||
'*': { repeat: true }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
interface MaskToken {
|
||||
pattern: RegExp
|
||||
multiple?: boolean
|
||||
optional?: boolean
|
||||
repeated?: boolean
|
||||
transform?: (char: string) => string
|
||||
}
|
||||
|
||||
export type MaskTokens = Record<string, MaskToken>
|
||||
|
||||
export const tokens: MaskTokens = {
|
||||
'#': { pattern: /[0-9]/ },
|
||||
'@': { pattern: /[a-zA-Z]/ },
|
||||
'*': { pattern: /[a-zA-Z0-9]/ }
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/* global HTMLInputElement */
|
||||
|
||||
function event (name, inputType = null) {
|
||||
const event = document.createEvent('Event')
|
||||
event.initEvent(name, true, true)
|
||||
if (inputType) {
|
||||
event.inputType = inputType
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
function findInputElement (el) {
|
||||
return (el instanceof HTMLInputElement) ? el : el.querySelector('input') || el
|
||||
}
|
||||
|
||||
function fixInputSelection (el, position, digit) {
|
||||
while (position && position < el.value.length && el.value.charAt(position - 1) !== digit) {
|
||||
position++
|
||||
}
|
||||
|
||||
const selectionRange = el.type ? el.type.match(/^(text|search|password|tel|url)$/i) : !el.type
|
||||
if (selectionRange && el === document.activeElement) {
|
||||
el.setSelectionRange(position, position)
|
||||
setTimeout(function () {
|
||||
el.setSelectionRange(position, position)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function isString (val) {
|
||||
return Object.prototype.toString.call(val) === '[object String]'
|
||||
}
|
||||
|
||||
export {
|
||||
event,
|
||||
findInputElement,
|
||||
fixInputSelection,
|
||||
isString
|
||||
}
|
||||
Reference in New Issue
Block a user