mirror of
https://github.com/tenrok/maska.git
synced 2026-05-15 11:59:38 +03:00
Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -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 it’s 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
@@ -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>
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
+8440
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user