2
0
mirror of https://github.com/tenrok/maska.git synced 2026-05-15 11:59:38 +03:00

Initial commit

This commit is contained in:
Alexander Shabunevich
2019-09-25 15:22:31 +03:00
commit 0690000a62
14 changed files with 9037 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+90
View File
@@ -0,0 +1,90 @@
# Maska
Simple zero-dependency input mask for Vue.js and vanilla JS.
- No dependencies
- Small size (~2 Kb gziped)
- Ability to define custom tokens
- Supports repeat symbols
- Works on any input (custom or native)
## Usage with Vue.js
If you load Vue.js via `<script>` then just add `v-maska` directive to your input:
``` html
<input v-maska="'###'">
```
You can add custom tokens by passing in object instead of string to directive:
``` html
<input v-maska="{ mask: 'Z*', tokens: { 'Z': { pattern: /[а-яА-Я]/ }}}">
```
If you use SFC then import `directive` component from library:
``` html
<template>
<form>
<input v-maska="'###'">
</form>
</template>
<script>
import { maska } from 'maska'
export default {
directives: { maska }
}
</script>
```
## Usage with vanilla JS
Just load script `maska.js` and init it, passing element(s) or selector expression:
``` javascript
var mask = Maska.create('.masked');
```
You can pass custom tokens while initialization:
``` javascript
var mask = Maska.create('.masked', {
tokens: { 'Z': { pattern: /[а-яА-Я]/ }}
});
```
You can destroy mask like that:
``` javascript
var mask = Maska.create('.masked');
mask.destroy();
```
## Mask syntax
Default tokens:
``` javascript
{
'#': { 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 }
}
```
- Escape symbol escapes next token (mask `!#` will render `#`)
- Repeat symbol allows repeating current token until its valid (e.g. mask `#*` for all digits or `A* A*` for `CARDHOLDER NAME`)
You can add your own tokens by passing them in `maska` directive or `create` method at initialization (see above). **Important**: `pattern` field should be JS *regular expression* (`/[0-9]/`) or *string without delimiters* (`"[0-9]"`).
## Source of Inspiration
- [vue-the-mask](https://vuejs-tips.github.io/vue-the-mask/)
- [jQuery Mask Plugin](http://igorescobar.github.io/jQuery-Mask-Plugin/)
+116
View File
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Maska demo</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css" integrity="sha256-vK3UTo/8wHbaUn+dTQD0X6dzidqc5l7gczvH+Bnowwk=" crossorigin="anonymous" />
<style>
body { padding: 2em 0 5em; background: #fafafa }
.container { max-width: 800px }
</style>
</head>
<body>
<div class="container">
<div class="content">
<h1 class="is-size-1">Maska library demo</h1>
<h2 class="is-size-3">Vue.js examples</h2>
<div class="box">
<form id="vue-form">
<div class="field">
<label class="label">Phone with code</label>
<div class="control">
<input v-maska="'+1 (###) ###-##-##'" value="12345678901" class="input">
</div>
<p class="help is-family-code">v-maska="'+1 (###) ###-##-##'"</p>
</div>
<div class="field">
<label class="label">Hex color (custom tokens)</label>
<div class="control">
<input v-maska="{ mask: '!#HHHHHH', tokens: { 'H': { pattern: /[0-9a-fA-F]/, uppercase: true }}}" class="input">
</div>
<p class="help is-family-code">v-maska="{ mask: '!#HHHHHH', tokens: { 'H': { pattern: /[0-9a-fA-F]/, uppercase: true }}}"</p>
</div>
</form>
</div>
<h2 class="is-size-3">Vanilla JS examples</h2>
<div class="box">
<form id="vanilla-form">
<div class="columns">
<div class="column">
<div class="field">
<label class="label">Phone with code</label>
<div class="control">
<input data-mask="+1 (###) ###-####" class="masked input">
</div>
<p class="help is-family-code">data-mask="+1 (###) ###-####"</p>
</div>
<div class="field">
<label class="label">Cardholder name</label>
<div class="control">
<input data-mask="A* A*" class="masked input">
</div>
<p class="help is-family-code">data-mask="A* A*"</p>
</div>
<div class="field">
<label class="label">All digits</label>
<div class="control">
<input data-mask="#*" class="masked input">
</div>
<p class="help is-family-code">data-mask="#*"</p>
</div>
</div>
<div class="column">
<div class="field">
<label class="label">Date</label>
<div class="control">
<input data-mask="##/##/####" class="masked input">
</div>
<p class="help is-family-code">data-mask="##/##/####"</p>
</div>
<div class="field">
<label class="label">Hex color (custom tokens)</label>
<div class="control">
<input data-mask="!#HHHHHH" class="custom-masked input">
</div>
<p class="help is-family-code">data-mask="!#HHHHHH"</p>
</div>
<div class="field">
<label class="label">Without mask (destroyed)</label>
<div class="control">
<input data-mask="###" class="unmasked input">
</div>
<p class="help is-family-code">data-mask="###"</p>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="../dist/maska.js"></script>
<script>
// vue
new Vue({
el: '#vue-form'
});
// vanilla default
Maska.create('#vanilla-form .masked');
// vanilla custom tokens
Maska.create('#vanilla-form .custom-masked', {
tokens: { 'H': { pattern: '[0-9a-fA-F]', uppercase: true }}
});
// vanilla destroy
var mask = Maska.create('#vanilla-form .unmasked');
mask.destroy();
</script>
</body>
</html>
+1
View File
File diff suppressed because one or more lines are too long
+8440
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
{
"name": "maska",
"version": "1.0.0",
"description": "Simple zero-dependency input mask for Vue.js and vanilla JS",
"keywords": [
"mask",
"input",
"vue"
],
"author": "Alexander Shabunevich <alex@aether.ru>",
"license": "MIT",
"scripts": {
"serve": "rimraf dist && webpack --watch",
"build": "rimraf dist && webpack -p",
"test": "jest",
"lint": "standard 'src/**'"
},
"module": "src/index.js",
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"jest": "^24.9.0",
"rimraf": "^3.0.0",
"standard": "^14.3.1",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9"
},
"browserslist": "> 0.25%, ie 11",
"babel": {
"presets": [
"@babel/preset-env"
]
}
}
+35
View File
@@ -0,0 +1,35 @@
import Maska from './maska'
import { isString } from './utils'
function getOpts (mask) {
const opts = {}
if (isString(mask)) {
opts.mask = mask
} else if (mask.mask) {
opts.mask = mask.mask
opts.tokens = mask.tokens ? mask.tokens : {}
}
return opts
}
function needUpdate (mask) {
if (isString(mask.value) && isString(mask.oldValue) && mask.value === mask.oldValue) {
return false
}
if (mask.value && mask.oldValue && mask.value.mask === mask.oldValue.mask) {
return false
}
return true
}
export default function directive (el, mask) {
if (!mask.value) return
if (mask.value && needUpdate(mask)) {
return new Maska(el, getOpts(mask.value))
}
}
+24
View File
@@ -0,0 +1,24 @@
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
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(install)
}
function create (el, options) {
return new Maska(el, options)
}
export default install
export {
create,
mask,
directive as maska,
tokens
}
+61
View File
@@ -0,0 +1,61 @@
export default function mask (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++
}
iv++
} else if (token && token.repeat) {
const maskCharPrev = mask[im - 1]
const tokenPrev = tokens[maskCharPrev]
if (token && token.repeat && 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 parentesis
while (masked && im < mask.length) { // eslint-disable-line no-unmodified-loop-condition
const maskCharRest = mask[im]
if (tokens[maskCharRest] && !tokens[maskCharRest].repeat) {
rest = ''
break
}
if (!tokens[maskCharRest]) {
rest += maskCharRest
}
im++
}
return ret + rest
}
function tokenTransform (value, token) {
if (token.uppercase) {
return value.toLocaleUpperCase()
} else if (token.lowercase) {
return value.toLocaleLowerCase()
}
return value
}
+61
View File
@@ -0,0 +1,61 @@
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.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 }
}
this._el = isString(el) ? document.querySelectorAll(el) : !el.length ? [el] : el
this.init()
}
init () {
for (let i = 0; i < this._el.length; i++) {
const el = findInputElement(this._el[i])
if (!el.dataset.mask && this._opts.mask) {
el.dataset.mask = this._opts.mask
}
this.updateValue(el)
el.addEventListener('input', evt => this.onInput(evt))
}
}
destroy () {
for (let i = 0; i < this._el.length; i++) {
const el = findInputElement(this._el[i])
el.removeEventListener('input', evt => this.onInput(evt))
delete el.dataset.mask
}
}
updateValue (el) {
if (!el.value || !el.dataset.mask) return
const position = el.selectionEnd
const digit = el.value[position - 1]
el.value = mask(el.value, el.dataset.mask, this._opts.tokens)
fixInputSelection(el, position, digit)
el.dispatchEvent(event('input'))
}
onInput (event) {
if (event.isTrusted) {
this.updateValue(event.target)
}
}
}
+10
View File
@@ -0,0 +1,10 @@
/* 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 }
}
+31
View File
@@ -0,0 +1,31 @@
/* global HTMLInputElement */
function event (name) {
const event = document.createEvent('Event')
event.initEvent(name, true, true)
return event
}
function findInputElement (el) {
return (el instanceof HTMLInputElement) ? el : el.querySelector('input') || el
}
function fixInputSelection (el, position, digit) {
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)
}
}
function isString (val) {
return Object.prototype.toString.call(val) === '[object String]'
}
export { event, findInputElement, fixInputSelection, isString }
+106
View File
@@ -0,0 +1,106 @@
import mask from './../src/mask'
import tokens from './../src/tokens'
test('12 #.#', () => {
expect(mask('12', '#.#', tokens)).toBe('1.2')
})
test('1 (#)', () => {
expect(mask('1', '(#)', tokens)).toBe('(1)')
})
test('1 [(#)]', () => {
expect(mask('1', '[(#)]', tokens)).toBe('[(1)]')
})
test('1 #.#', () => {
expect(mask('1', '#.#', tokens)).toBe('1')
})
test('1. #.#', () => {
expect(mask('1.', '#.#', tokens)).toBe('1.')
})
test('123 #.#', () => {
expect(mask('123', '#.#', tokens)).toBe('1.2')
})
test('Raw phone number', () => {
expect(mask('44998765432', '+55 (##) #####-####', tokens, false)).toBe('44998765432')
})
test('abcd12345 AAA-####', () => {
expect(mask('abcd12345', 'AAA-####', tokens)).toBe('ABC-1234')
})
test('a5-12-34 (XX) - ## - ##', () => {
expect(mask('a5-12-34', '(XX) - ## - ##', tokens)).toBe('(a5) - 12 - 34')
})
test('123 ##(#)', () => {
expect(mask('123', '##(#)', tokens)).toBe('12(3)')
})
test('12 #!#(#)', () => {
expect(mask('12', '#!#(#)', tokens)).toBe('1#(2)')
})
test('12 +1 #', () => {
expect(mask('12', '+1 #', tokens)).toBe('+1 2')
})
test('2 +1 #', () => {
expect(mask('2', '+1 #', tokens)).toBe('+1 2')
})
test('abc DEF AAA aaa', () => {
expect(mask('abc DEF', 'AAA aaa', tokens)).toBe('ABC def')
})
test('123abc #*', () => {
expect(mask('123abc', '#*', tokens)).toBe('123')
})
test('123abc A*', () => {
expect(mask('123abc', 'A*', tokens)).toBe('ABC')
})
test('123abc #A*', () => {
expect(mask('123abc', '#A*', tokens)).toBe('1ABC')
})
test('123abc ## A*', () => {
expect(mask('123abc', '## A*', tokens)).toBe('12 ABC')
})
test('123abc #*A', () => {
expect(mask('123abc', '#*A', tokens)).toBe('123A')
})
test('123abc #*A*', () => {
expect(mask('123abc', '#*A*', tokens)).toBe('123ABC')
})
test('123 abc DEF 456 -> A* a*', () => {
expect(mask('123 abc DEF 456', 'A* a*', tokens)).toBe('ABC def')
})
test('abc123 A!*', () => {
expect(mask('abc123', 'A!*', tokens)).toBe('A*')
})
test('123abc #!*A*', () => {
expect(mask('123abc', '#!*A*', tokens)).toBe('1*ABC')
})
test('123abc (#*)A*', () => {
expect(mask('123abc', '(#*)A*', tokens)).toBe('(123)ABC')
})
test('123abc -> # (A*)', () => {
expect(mask('123abc', '# (A*)', tokens)).toBe('1 (ABC)')
})
test('Raw 123abc ##(A*)', () => {
expect(mask('123abc', '##(A*)', tokens, false)).toBe('12ABC')
})
+25
View File
@@ -0,0 +1,25 @@
const prod = process.env.NODE_ENV === 'production'
module.exports = {
mode: prod ? 'production' : 'development',
output: {
filename: 'maska.js',
library: 'Maska',
libraryTarget: 'umd',
umdNamedDefine: true
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
}