├── .browserslistrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── build ├── release.sh └── rollup.config.js ├── dist ├── vue-mask.esm.js ├── vue-mask.min.js └── vue-mask.ssr.js ├── package-lock.json ├── package.json ├── src-doc ├── App.vue ├── InputCode.vue └── main.js ├── src ├── directive.js ├── entry.esm.js ├── entry.js ├── helpers.js ├── index.js ├── masker.js └── masks │ ├── cep.js │ ├── cnpj.js │ ├── cpf.js │ ├── credit-card.js │ ├── date.js │ ├── decimal.js │ ├── hour.js │ ├── index.js │ ├── mask.js │ ├── number.js │ └── phone.js └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | current node 2 | last 2 versions and > 2% 3 | ie > 10 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | npm-debug.log 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-present Devindex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Vue mask 2 | ============== 3 | 4 | Input mask lib for Vue.js based on [String-Mask](https://github.com/the-darc/string-mask) 5 | 6 | ## Installation 7 | 8 | This version only works in Vue 3. 9 | 10 | ```bash 11 | # npm 12 | $ npm i -S @devindex/vue-mask 13 | 14 | # yarn 15 | $ yarn add @devindex/vue-mask 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```javascript 21 | import { createApp } from 'vue'; 22 | import VueMask from '@devindex/vue-mask'; // <-- ADD THIS LINE 23 | import App from './App.vue'; 24 | 25 | const app = createApp(App); 26 | 27 | app.use(VueMask); // <-- ADD THIS LINE 28 | 29 | app.mount('#app'); 30 | ``` 31 | 32 | Basic usage on HTML input element 33 | 34 | ```html 35 | 36 | ``` 37 | 38 | ## Available masks 39 | 40 | ### v-mask 41 | 42 | ```html 43 | 44 | ``` 45 | 46 | ### v-mask-date (us|br) 47 | 48 | ```html 49 | 50 | ``` 51 | 52 | ### v-mask-phone (us|br) 53 | 54 | ```html 55 | 56 | ``` 57 | 58 | ### v-mask-decimal (us|br) 59 | 60 | ```html 61 | 62 | ``` 63 | 64 | ### v-mask-number 65 | 66 | ```html 67 | 68 | ``` 69 | 70 | ### v-mask-cpf 71 | 72 | ```html 73 | 74 | ``` 75 | 76 | ### v-mask-cnpj 77 | 78 | ```html 79 | 80 | ``` 81 | 82 | ### v-mask-cep 83 | 84 | ```html 85 | 86 | ``` 87 | 88 | ### v-mask-cc 89 | 90 | ```html 91 | 92 | ``` 93 | 94 | ## Special mask characters 95 | 96 | Character | Description 97 | --- | --- 98 | `0` | Any numbers 99 | `9` | Any numbers (Optional) 100 | `#` | Any numbers (Recursive) 101 | `A` | Any alphanumeric character 102 | `S` | Any letter 103 | `U` | Any letter (All lower case character will be mapped to uppercase) 104 | `L` | Any letter (All upper case character will be mapped to lowercase) 105 | `$` | Escape character, used to escape any of the special formatting characters. 106 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const devPresets = ['@vue/babel-preset-app']; 2 | const buildPresets = [ 3 | [ 4 | '@babel/preset-env', 5 | // Config for @babel/preset-env 6 | { 7 | // Example: Always transpile optional chaining/nullish coalescing 8 | // include: [ 9 | // /(optional-chaining|nullish-coalescing)/ 10 | // ], 11 | }, 12 | ], 13 | ]; 14 | 15 | module.exports = { 16 | presets: (process.env.NODE_ENV === 'development' ? devPresets : buildPresets), 17 | }; 18 | -------------------------------------------------------------------------------- /build/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "Enter release version: " 5 | read VERSION 6 | 7 | read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r 8 | echo # (optional) move to a new line 9 | if [[ $REPLY =~ ^[Yy]$ ]] 10 | then 11 | echo "Releasing $VERSION ..." 12 | # npm test 13 | VERSION=$VERSION npm run build 14 | 15 | # commit 16 | git add -A 17 | git commit -m "[build] $VERSION" 18 | npm version $VERSION --message "[release] $VERSION" 19 | 20 | # publish 21 | git push origin refs/tags/v$VERSION 22 | git push 23 | npm publish --access public 24 | fi 25 | -------------------------------------------------------------------------------- /build/rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import vue from 'rollup-plugin-vue'; 5 | import alias from '@rollup/plugin-alias'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import resolve from '@rollup/plugin-node-resolve'; 8 | import replace from '@rollup/plugin-replace'; 9 | import babel from '@rollup/plugin-babel'; 10 | import PostCSS from 'rollup-plugin-postcss'; 11 | import { terser } from 'rollup-plugin-terser'; 12 | import minimist from 'minimist'; 13 | 14 | // Get browserslist config and remove ie from es build targets 15 | const esbrowserslist = fs.readFileSync('./.browserslistrc') 16 | .toString() 17 | .split('\n') 18 | .filter((entry) => entry && entry.substring(0, 2) !== 'ie'); 19 | 20 | // Extract babel preset-env config, to combine with esbrowserslist 21 | const babelPresetEnvConfig = require('../babel.config') 22 | .presets.filter((entry) => entry[0] === '@babel/preset-env')[0][1]; 23 | 24 | const argv = minimist(process.argv.slice(2)); 25 | 26 | const projectRoot = path.resolve(__dirname, '..'); 27 | 28 | const baseConfig = { 29 | input: 'src/entry.js', 30 | plugins: { 31 | preVue: [ 32 | alias({ 33 | entries: [ 34 | { 35 | find: '@', 36 | replacement: `${path.resolve(projectRoot, 'src')}`, 37 | }, 38 | ], 39 | }), 40 | ], 41 | replace: { 42 | preventAssignment: true, 43 | 'process.env.NODE_ENV': JSON.stringify('production'), 44 | }, 45 | vue: { 46 | preprocessStyles: true, 47 | preprocessOptions: { 48 | scss: { 49 | includePaths: ['node_modules/', 'src/'], 50 | importer(path) { 51 | return { file: path[0] !== '~' ? path : path.slice(1) }; 52 | } 53 | }, 54 | }, 55 | }, 56 | postVue: [ 57 | resolve({ 58 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'], 59 | }), 60 | // Process only ` 223 | -------------------------------------------------------------------------------- /src-doc/InputCode.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 38 | -------------------------------------------------------------------------------- /src-doc/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import VueHighlight from 'vue3-highlightjs'; 3 | import VueMask from '../src/entry'; 4 | import App from './App.vue'; 5 | import 'spectre.css/src/spectre.scss'; 6 | import 'spectre.css/src/spectre-icons.scss'; 7 | 8 | const app = createApp(App); 9 | 10 | app.use(VueMask); 11 | app.use(VueHighlight); 12 | 13 | app.mount('#app'); 14 | -------------------------------------------------------------------------------- /src/directive.js: -------------------------------------------------------------------------------- 1 | import { getInputElement, createEvent } from './helpers'; 2 | 3 | function updater(el, masker) { 4 | const currentValue = el.value; 5 | const oldValue = el.dataset.value; 6 | 7 | if (oldValue === currentValue) { 8 | return; 9 | } 10 | 11 | const newValue = masker(currentValue, { el }); 12 | 13 | if (newValue === currentValue) { 14 | el.dataset.value = currentValue; 15 | return; 16 | } 17 | 18 | // Get current cursor position 19 | let position = el.selectionEnd; 20 | 21 | // Find next cursor position 22 | if (position === currentValue.length) { 23 | position = newValue.length; 24 | } else if (position > 0 && position <= newValue.length) { 25 | const digit = currentValue.charAt(position - 1); 26 | 27 | if (digit !== newValue.charAt(position - 1)) { 28 | if (digit === newValue.charAt(position)) { 29 | position += 1; 30 | } else if (digit === newValue.charAt(position - 2)) { 31 | position -= 1; 32 | } 33 | } 34 | } 35 | 36 | el.value = newValue; 37 | el.dataset.value = newValue; 38 | 39 | if (el === document.activeElement) { 40 | // Restore cursor position 41 | el.setSelectionRange(position, position); 42 | } 43 | 44 | el.dispatchEvent(createEvent('input')); 45 | } 46 | 47 | export default function make(maskerFn) { 48 | const maskerMap = new WeakMap(); 49 | const inputMap = new WeakMap(); 50 | // const eventMap = new WeakMap(); 51 | 52 | return { 53 | beforeMount(el, binding) { 54 | const masker = maskerFn({ 55 | value: binding.value, 56 | locale: binding.arg || Object.keys(binding.modifiers)[0] || null, 57 | }); 58 | 59 | const inputEl = getInputElement(el); 60 | 61 | // const eventHandler = ({ isTrusted }) => { 62 | // if (isTrusted) { 63 | // updater(inputEl, masker); 64 | // } 65 | // }; 66 | 67 | maskerMap.set(el, masker); 68 | inputMap.set(el, inputEl); 69 | // eventMap.set(el, eventHandler); 70 | 71 | // inputEl.addEventListener('input', eventHandler); 72 | }, 73 | mounted(el) { 74 | updater(inputMap.get(el), maskerMap.get(el)); 75 | }, 76 | updated(el) { 77 | updater(inputMap.get(el), maskerMap.get(el)); 78 | }, 79 | unmounted(el) { 80 | // el.removeEventListener('input', inputMap.get(el)); 81 | maskerMap.delete(el); 82 | inputMap.delete(el); 83 | // eventMap.delete(el); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/entry.esm.js: -------------------------------------------------------------------------------- 1 | import * as masks from '@/masks/index'; 2 | import makeDirective from '@/directive'; 3 | 4 | // install function executed by Vue.use() 5 | const install = function installPlugin(app) { 6 | // Register directives 7 | for (const name in masks) { 8 | app.directive(name, makeDirective(masks[name])); 9 | } 10 | }; 11 | 12 | export { default as masker } from '@/masker'; 13 | export { default as makeDirective } from '@/directive'; 14 | export { filterNumbers, filterAlphanumeric, filterLetters } from '@/helpers'; 15 | 16 | // Create module definition for Vue.use() 17 | export default install; 18 | -------------------------------------------------------------------------------- /src/entry.js: -------------------------------------------------------------------------------- 1 | // iife/cjs usage extends esm default export - so import it all 2 | import plugin from '@/entry.esm'; 3 | 4 | export default plugin; 5 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export const getInputElement = (el) => { 2 | const inputEl = el.tagName.toLowerCase() !== 'input' 3 | ? el.querySelector('input:not([readonly])') 4 | : el; 5 | 6 | if (!inputEl) { 7 | throw new Error('Mask directive requires at least one input'); 8 | } 9 | 10 | return inputEl; 11 | }; 12 | 13 | export function createEvent(name) { 14 | const event = document.createEvent('HTMLEvents'); 15 | event.initEvent(name, true, true); 16 | return event; 17 | } 18 | 19 | export const filterNumbers = (v) => ( 20 | v.replace(/\D/g, '') 21 | ); 22 | 23 | export const filterLetters = (v) => ( 24 | v.replace(/[^a-zA-Z]/g, '') 25 | ); 26 | 27 | export const filterAlphanumeric = (v) => ( 28 | v.replace(/[^a-zA-Z0-9]/g, '') 29 | ); 30 | 31 | export const parsePreFn = (arg) => { 32 | if (typeof arg === 'function') { 33 | return arg; 34 | } 35 | 36 | switch (arg) { 37 | case 'filter-number': 38 | return filterNumbers; 39 | case 'filter-letter': 40 | return filterLetters; 41 | default: 42 | return filterAlphanumeric; 43 | } 44 | }; 45 | 46 | export const parsePostFn = (arg) => { 47 | if (typeof arg === 'function') { 48 | return arg; 49 | } 50 | 51 | return (value) => ( 52 | value.trim().replace(/[^0-9]$/, '') 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { default as install } from './entry'; 2 | 3 | export default { install }; 4 | 5 | // if (typeof window !== 'undefined' && window.Vue) { 6 | // window.Vue.use({install}); 7 | // } 8 | -------------------------------------------------------------------------------- /src/masker.js: -------------------------------------------------------------------------------- 1 | import StringMask from 'string-mask'; 2 | import { parsePostFn, parsePreFn } from './helpers'; 3 | 4 | const delimiter = '\u00a7'; 5 | 6 | export default function masker(fn) { 7 | return (args) => { 8 | const data = fn(args); 9 | 10 | const pre = parsePreFn('pre' in data ? data.pre : null); 11 | const post = parsePostFn('post' in data ? data.post : null); 12 | 13 | const formatter = 'pattern' in data && data.pattern 14 | ? new StringMask(data.pattern, data.options || {}) 15 | : null; 16 | 17 | const handler = 'handler' in data && typeof data.handler === 'function' 18 | ? data.handler 19 | : (value) => (formatter ? formatter.apply(value) : value); 20 | 21 | return (str, args = {}) => { 22 | args = { ...args, delimiter }; 23 | 24 | str = pre(str, args); 25 | 26 | let [prefix, value] = ( 27 | !str.includes(delimiter) ? `${delimiter}${str}` : str 28 | ).split(delimiter); 29 | 30 | value = handler(value, args); 31 | 32 | return post(`${prefix}${value}`, args); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/masks/cep.js: -------------------------------------------------------------------------------- 1 | import masker from '../masker'; 2 | import { filterNumbers } from '../helpers'; 3 | 4 | export default masker(() => ({ 5 | pattern: '00.000-000', 6 | pre: filterNumbers, 7 | })); 8 | -------------------------------------------------------------------------------- /src/masks/cnpj.js: -------------------------------------------------------------------------------- 1 | import masker from '../masker'; 2 | import { filterNumbers } from '../helpers'; 3 | 4 | export default masker(() => ({ 5 | pattern: '00.000.000/0000-00', 6 | pre: filterNumbers, 7 | })); 8 | -------------------------------------------------------------------------------- /src/masks/cpf.js: -------------------------------------------------------------------------------- 1 | import masker from '../masker'; 2 | import { filterNumbers } from '../helpers'; 3 | 4 | export default masker(() => ({ 5 | pattern: '000.000.000-00', 6 | pre: filterNumbers, 7 | })); 8 | -------------------------------------------------------------------------------- /src/masks/credit-card.js: -------------------------------------------------------------------------------- 1 | import masker from '../masker'; 2 | import { filterNumbers } from '../helpers'; 3 | 4 | export default masker(() => ({ 5 | pattern: '0000 0000 0000 0000', 6 | pre: filterNumbers, 7 | })); 8 | -------------------------------------------------------------------------------- /src/masks/date.js: -------------------------------------------------------------------------------- 1 | import masker from '../masker'; 2 | import { filterNumbers } from '../helpers'; 3 | 4 | const patterns = { 5 | us: '0000-00-00', 6 | br: '00/00/0000' 7 | }; 8 | 9 | export default masker(({ locale = null } = {}) => ({ 10 | pattern: patterns[locale || 'us'], 11 | pre: filterNumbers, 12 | })); 13 | -------------------------------------------------------------------------------- /src/masks/decimal.js: -------------------------------------------------------------------------------- 1 | import masker from '../masker'; 2 | import { filterNumbers } from '../helpers'; 3 | 4 | const config = { 5 | us: { thousand: ',', decimal: '.' }, 6 | br: { thousand: '.', decimal: ',' } 7 | }; 8 | 9 | export default masker(({ locale, value }) => { 10 | const conf = config[locale || 'us']; 11 | 12 | const patternParts = [`#${conf.thousand}##0`]; 13 | const precision = value || 0; 14 | 15 | if (precision) { 16 | patternParts.push( 17 | conf.decimal, 18 | new Array(precision).fill('0').join('') 19 | ); 20 | } 21 | 22 | return { 23 | pattern: patternParts.join(''), 24 | options: { reverse: true }, 25 | pre(value, { delimiter }) { 26 | if (!value) { 27 | return ''; 28 | } 29 | 30 | const sign = value.startsWith('-') ? '-' : ''; 31 | 32 | let [number, fraction = ''] = value.split(conf.decimal).map(filterNumbers); 33 | 34 | if (fraction && fraction.length > precision) { 35 | number = `${number}${fraction.slice(0, -precision)}`; 36 | fraction = fraction.slice(-precision); 37 | } 38 | 39 | return [sign, delimiter, Number(number), fraction].join(''); 40 | }, 41 | post(value) { 42 | return value; 43 | }, 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /src/masks/hour.js: -------------------------------------------------------------------------------- 1 | import masker from '../masker'; 2 | import { filterNumbers } from '../helpers'; 3 | 4 | export default masker(() => ({ 5 | pattern: '00:00', 6 | pre: filterNumbers, 7 | })); 8 | -------------------------------------------------------------------------------- /src/masks/index.js: -------------------------------------------------------------------------------- 1 | export { default as mask } from './mask'; 2 | export { default as maskDate } from './date'; 3 | export { default as maskHour } from './hour'; 4 | export { default as maskPhone } from './phone'; 5 | export { default as maskDecimal } from './decimal'; 6 | export { default as maskNumber } from './number'; 7 | export { default as maskCpf } from './cpf'; 8 | export { default as maskCnpj } from './cnpj'; 9 | export { default as maskCep } from './cep'; 10 | export { default as maskCc } from './credit-card'; 11 | -------------------------------------------------------------------------------- /src/masks/mask.js: -------------------------------------------------------------------------------- 1 | import masker from '../masker'; 2 | import { filterAlphanumeric } from '../helpers'; 3 | 4 | export default masker(({ value: pattern }) => ({ 5 | pattern, 6 | pre: filterAlphanumeric, 7 | post: (value) => ( 8 | value.trim().replace(/[^a-zA-Z0-9]$/, '') 9 | ), 10 | })); 11 | -------------------------------------------------------------------------------- /src/masks/number.js: -------------------------------------------------------------------------------- 1 | import masker from '../masker'; 2 | import { filterNumbers } from '../helpers'; 3 | 4 | export default masker(() => { 5 | return { 6 | pattern: '#0', 7 | options: { reverse: true }, 8 | pre: filterNumbers 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /src/masks/phone.js: -------------------------------------------------------------------------------- 1 | import StringMask from 'string-mask'; 2 | import masker from '../masker'; 3 | import { filterNumbers } from '../helpers'; 4 | 5 | const handlers = { 6 | get us() { 7 | const phone = new StringMask('(000) 000-0000'); 8 | return (value) => phone.apply(value); 9 | }, 10 | get br() { 11 | const phone = new StringMask('(00) 0000-0000'); 12 | const phone9 = new StringMask('(00) 9 0000-0000'); 13 | const phone0800 = new StringMask('0000-000-0000'); 14 | 15 | return (value) => { 16 | if (value.startsWith('0800'.slice(0, value.length))) { 17 | return phone0800.apply(value); 18 | } else if (value.length <= 10) { 19 | return phone.apply(value); 20 | } 21 | return phone9.apply(value); 22 | } 23 | } 24 | }; 25 | 26 | export default masker(({ locale }) => { 27 | const handler = handlers[locale || 'us']; 28 | 29 | return { 30 | pre: filterNumbers, 31 | handler, 32 | }; 33 | }); 34 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | chainWebpack: (config) => { 5 | config.resolve.alias 6 | .set('src', path.resolve(__dirname, 'src')) 7 | .set('@', path.resolve(__dirname, 'src')); 8 | }, 9 | }; 10 | --------------------------------------------------------------------------------