├── .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 |
2 |
3 |
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 |
--------------------------------------------------------------------------------