Open thamibn opened 3 years ago
I've done some experiments and find it's not easy to integrate it whether inside or outside the library.
The main problems are that they don't offer a DOM independant API and the handling of selection range issues of the input element.
However this is a somehow useful feature. I'll try to make it work later.
Might not be the optimal solution, but vue-the-mask offers a directive, that can easily be ported to Vue3 and is indeed working (at least for my purposes) with the existing NInput element. With a wrapper component and some tweaking, it's also possible to change between masked/unmasked value (while the displayed value is masked) and also get selection persistence.
./directives/mask.js
function event(name, detail) {
const evt = new Event(name, { bubbles: true, cancelable: true })
return evt
}
function dynamicMask(maskit, masks, tokens) {
masks = masks.sort((a, b) => a.length - b.length)
return function (value, mask, masked = true) {
let i = 0
while (i < masks.length) {
const currentMask = masks[i]
i++
const nextMask = masks[i]
if (!(nextMask && maskit(value, nextMask, true, tokens).length > currentMask.length)) {
return maskit(value, currentMask, masked, tokens)
}
}
return '' // empty masks
}
}
function maskit(value, mask, masked = true, tokens) {
value = value || ''
mask = mask || ''
let iMask = 0
let iValue = 0
let output = ''
while (iMask < mask.length && iValue < value.length) {
let cMask = mask[iMask]
const masker = tokens[cMask]
const cValue = value[iValue]
if (masker && !masker.escape) {
if (masker.pattern.test(cValue)) {
output += masker.transform ? masker.transform(cValue) : cValue
iMask++
}
iValue++
} else {
if (masker && masker.escape) {
iMask++ // take the next mask char and treat it as char
cMask = mask[iMask]
}
if (masked) output += cMask
if (cValue === cMask) iValue++ // user typed the same char
iMask++
}
}
// fix mask that ends with a char: (#)
let restOutput = ''
while (iMask < mask.length && masked) {
const cMask = mask[iMask]
if (tokens[cMask]) {
restOutput = ''
break
}
restOutput += cMask
iMask++
}
return output + restOutput
}
export function masker(value, mask, masked = true, tokens) {
return Array.isArray(mask)
? dynamicMask(maskit, mask, tokens)(value, mask, masked, tokens)
: maskit(value, mask, masked, tokens)
}
export const tokens = {
'#': { pattern: /\d/ },
X: { pattern: /[0-9a-zA-Z]/ },
S: { pattern: /[a-zA-Z]/ },
A: { pattern: /[a-zA-Z]/, transform: v => v.toLocaleUpperCase() },
H: { pattern: /[0-9a-fA-F]/, transform: v => v.toLocaleUpperCase() },
a: { pattern: /[a-zA-Z]/, transform: v => v.toLocaleLowerCase() },
'!': { escape: true }
}
export default function (el, binding, vnode) {
let config = binding.value
if (Array.isArray(config) || typeof config === 'string') {
config = {
mask: config,
tokens: tokens
}
} else return
if (el.tagName.toLocaleUpperCase() !== 'INPUT') {
const els = el.getElementsByTagName('input')
if (els.length !== 1) {
throw new Error("v-mask directive requires 1 input, found " + els.length)
} else {
el = els[0]
}
}
let position = el.selectionEnd
const digit = el.value[position - 1]
const newDisplay = masker(el.value, config.mask, true, config.tokens)
if (newDisplay !== el.value) {
el.value = newDisplay
el.dispatchEvent(event('input'))
while (position < el.value.length && el.value.charAt(position - 1) !== digit) {
position++
}
if (el === document.activeElement) {
el.setSelectionRange(position, position)
setTimeout(function () {
el.setSelectionRange(position, position)
}, 0)
}
}
}
./main.js
import mask from './directives/mask'
app.directive('mask', mask)
./components/MaskedInput.vue
<template>
<n-input :value="display" v-mask="mask" @input="refresh" />
</template>
<script setup>
import { masker, tokens } from "@/directives/mask";
const emit = defineEmits(["update:value"]);
const props = defineProps({
value: [String, Number],
mask: {
type: [String, Array],
required: true,
},
masked: {
type: Boolean,
default: false,
},
});
const { mask, masked, value } = toRefs(props);
const display = ref(value.value);
const lastValue = ref(null);
watch(
() => value.value,
(newValue) => {
if (newValue !== lastValue.value) {
display.value = newValue;
}
}
);
watch(
() => masked.value,
() => refresh(display.value)
);
const refresh = (value) => {
display.value = value;
const val = masker(value, mask.value, masked.value, tokens);
if (val !== lastValue.value) {
lastValue.value = val;
emit("update:value", val);
}
};
</script>
Use somewhere
<masked-input v-model:value="value" :mask="'##/##/####'" />
Hey @B3nsten did you get it to work somehow? I'm in a project that demands masks, including date and currency.
Hey @B3nsten did you get it to work somehow? I'm in a project that demands masks, including date and currency.
The posted code was working. What's erroring for you?
I'll give it a try, now! Have to adapt a bit because Im working with Nuxt 3!
It worked ❤️! Now I'm missing currency mask. Any chance you have it already?
Well, depending on your needs you might get away with something like:
<masked-input v-model:value="value" :mask="['#.##', '##.##', '###.##']" />
You'd have to change a lot of things if you need negative numbers, want to handle the money prefix inside the input (I'd use a naive input group, tho), etc. This approach is not the best for handling variable input lengths and in this state is not suitable to handle anything but strings.
Thanks a bunch! I've been working on this feature for a week! You saved me!
Hello. There is an easier way, it is not necessary to create a new MaskedInput component, you can use the package https://github.com/beholdr/maska , and to set the mask, use as
<n-input placeholder="Phone number *" v-model:value="form.phone" :input-props="{ vMaska: true, 'data-mask': '+# ### ###-##-##' }"/>
@rom-rzic does it work? Tried but nothing happened
Hello. There is an easier way, it is not necessary to create a new MaskedInput component, you can use the package https://github.com/beholdr/maska , and to set the mask, use as
<n-input placeholder="Phone number *" v-model:value="form.phone" :input-props="{ vMaska: true, 'data-mask': '+# ### ###-##-##' }"/>
Hello! Could you show how register vMaska in component? I have tried your example but in doesn't work. I register maska in script tag import { vMaska } from "maska"
Hello. There is an easier way, it is not necessary to create a new MaskedInput component, you can use the package https://github.com/beholdr/maska , and to set the mask, use as
<n-input placeholder="Phone number *" v-model:value="form.phone" :input-props="{ vMaska: true, 'data-mask': '+# ### ###-##-##' }"/>
Hello! Could you show how register vMaska in component? I have tried your example but in doesn't work. I register maska in script tag import { vMaska } from "maska"
I found the solution!
<n-input
v-model:value="formData.user.phone"
placeholder="+7 700 777 77 77"
v-maska
:input-props="{
'data-maska': '+# ### ### ## ##',
}"
/>
The reason why @rom-rzic example didn't work because 'data-mask'
and he didn't set v-maska
directive in n-input
also you don't have to set vMaska: true
in input-props
. You have to use 'data-maska'
and don't forget to set v-maska
directive!
Welcome 🎉
@rom-rzic does it work? Tried but nothing happened
Solution https://github.com/tusen-ai/naive-ui/issues/1385#issuecomment-1728124131
someone got it working for n-input pair?
This function solves the problem (这个功能解决的问题)
Avoids users to enter wrong data from the get go
Expected API (期望的 API)
This will allow us to mask inputs to specific format e.g (##)-(####)
Means a user will enter the data in that format.
Find below pure JavaScript example https://imask.js.org/