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

New version code prepare

This commit is contained in:
Alexander Shabunevich
2022-12-04 16:50:18 +03:00
parent fadf78f5bb
commit 31c286bb6b
48 changed files with 15195 additions and 5502 deletions
+12
View File
@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
+25 -3
View File
@@ -1,3 +1,25 @@
node_modules/
package-lock.json
/.idea/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
coverage
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+5
View File
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none"
}
+19
View File
@@ -0,0 +1,19 @@
The MIT License (MIT)
Copyright (c) 2018 Form.io
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+27 -186
View File
@@ -1,196 +1,37 @@
# Maska
Simple zero-dependency input mask for Vue.js and vanilla JS. [Demo and examples](https://beholdr.github.io/maska/).
Simple zero-dependency input mask for Vue 2/3 or Vanilla JS.
- No dependencies
- Small size (~2 Kb gziped)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/maska)](https://bundlephobia.com/package/maska)
[![build](https://github.com/beholdr/pos/actions/workflows/build.yml/badge.svg)](https://github.com/beholdr/pos/actions/workflows/build.yml)
![code coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/beholdr/7f2a04de5e494f9a3820832520ee2562/raw/badge.json)
**[Documentation & demo →](https://beholdr.github.io/maska/)**
## Features ✨
- No dependencies and small size: ~2.5 Kb gziped
- Works with any native input
- Ability to define custom tokens
- Supports repeat symbols and dynamic masks
- Works on any input (custom or native)
- Dynamic, reversed and eager masks
- Token modifiers, transform functions and hooks
## Install
## Whats new in v2 🎉
npm install maska
- Fully rewritten in TypeScript
- Autobind to vue variable
- Eager and reversed masking modes
- Optonal and repeated tokens
- Simplified syntax for custom tokens
- Dynamic masks with custom function logic
- Hooks for pre/post- processing
- Ability to replace or merge custom tokens
To load latest version from CDN you can use:
``` html
<script src="https://cdn.jsdelivr.net/npm/maska@latest/dist/maska.js"></script>
```
## Usage with Vue 2.x
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: /[а-яА-Я]/ }}}">
```
With bundlers you can add global directive:
``` javascript
import Maska from 'maska'
Vue.use(Maska)
```
or import `maska` directive for local usage in component:
``` html
<template>
<form>
<input v-maska="'###'">
</form>
</template>
<script>
import { maska } from 'maska'
export default {
directives: { maska }
}
</script>
```
With Vue you could use computed property as mask value. In this case mask will be reactive.
## Usage with Vue 3.x
With Vue 3.x you need to explicitly add Maska `plugin` or `directive` to your app:
``` javascript
const app = Vue.createApp({...})
// use as plugin
app.use(Maska);
// or as directive
// app.directive('maska', Maska.maska);
app.mount('#app');
```
## Usage with vanilla JS
Just load script `maska.js` and init it, passing element(s) or `document.querySelector` expression:
``` javascript
var mask = Maska.create('.masked');
```
Mask could be set as `data-mask` attribute on element:
``` html
<input data-mask='##/##/####'>
```
or can be set by `mask` option on initialization:
``` javascript
var mask = Maska.create('.masked', {
mask: '##/##/####'
});
```
You can pass custom tokens while initialization:
``` javascript
var mask = Maska.create('.masked', {
tokens: { 'Z': { pattern: /[а-яА-Я]/ }}
});
```
You also can pass custom preprocessing transformation function for entire input:
``` javascript
var mask = Maska.create('.masked', {
tokens: { 'Z': { pattern: /[а-яА-Я]/ }},
preprocessor: value => {
return value.toUpperCase();
}
});
```
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]"`).
### Transform function for tokens
While specifying custom tokens you can also add a symbol-transformation behavior such as uppercase, lowercase, or even define a transform function:
``` javascript
{
'T': { pattern: /[0-9]/, transform: (char) => String(Number(char) % 2) } // '1234567890' -> '1010101010'
}
```
## Use mask programmatically
You can use `mask` function directly by importing it (or using `Maska.mask` if you use script tag)
``` javascript
import { mask } from 'maska'
const maskedValue = mask(value, '###')
```
## Getting raw (unmasked) value
To get raw value read `data-mask-raw-value` property of input. You can subscribe to `@maska` event to know when this value updates. Please see [examples page](https://beholdr.github.io/maska/).
```html
@maska="rawValue = $event.target.dataset.maskRawValue"
```
## Dynamic masks
To use several masks on single input, pass array instead of string as mask value.
You could use it with Vue directives:
``` html
<input v-maska="['+1 (###) ##-##-##', '+1 (###) ###-##-##']">
<input v-maska="{ mask: ['!#HHHHHH', '!#HHHHHH-HH'], tokens: { 'H': { pattern: /[0-9a-fA-F]/, uppercase: true }}}">
```
and with vanilla JS attribute, but make sure that mask value is proper `JSON`, so use double quotes inside array:
``` html
<input data-mask='["# cm", "#.# cm", "#.## cm"]'>
```
## Known issues
When used on input of type `number` could have inconsistent behavior in different browsers. Use attribute `inputmode` if you just need a numeric keyboard for given input.
## Source of Inspiration
## Source of Inspiration 💡
- [vue-the-mask](https://vuejs-tips.github.io/vue-the-mask/)
- [jQuery Mask Plugin](http://igorescobar.github.io/jQuery-Mask-Plugin/)
## License 📄
[MIT](LICENSE.txt)
+499
View File
@@ -0,0 +1,499 @@
# Live Demo
Here are several examples of basic masks that you could create with **Maska**.
This demo is interactive: feel free to edit the examples.
<div id="demo-app"></div>
<script src="dist/demo.js"></script>
# Install
<!-- tabs:start -->
## **Via npm**
```
npm i maska
```
## **From CDN**
To include library from CDN, use UMD format and prefix all classes and directives with `Maska.`
``` html
<script src="https://cdn.jsdelivr.net/npm/maska@latest/dist/maska.umd.js"></script>
<script>
new Maska.MaskInput("[data-maska]") // for masked input
const mask = new Maska.Mask({ mask: "#-#" }) // for programmatic use
</script>
```
<!-- tabs:end -->
# Usage
**Maska** library consists of three main components:
- `Mask` class to mask string values
- `MaskInput` class to apply `Mask` processing to `<input>`
- `vMaska` directive to simplify using of library within Vue components
<!-- tabs:start -->
## **Vanilla JS**
Start with simple input element and `data-maska` attribute:
``` html
<input data-maska="#-#">
```
Then import and init `MaskInput`, passing input element(s) or `querySelector` expression as first argument:
``` js
import { MaskInput } from "maska"
new MaskInput("[data-maska]")
```
Usually you set mask via `data-maska` attribute. But you can pass mask and other [options](#options) via second argument (it will be a default options that can be overriden by `data-maska-` attributes):
``` js
new MaskInput("input", { mask: "#-#" })
```
To destroy mask use `destroy()` method:
``` js
const mask = new MaskInput(...)
mask.destroy()
```
## **Vue**
Import `vMaska` directive and apply it to the input along with `data-maska` attribite:
<!-- tabs:start -->
### **Composition API**
``` html
<script setup>
import { vMaska } from "maska"
</script>
<template>
<input v-maska data-maska="#-#">
</template>
```
### **Options API**
``` html
<script>
import { vMaska } from "maska"
export default {
directives: { maska: vMaska }
}
</script>
<template>
<input v-maska data-maska="#-#">
</template>
```
<!-- tabs:end -->
### Set options with directive
To set default options for the mask you could use directive *argument* (part after `v-maska:`). It can be plain or reactive object and should be wrapped in `[]`:
<!-- tabs:start -->
### **Composition API**
``` html
<script setup>
import { reactive } from "vue"
import { vMaska } from "maska"
const options = reactive({
mask: "#-#",
eager: true
})
</script>
<template>
<input v-maska:[options]>
</template>
```
### **Options API**
``` html
<script>
import { vMaska } from "maska"
export default {
directives: { maska: vMaska },
data: () => ({
options: {
mask: "#-#",
eager: true
}
})
}
</script>
<template>
<input v-maska:[options]>
</template>
```
<!-- tabs:end -->
### Bind to variable
Its very easy to bind mask result to a variable.
This variable should be reactive object and will contains three fields:
- `masked`: string with masked result
- `unmasked`: string with unmasked result
- `completed`: boolean flag indicating that mask is completed
<!-- tabs:start -->
### **Composition API**
``` html
<script setup>
import { reactive } from "vue"
import { vMaska } from "maska"
const binded = reactive({})
</script>
<template>
<input v-maska="binded">
<p v-if="binded.completed">
Masked value: {{ binded.masked }}
Unmasked value: {{ binded.unmasked }}
</p>
</template>
```
### **Options API**
``` html
<script>
import { vMaska } from "maska"
export default {
directives: { maska: vMaska },
data: () => ({
binded: {
masked: "",
unmasked: "",
completed: false
}
})
}
</script>
<template>
<input v-maska="binded">
<p v-if="binded.completed">
Masked value: {{ binded.masked }}
Unmasked value: {{ binded.unmasked }}
</p>
</template>
```
<!-- tabs:end -->
#### Global registration of directive
<!-- tabs:start -->
### **Vue 3**
``` js
import { createApp } from "vue"
import { vMaska } from "maska"
createApp({}).directive("maska", vMaska)
// or in case of CDN load
Vue.createApp({}).directive("maska", Maska.vMaska)
```
### **Vue 2**
``` js
import Vue from "vue"
import { vMaska } from "maska"
Vue.directive("maska", vMaska)
// or in case of CDN load
Vue.directive("maska", Maska.vMaska)
```
<!-- tabs:end -->
<!-- tabs:end -->
# Options
## `Mask` options
Every option of `Mask` class can be set via `data-maska-` attributes or by passing on init.
Options passed on init will be used as defaults and could be overriden by `data-maska-` attributes.
<!-- tabs:start -->
### **Description**
- `mask` / `data-maska` — mask to apply (**string**, **array of strings** or **function**)
- `tokens` / `data-maska-tokens` — custom tokens object
- `tokensReplace` / `data-maska-tokens-replace` — if custom tokens should replace default tokens (if not set they will merge)
- `eager` / `data-maska-eager` — eager mode
- `reversed` / `data-maska-reversed` — reversed mode
### **Types**
``` js
interface MaskOptions {
mask?: MaskType
tokens?: MaskTokens
tokensReplace?: boolean
eager?: boolean
reversed?: boolean
}
```
<!-- tabs:end -->
``` html
<input data-maska="A-A" data-maska-tokens="A:[A-Z]" data-maska-eager>
```
## `MaskInput` options
`MaskInput` options could be set only by passing second argument on init.
Along with `MaskInput` options you could pass any `Mask` options as well (or set them via `data-maska-` attributes).
<!-- tabs:start -->
### **Description**
- `onMaska` — [callback](#events) on every mask processing
- `preProcess` — [hook](#hooks) before mask processing
- `postProcess` — [hook](#hooks) after mask processing
### **Types**
``` js
interface MaskInputOptions extends MaskOptions {
onMaska?: (detail: MaskaDetail) => void
preProcess?: (value: string) => string
postProcess?: (value: string) => string
}
```
<!-- tabs:end -->
``` js
new MaskInput("input", {
mask: "#-#",
reversed: true,
onMaska: (detail) => console.log(detail.completed)
})
```
# Tokens
There are 3 tokens defined by default:
``` js
{
'#': { pattern: /[0-9]/ }, // digits
'@': { pattern: /[a-zA-Z]/ }, // letters
'*': { pattern: /[a-zA-Z0-9]/ }, // letters & digits
}
```
?> Use `!` before token to escape symbol. For example `!#` will render `#` instead of a digit.
## Custom tokens
Add custom tokens via `data-maska-tokens` attribute or by `tokens` option:
<!-- tabs:start -->
### **By data-attr**
When using `data-maska-tokens`, there are two possible formats:
- **Full form** should be a valid JSON-string (but can use both single and double quotes) with pattern in string format without delimiters
- **Simple form** should be a string in format: `T:P:M|T:P:M` where:
- `T` is token
- `P` is pattern in string form
- `M` is optional modifier (see below)
- `|` is separator for multiple tokens
``` html
<input data-maska="Z-Z" data-maska-tokens="{ 'Z': { 'pattern': '[A-Z]' }}">
<input data-maska="Z-Z" data-maska-tokens="Z:[A-Z]">
<input data-maska="Z-z" data-maska-tokens="Z:[A-Z]|z:[a-z]:multiple">
```
?> You cant set `transform` function for token via `data-maska-tokens`.
If you need this, use `tokens` option instead.
### **By option**
When using `tokens` option, pattern should be a valid regular expression object:
``` js
new MaskInput("[data-maska]", {
mask: "A-A",
tokens: {
A: { pattern: /[A-Z]/, transform: (chr) => chr.toUpperCase() }
}
})
```
<!-- tabs:end -->
## Token modifiers
Every token can have a modifier, for example:
``` js
{
0: { pattern: /[0-9]/, optional: true },
9: { pattern: /[0-9]/, repeated: true },
}
```
- `optional` for optional token
- `multiple` for token that can match multiple characters till the next token starts
- `repeated` for tokens that should be repeated. This token will match only one character, but the token itself (or group of such tokens) can be repeated any number of times
| Modifier | Example usage | Mask | Tokens
| --- | --- | --- | ---
| `optional` | IP address | `#00.#00.#00.#00` | `0:[0-9]:optional`
| `multiple` | CARDHOLDER NAME | `A A` | `A:[A-Z]:multiple`
| `repeated` | Money (1 234,56) | `9 99#,##` <small>reversed</small> | `9:[0-9]:repeated`
## Transform tokens
For custom tokens you can define `transform` function, applied to a character before masking.
For example, transforming letters to uppercase on input:
``` js
{
A: { pattern: /[A-Z]/, transform: (chr) => chr.toUpperCase() }
}
```
?> You can also use [hooks](#hooks) for transforming whole value before/after masking.
# Dynamic masks
Pass `mask` as **array** or **function** to make it dynamic:
- With **array** a suitable mask will be chosen by length (smallest first)
- With **function** you can select mask with custom logic using value
``` js
new MaskInput("input", {
mask: (value) => value.startsWith('1') ? '#-#' : '##-##'
})
```
# Hooks
Use hooks for transforming whole value:
- `preProcess` hook called before mask processing
- `postProcess` hook called after mask processing, but before setting input's value
For example, you can use it to check for a maximum length of masked string:
``` js
new MaskInput("input", {
postProcess: (value) => value.slice(0, 5)
})
```
# Events
You can subscribe to `maska` event, fired every time when mask applied:
<!-- tabs:start -->
## **Vanilla JS**
``` js
document.querySelector("input").addEventListener("maska", onMaska)
```
## **Vue**
``` html
<input v-maska data-maska="#-#" @maska="onMaska" />
```
<!-- tabs:end -->
Callback will receive custom event contains `detail` property with given structure:
<!-- tabs:start -->
### **Description**
- `masked`: masked value
- `unmasked`: unmasked value
- `completed`: flag that current mask is completed
### **Types**
``` js
interface MaskaDetail {
masked: string
unmasked: string
completed: boolean
}
```
<!-- tabs:end -->
``` js
const onMaska = (event) => {
console.log({
masked: event.detail.masked,
unmasked: event.detail.unmasked,
completed: event.detail.completed
})
}
```
Alternatively, you can pass callback function directly with `MaskInput` option `onMaska`:
<!-- tabs:start -->
### **Vanilla JS**
``` js
new MaskInput("input", {
onMaska: (detail) => console.log(detail.completed)
})
```
### **Vue**
``` html
<script setup>
const options = {
onMaska: (detail) => console.log(detail.completed)
}
</script>
<template>
<input v-maska:[options]>
</template>
```
<!-- tabs:end -->
# Programmatic use
You can use mask function programmatically by importing `Mask` class.
There are three methods available:
- `masked(value)` returns masked version of given value
- `unmasked(value)` returns unmasked version of given value
- `completed(value)` returns `true` if given value makes mask complete
``` js
import { Mask } from "maska"
const mask = new Mask({ mask: "#-#" })
mask.masked("12") // = 1-2
mask.unmasked("12") // = 12
mask.completed("12") // = true
```
-200
View File
@@ -1,200 +0,0 @@
<!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 { background: #fafafa }
body > section > .container { max-width: 800px }
</style>
</head>
<body>
<section class="section">
<div class="container">
<div class="content">
<div class="is-pulled-right"><a href="https://github.com/beholdr/maska" class="button is-link is-medium">Github</a></div>
<h1 class="is-size-2 is-marginless">Maska demo</h1>
<h2 class="is-size-4">Vue.js examples</h2>
<div class="box">
<form id="vue-form">
<div class="field">
<label class="label">Phone with code: {{ phone }} (raw value: {{ phoneRaw }})</label>
<div class="control">
<input v-maska="['+1 (###) ##-##-##', '+1 (###) ###-##-##']" class="input" type="tel" autocomplete="tel" v-model="phone" @maska="phoneRaw = $event.target.dataset.maskRawValue">
</div>
<p class="help is-family-code">v-maska="['+1 (###) ##-##-##', '+1 (###) ###-##-##']"</p>
</div>
<div class="field">
<label class="label">Reactive mask: <input type="checkbox" v-model="dotFormat"> use dot as date separator?</label>
<div class="control">
<input v-maska="dateMask" class="input">
</div>
<p class="help is-family-code">v-maska="dateMask" — reactive mask by `dateMask` computed property</p>
</div>
<div class="field">
<label class="label">Hex color (custom tokens): {{ color }}</label>
<div class="control">
<input v-maska="{ mask: '!#HHHHHH', tokens: { 'H': { pattern: /[0-9a-fA-F]/, uppercase: true }}}" class="input" v-model="color">
</div>
<p class="help is-family-code">v-maska="{ mask: '!#HHHHHH', tokens: { 'H': { pattern: /[0-9a-fA-F]/, uppercase: true }}}"</p>
</div>
<div class="field">
<div class="field-body">
<div class="field">
<label class="label">Masked input:</label>
<div class="control">
<input v-maska="customMask" class="input">
</div>
</div>
<div class="field">
<label class="label">mask to apply:</label>
<div class="control">
<input class="input" v-model="customMask">
</div>
</div>
</div>
</div>
</form>
</div>
<h2 class="is-size-4">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" type="tel" autocomplete="tel">
</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">Leet speak (tokens with transform)</label>
<div class="control">
<input data-mask="T*" class="transform-masked input">
</div>
<p class="help is-family-code">data-mask="T*"</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 class="field">
<label class="label">Dynamic mask</label>
<div class="control">
<input data-mask='["# cm", "#.# cm", "#.## cm"]' class="masked input">
</div>
<p class="help is-family-code">data-mask='["# cm", "#.# cm", "#.## cm"]'</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">Credit card</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 class="field">
<label class="label">CPF/CNPJ</label>
<div class="control">
<input data-mask='["###.###.###-##", "##.###.###/####-##"]' class="masked input">
</div>
<p class="help is-family-code">data-mask='["###.###.###-##", "##.###.###/####-##"]'</p>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/maska@1.5.1/dist/maska.js"></script>
<script>
// vue
new Vue({
el: '#vue-form',
data: {
phone: '19992345678',
phoneRaw: '',
dotFormat: false,
color: null,
customMask: '#*'
},
computed: {
dateMask: function() {
return this.dotFormat ? '##.##.####' : '##/##/####'
}
}
});
function leetSpeak(char) {
const letters = Object.entries({
'a': '4', 'b': '8', 'e': '3', 'f': 'ph', 'g': '9', 'i': '1', 'o': '0', 's': '5', 't': '7'
}).reduce((acc, [from, to]) => {
acc[from] = to
acc[from.toLocaleUpperCase()] = to
return acc
}, {})
return letters[char] ? letters[char] : char
}
// 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 }}
});
Maska.create('#vanilla-form .transform-masked', {
tokens: { 'T': { pattern: '[ 0-9a-zA-Z]', transform: leetSpeak }}
});
// vanilla destroy
var mask = Maska.create('#vanilla-form .unmasked');
mask.destroy();
</script>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1.0, shrink-to-fit=no, viewport-fit=cover">
<title>maska docs</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%220.9em%22 font-size=%22128%22>*️⃣</text></svg>">
<!-- Themes (light + dark) -->
<link rel="stylesheet" media="(prefers-color-scheme: dark)" href="https://cdn.jsdelivr.net/npm/docsify-themeable@0/dist/css/theme-simple-dark.css">
<link rel="stylesheet" media="(prefers-color-scheme: light)" href="https://cdn.jsdelivr.net/npm/docsify-themeable@0/dist/css/theme-simple.css">
<link rel="stylesheet" href="dist/demo.css">
<style>
:root {
--theme-hue: 42;
--heading-h1-font-size: var(--modular-scale-3);
--heading-h2-font-size: var(--modular-scale-2);
--heading-h3-font-size: var(--modular-scale-1);
}
.sidebar > .app-name {
font-weight: 500;
}
@media screen and (max-width: 500px) {
.markdown-section {
padding-left: 16px;
padding-right: 16px;
}
}
</style>
</head>
<body>
<div id="app"></div>
<script>
window.$docsify = {
name: 'maska',
repo: 'beholdr/maska',
maxLevel: 2
}
</script>
<!-- Required -->
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/docsify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-themeable@0/dist/js/docsify-themeable.min.js"></script>
<!-- Recommended -->
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-tabs@1"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-copy-code@2"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify/lib/plugins/external-script.min.js"></script>
</body>
</html>
-5
View File
@@ -1,5 +0,0 @@
const config = {
testEnvironment: "jsdom",
};
module.exports = config;
+11106
View File
File diff suppressed because it is too large Load Diff
+43 -34
View File
@@ -1,50 +1,59 @@
{
"name": "maska",
"version": "1.5.1",
"description": "Simple zero-dependency input mask for Vue.js and vanilla JS",
"description": "Simple zero-dependency input mask for Vue 2/3 or Vanilla JS",
"keywords": [
"mask",
"input",
"inputmask",
"vue"
],
"author": "Alexander Shabunevich <alex@aether.ru>",
"author": "Alexander Shabunevich",
"homepage": "https://beholdr.github.io/maska/",
"repository": {
"url": "https://github.com/beholdr/maska",
"type": "git"
},
"license": "MIT",
"scripts": {
"serve": "rimraf dist && NODE_ENV=development rollup -c --watch",
"build": "rimraf dist && NODE_ENV=production rollup -c && NODE_ENV=production DISABLE_BABEL=yes rollup -c",
"test": "jest",
"lint": "standard 'src/**'",
"lint:fix": "standard 'src/**' --fix"
},
"main": "dist/maska.umd.js",
"module": "dist/maska.esm.js",
"unpkg": "dist/maska.umd.js",
"jsdelivr": "dist/maska.umd.js",
"types": "types/index.d.ts",
"devDependencies": {
"@babel/core": "^7.7.5",
"@babel/preset-env": "^7.7.5",
"babel-jest": "^27.2.4",
"jest": "^27.2.4",
"rimraf": "^3.0.0",
"rollup": "^2.0.2",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-terser": "^7.0.0",
"standard": "^16.0.3",
"typescript": "^4.1.2"
},
"browserslist": [
"> 0.25%",
"ie 11"
"type": "module",
"files": [
"dist"
],
"babel": {
"presets": [
"@babel/preset-env"
]
"types": "./dist/types/index.d.ts",
"main": "./dist/maska.umd.js",
"module": "./dist/maska.js",
"exports": {
".": {
"import": "./dist/maska.js",
"require": "./dist/maska.umd.js"
}
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:watch": "vite build --watch",
"build:demo": "vite build --config vite.config.demo.ts",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"lint": "ts-standard src",
"lint:fix": "ts-standard --fix src",
"preversion": "npm run build && npm test"
},
"devDependencies": {
"@testing-library/dom": "^8.17.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^14.4.3",
"@types/node": "^18.6.4",
"@vitejs/plugin-vue": "^3.2.0",
"@vitest/coverage-c8": "^0.25.0",
"@vue/test-utils": "^2.2.1",
"happy-dom": "^7.4.0",
"ts-standard": "^12.0.0",
"typescript": "^4.6.4",
"vite": "^3.1.0",
"vite-plugin-banner": "^0.6.1",
"vite-plugin-dts": "^1.6.5",
"vitest": "^0.25.0",
"vue": "^3.2.41",
"vue-live": "^2.1.0"
}
}
-54
View File
@@ -1,54 +0,0 @@
import babel from 'rollup-plugin-babel';
import { terser } from "rollup-plugin-terser";
const { version } = require('./package.json')
const isProduction = process.env.NODE_ENV === 'production'
const useBabel = process.env.DISABLE_BABEL !== 'yes'
const banner = `/*!
* maska v${version}
* (c) 2019-${(new Date()).getFullYear()} Alexander Shabunevich
* Released under the MIT License.
*/`
const getDistFolder = (fileName = '') => `dist/${useBabel ? '' : 'es6/'}${fileName}`
const makeFileName = (format) => getDistFolder(`maska.${format}.js`)
const makeOutputConfig = (format = true) => {
return {
banner,
format,
file: makeFileName(format),
name: 'Maska'
}
}
const plugins = [
useBabel && babel({
exclude: 'node_modules/**'
}),
isProduction && terser()
].filter(Boolean)
export default {
input: 'src/index.js',
plugins,
output: [
{
...makeOutputConfig('esm')
},
{
...makeOutputConfig('umd'),
exports: 'named',
},
{
...makeOutputConfig('umd'),
file: getDistFolder('maska.js'),
exports: 'named',
},
],
watch: {
exclude: 'node_modules/**'
}
};
+153
View File
@@ -0,0 +1,153 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VueLive } from 'vue-live'
import 'vue-live/style.css'
const examples = [
{
label: 'Simple mask',
code: `<input v-maska data-maska="#-#" value="12">`
},
{
label: 'Phone mask',
code: `<input v-maska data-maska="+1 ### ###-##-##">`
},
{
label: 'HEX-color',
code: `<input\n v-maska\n data-maska="!#HHHHHH"\n data-maska-tokens="H:[0-9a-fA-F]"\n>`
},
{
label: 'IP address with optional digits',
code: `<input\n v-maska\n data-maska="100.100.100.100"\n data-maska-tokens="1:[0-2]|0:[0-9]:optional"\n>`
},
{
label: 'Dynamic mask: CPF/CNPJ',
code: `<input\n v-maska\n data-maska="[\n '###.###.###-##',\n '##.###.###/####-##'\n ]"\n>`
},
{
label: 'Cardholder name: via hook',
code: `const options = {\n preProcess: (val) => val.toUpperCase()\n}\n\n<input\n v-maska:[options]\n data-maska="A A"\n data-maska-tokens="A:[A-Z]:multiple"\n>`
},
{
label: 'Cardholder name: via token transform',
code: `const options = {\n tokens: {\n A: {\n pattern: /[A-Z]/,\n multiple: true,\n transform: (chr) => chr.toUpperCase()\n }\n }\n}\n\n<input v-maska:[options] data-maska="A A">`
},
{
label: 'Year: with current year as a limit',
code: `const options = {\n postProcess: (val) => {\n const max = "" + new Date().getFullYear()\n return val > max ? max : val\n }\n}\n\n<input v-maska:[options] data-maska="####">`
},
{
label: 'Money format: repeated and reversed',
code: `<input\n v-maska\n data-maska="9 99#,##"\n data-maska-tokens="9:[0-9]:repeated"\n data-maska-reversed\n>`
}
]
const selectedExample = ref(0)
const code = computed(() => examples[selectedExample.value].code)
</script>
<template>
<div>
<div class="demo-select">
<label for="demo-example-select">Choose mask example:</label>
<select v-model="selectedExample" id="demo-example-select">
<option v-for="(example, idx) in examples" :value="idx">
{{ example.label }}
</option>
</select>
</div>
<VueLive :code="code" />
</div>
</template>
<style>
.demo-select {
border: 1px solid var(--docsifytabs-border-color);
padding: 1rem var(--docsifytabs-content-padding) 1.5rem;
border-bottom: none;
border-radius: 5px 5px 0 0;
}
.demo-select > label {
display: block;
margin-bottom: 4px;
color: var(--search-input-placeholder-color);
}
.demo-select > select {
width: 100%;
padding: 7px;
border-radius: 4px;
color: var(--search-input-color);
border: 1px solid var(--search-input-border-color);
background-color: var(--search-input-background-color);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
appearance: none;
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
}
.VueLive-container {
border: 1px solid var(--docsifytabs-border-color);
margin: var(--docsifytabs-margin);
margin-top: 0;
border-radius: 0 0 5px 5px;
}
.VueLive-container .VueLive-editor {
padding: 2.3rem var(--docsifytabs-content-padding);
background-color: var(--code-theme-background);
border-radius: 0 0 0 5px;
width: 85%;
position: relative;
}
.VueLive-container .VueLive-editor::after {
content: 'Сode';
position: absolute;
top: 0.75em;
right: 0.75em;
opacity: 0.6;
color: inherit;
font-size: var(--font-size-s);
line-height: 1;
}
.VueLive-container .VueLive-editor .prism-editor__editor,
.VueLive-container .VueLive-editor .prism-editor__textarea {
font-family: var(--code-font-family);
}
.VueLive-container .VueLivePreview {
padding: 2rem var(--docsifytabs-content-padding);
border-radius: 0 0 5px 0;
background-color: transparent;
position: relative;
}
.VueLive-container .VueLivePreview::after {
content: 'Result';
position: absolute;
top: 0.75em;
right: var(--docsifytabs-content-padding);
opacity: 0.6;
color: inherit;
font-size: var(--font-size-s);
line-height: 1;
}
.VueLive-container .VueLivePreview input {
font-size: var(--modular-scale-1);
padding: 8px 10px;
border: 1px solid var(--search-input-border-color);
border-radius: 4px;
background-color: var(--search-input-background-color);
color: var(--search-input-color);
width: 100%;
max-width: 100%;
}
@media screen and (max-width: 500px) {
.VueLive-container {
flex-direction: column;
}
.VueLive-container .VueLive-editor,
.VueLive-container .VueLivePreview {
width: 100%;
}
}
</style>
+8
View File
@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import { vMaska } from '..'
import Demo from './Demo.vue'
createApp(Demo)
.directive('maska', vMaska)
.mount('#demo-app')
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
-40
View File
@@ -1,40 +0,0 @@
import Maska from './maska'
import { isString } from './utils'
function getOpts (mask) {
const opts = {}
if (mask.mask) {
opts.mask = Array.isArray(mask.mask) ? JSON.stringify(mask.mask) : mask.mask
opts.tokens = mask.tokens ? { ...mask.tokens } : {}
opts.preprocessor = mask.preprocessor
} else {
opts.mask = Array.isArray(mask) ? JSON.stringify(mask) : mask
}
return opts
}
function needUpdate (mask) {
return !(
(isString(mask.value) && mask.value === mask.oldValue) ||
(Array.isArray(mask.value) && JSON.stringify(mask.value) === JSON.stringify(mask.oldValue)) ||
(mask.value && mask.value.mask && mask.oldValue && mask.oldValue.mask && mask.value.mask === mask.oldValue.mask)
)
}
const directive = () => {
const state = new WeakMap()
return (el, mask) => {
if (!mask.value) return
if (state.has(el) && !needUpdate(mask)) {
return
}
state.set(el, new Maska(el, getOpts(mask.value)))
}
}
export default directive()
+25
View File
@@ -0,0 +1,25 @@
import { Directive } from 'vue'
import { MaskaDetail, MaskInput, MaskInputOptions } from './mask-input'
type MaskaDirective = Directive<HTMLInputElement, MaskaDetail | undefined>
const masks = new WeakMap<HTMLInputElement, MaskInput>()
export const vMaska: MaskaDirective = (el, binding) => {
if (masks.get(el) != null) {
masks.get(el)?.destroy()
}
const opts = { ...(binding.arg as MaskInputOptions) } ?? {}
if (binding.value != null) {
const binded = binding.value
opts.onMaska = (detail: MaskaDetail) => {
binded.masked = detail.masked
binded.unmasked = detail.unmasked
binded.completed = detail.completed
}
}
masks.set(el, new MaskInput(el, opts))
}
-25
View File
@@ -1,25 +0,0 @@
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 (only Vue 2)
if (typeof window !== 'undefined' && window.Vue && window.Vue.use) {
window.Vue.use(install)
}
function create (el, options) {
return new Maska(el, options)
}
export default install
export {
install,
create,
mask,
directive as maska,
tokens
}
+13
View File
@@ -0,0 +1,13 @@
import { Mask, MaskType, MaskOptions } from './mask'
import { MaskInput, MaskInputOptions, MaskaDetail } from './mask-input'
import { vMaska } from './directive'
import { tokens, MaskTokens } from './tokens'
export { Mask, MaskInput, tokens, vMaska }
export type {
MaskaDetail,
MaskInputOptions,
MaskOptions,
MaskTokens,
MaskType
}
+149
View File
@@ -0,0 +1,149 @@
import { Mask, MaskOptions } from './mask'
import { parseMask, parseOpts, parseTokens } from './parser'
export interface MaskInputOptions extends MaskOptions {
onMaska?: (detail: MaskaDetail) => void
preProcess?: (value: string) => string
postProcess?: (value: string) => string
}
export interface MaskaDetail {
masked: string
unmasked: string
completed: boolean
}
export class MaskInput {
readonly items = new Map<HTMLInputElement, Mask>()
constructor (
target: string | NodeListOf<HTMLInputElement> | HTMLInputElement,
readonly options: MaskInputOptions = {}
) {
const { onMaska, preProcess, postProcess, ...opts } = options
if (typeof target === 'string') {
this.init(Array.from(document.querySelectorAll(target)), opts)
} else {
this.init('length' in target ? Array.from(target) : [target], opts)
}
}
destroy (): void {
for (const input of this.items.keys()) {
input.removeEventListener('input', this.inputEvent)
input.removeEventListener('beforeinput', this.beforeinputEvent)
}
this.items.clear()
}
private init (inputs: HTMLInputElement[], defaults: MaskOptions): void {
for (const input of inputs) {
const opts = { ...defaults }
if (input.dataset.maska != null && input.dataset.maska !== '') {
opts.mask = parseMask(input.dataset.maska)
}
if (input.dataset.maskaEager != null) {
opts.eager = parseOpts(input.dataset.maskaEager)
}
if (input.dataset.maskaReversed != null) {
opts.reversed = parseOpts(input.dataset.maskaReversed)
}
if (input.dataset.maskaTokensReplace != null) {
opts.tokensReplace = parseOpts(input.dataset.maskaTokensReplace)
}
if (input.dataset.maskaTokens != null) {
opts.tokens = parseTokens(input.dataset.maskaTokens)
}
const mask = new Mask(opts)
this.items.set(input, mask)
if (input.value !== '') {
this.setMaskedValue(input, input.value)
}
input.addEventListener('input', this.inputEvent)
input.addEventListener('beforeinput', this.beforeinputEvent)
}
}
private readonly beforeinputEvent = (e: Event | InputEvent): void => {
const input = e.target as HTMLInputElement
const mask = this.items.get(input) as Mask
// delete first character in eager mask when it's the only left
if (
mask.eager &&
'inputType' in e &&
e.inputType.startsWith('delete') &&
mask.unmasked(input.value).length <= 1
) {
this.setMaskedValue(input, '')
}
}
private readonly inputEvent = (e: Event | InputEvent): void => {
const input = e.target as HTMLInputElement
const mask = this.items.get(input) as Mask
const valueOld = input.value
const ss = input.selectionStart
const se = input.selectionEnd
let value = valueOld
if (mask.eager) {
const unmasked = mask.unmasked(valueOld)
const maskedUnmasked = mask.masked(unmasked)
if (unmasked === '' && 'data' in e && e.data != null) {
// empty state and something like `space` pressed
value = e.data
} else if (
maskedUnmasked.startsWith(valueOld) ||
mask.completed(unmasked)
) {
value = unmasked
}
}
this.setMaskedValue(input, value)
// set caret position
if ('inputType' in e) {
if (
e.inputType.startsWith('delete') ||
(ss != null && ss < valueOld.length)
) {
input.setSelectionRange(ss, se)
}
}
}
private setMaskedValue (input: HTMLInputElement, value: string): void {
const mask = this.items.get(input) as Mask
if (this.options.preProcess != null) {
value = this.options.preProcess(value)
}
value = mask.masked(value)
if (this.options.postProcess != null) {
value = this.options.postProcess(value)
}
input.value = value
const detail = {
masked: mask.masked(value),
unmasked: mask.unmasked(value),
completed: mask.completed(value)
}
if (this.options.onMaska != null) {
this.options.onMaska(detail)
}
input.dispatchEvent(new CustomEvent<MaskaDetail>('maska', { detail }))
}
}
-120
View File
@@ -1,120 +0,0 @@
import defaultTokens from './tokens'
export default function mask (value, mask, tokens = defaultTokens, masked = true) {
return (processMask(mask).length > 1)
? dynamic(mask)(value, mask, tokens, masked)
: process(value, mask, tokens, masked)
}
function processMask (mask) {
try {
return JSON.parse(mask)
} catch {
return [mask]
}
}
function dynamic (mask) {
const masks = processMask(mask).sort((a, b) => a.length - b.length)
return function (value, mask, tokens, masked = true) {
const processed = masks.map(m => process(value, m, tokens, false))
const last = processed.pop()
for (const i in masks) {
if (checkMask(last, masks[i], tokens)) {
return process(value, masks[i], tokens, masked)
}
}
return '' // empty masks
}
function checkMask (variant, mask, tokens) {
for (const tok in tokens) {
if (tokens[tok].escape) {
mask = mask.replace(new RegExp(tok + '.{1}', 'g'), '')
}
}
return (mask.split('').filter(el => tokens[el] && tokens[el].pattern).length >= variant.length)
}
}
function process (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++
// check next char
if (masked && mask[im]) {
if (!tokens[mask[im]]) {
ret += mask[im]
im++
} else if (tokens[mask[im]] && tokens[mask[im]].escape) {
ret += mask[im + 1]
im = im + 2
}
}
}
iv++
} else if (token && token.repeat) {
const tokenPrev = tokens[mask[im - 1]]
if (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 parenthesis
while (masked && im < mask.length) { // eslint-disable-line no-unmodified-loop-condition
const maskCharRest = mask[im]
if (tokens[maskCharRest]) {
rest = ''
break
}
rest += maskCharRest
im++
}
return ret + rest
}
/**
*
* @param {String} value
* @param {'uppercase' | 'lowercase' | 'transform'} token
*/
function tokenTransform (value, token) {
if (token.transform) {
value = token.transform(value)
}
if (token.uppercase) {
return value.toLocaleUpperCase()
} else if (token.lowercase) {
return value.toLocaleLowerCase()
}
return value
}
+208
View File
@@ -0,0 +1,208 @@
import { MaskTokens, tokens } from './tokens'
export type MaskType = string | string[] | ((input: string) => string)
export interface MaskOptions {
mask?: MaskType
tokens?: MaskTokens
tokensReplace?: boolean
eager?: boolean
reversed?: boolean
}
export class Mask {
readonly mask: MaskType = ''
readonly tokens = tokens
readonly eager = false
readonly reversed = false
private readonly memo = new Map()
constructor (opts: MaskOptions = {}) {
if (opts.tokens != null) {
opts.tokens = (opts.tokensReplace as boolean)
? { ...opts.tokens }
: { ...tokens, ...opts.tokens }
for (const token of Object.values(opts.tokens)) {
if (typeof token.pattern === 'string') {
token.pattern = new RegExp(token.pattern)
}
}
}
if (opts.mask == null) {
opts.mask = ''
} else if (typeof opts.mask === 'object') {
if (opts.mask.length > 1) {
opts.mask.sort((a, b) => a.length - b.length)
} else {
opts.mask = opts.mask[0] ?? ''
}
}
Object.assign(this, opts)
}
masked (value: string): string {
return this.process(value, this.findMask(value))
}
unmasked (value: string): string {
return this.process(value, this.findMask(value), false)
}
completed (value: string): boolean {
const length = this.process(value, this.findMask(value)).length
if (typeof this.mask === 'string') {
return length >= this.mask.length
} else if (typeof this.mask === 'function') {
return length >= this.findMask(value).length
} else {
return (
this.mask.filter((m) => length >= m.length).length === this.mask.length
)
}
}
private findMask (value: string): string {
if (typeof this.mask === 'string') {
return this.mask
} else if (typeof this.mask === 'function') {
return this.mask(value)
}
const last = this.process(value, this.mask.slice(-1).pop() ?? '', false)
return (
this.mask.find(
(mask) => this.process(value, mask, false).length >= last.length
) ?? ''
)
}
private escapeMask (maskRaw: string): {
mask: string
escaped: number[]
} {
const chars: string[] = []
const escaped: number[] = []
maskRaw.split('').forEach((ch, i) => {
if (ch === '!' && maskRaw[i - 1] !== '!') {
escaped.push(i - escaped.length)
} else {
chars.push(ch)
}
})
return { mask: chars.join(''), escaped }
}
private process (value: string, maskRaw: string, masked = true): string {
const key = `value=${value},mask=${maskRaw},masked=${masked ? 1 : 0}`
if (this.memo.has(key)) return this.memo.get(key)
const { mask, escaped } = this.escapeMask(maskRaw)
const result: string[] = []
const offset = this.reversed ? -1 : 1
const method = this.reversed ? 'unshift' : 'push'
const lastMaskChar = this.reversed ? 0 : mask.length - 1
const check = this.reversed
? () => m > -1 && v > -1
: () => m < mask.length && v < value.length
const notLastMaskChar = (m: number): boolean =>
(!this.reversed && m <= lastMaskChar) ||
(this.reversed && m >= lastMaskChar)
let lastRawMaskChar
let repeatedPos = -1
let m = this.reversed ? mask.length - 1 : 0
let v = this.reversed ? value.length - 1 : 0
while (check()) {
const maskChar = mask.charAt(m)
const token = this.tokens[maskChar]
const valueChar =
token?.transform != null
? token.transform(value.charAt(v))
: value.charAt(v)
if (!escaped.includes(m) && token != null) {
if (valueChar.match(token.pattern) != null) {
result[method](valueChar)
if (token.repeated as boolean) {
if (repeatedPos === -1) {
repeatedPos = m
} else if (m === lastMaskChar && m !== repeatedPos) {
m = repeatedPos - offset
}
if (lastMaskChar === repeatedPos) {
m -= offset
}
} else if (token.multiple as boolean) {
m -= offset
}
m += offset
} else if (token.multiple as boolean) {
const hasValue = result[v - offset]?.match(token.pattern) != null
const nextMask = mask.charAt(m + offset)
if (hasValue && nextMask !== '' && this.tokens[nextMask] == null) {
m += offset
v -= offset
} else {
result[method]('')
}
} else if (valueChar === lastRawMaskChar) {
// matched the last untranslated (raw) mask character that we encountered
// likely an insert offset the mask character from the last entry;
// fall through and only increment v
lastRawMaskChar = undefined
} else if (token.optional as boolean) {
m += offset
v -= offset
} else {
// invalid input
}
v += offset
} else {
if (masked && !this.eager) {
result[method](maskChar)
}
if (valueChar === maskChar && !this.eager) {
v += offset
} else {
lastRawMaskChar = maskChar
}
if (!this.eager) {
m += offset
}
}
if (this.eager) {
while (
notLastMaskChar(m) &&
(this.tokens[mask.charAt(m)] == null || escaped.includes(m))
) {
if (masked) {
result[method](mask.charAt(m))
} else if (mask.charAt(m) === value.charAt(v)) {
v += offset
}
m += offset
}
}
}
this.memo.set(key, result.join(''))
return this.memo.get(key)
}
}
-101
View File
@@ -1,101 +0,0 @@
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.preprocessor != null && typeof opts.preprocessor !== 'function') {
throw new Error('Maska: preprocessor must be a function')
}
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 },
preprocessor: opts.preprocessor
}
this._el = isString(el) ? document.querySelectorAll(el) : !el.length ? [el] : el
this.inputEvent = (e) => this.updateValue(e.target, e)
this.init()
}
init () {
for (let i = 0; i < this._el.length; i++) {
const el = findInputElement(this._el[i])
if (this._opts.mask && (!el.dataset.mask || el.dataset.mask !== this._opts.mask)) {
el.dataset.mask = this._opts.mask
}
setTimeout(() => this.updateValue(el), 0)
if (!el.dataset.maskInited) {
el.dataset.maskInited = true
el.addEventListener('input', this.inputEvent)
el.addEventListener('beforeinput', this.beforeInput)
}
}
}
destroy () {
for (let i = 0; i < this._el.length; i++) {
const el = findInputElement(this._el[i])
el.removeEventListener('input', this.inputEvent)
el.removeEventListener('beforeinput', this.beforeInput)
delete el.dataset.mask
delete el.dataset.maskInited
}
}
updateValue (el, evt) {
if (!el || !el.type) return
const wrongNum = el.type.match(/^number$/i) && el.validity.badInput
if ((!el.value && !wrongNum) || !el.dataset.mask) {
el.dataset.maskRawValue = ''
this.dispatch('maska', el, evt)
return
}
let position = el.selectionEnd
const oldValue = el.value
const digit = oldValue[position - 1]
el.dataset.maskRawValue = mask(el.value, el.dataset.mask, this._opts.tokens, false)
let elValue = el.value
if (this._opts.preprocessor) {
elValue = this._opts.preprocessor(elValue)
}
el.value = mask(elValue, el.dataset.mask, this._opts.tokens)
if (evt && evt.inputType === 'insertText' && position === oldValue.length) {
position = el.value.length
}
fixInputSelection(el, position, digit)
this.dispatch('maska', el, evt)
if (el.value !== oldValue) {
this.dispatch('input', el, evt)
}
}
beforeInput (e) {
if (e && e.target && e.target.type && e.target.type.match(/^number$/i) && e.data && isNaN(e.target.value + e.data)) {
e.preventDefault()
}
}
dispatch (name, el, evt) {
el.dispatchEvent(event(name, (evt && evt.inputType) || null))
}
}
+29
View File
@@ -0,0 +1,29 @@
import { MaskType } from './mask'
import { MaskTokens } from './tokens'
const parseJson = (value: string): any => JSON.parse(value.replaceAll("'", '"'))
export const parseOpts = (value: string): boolean =>
value !== '' ? Boolean(JSON.parse(value)) : true
export const parseMask = (value: string): MaskType =>
value.startsWith('[') && value.endsWith(']') ? parseJson(value) : value
export const parseTokens = (value: string): MaskTokens => {
if (value.startsWith('{') && value.endsWith('}')) {
return parseJson(value)
}
const tokens: MaskTokens = {}
value.split('|').forEach((token) => {
const parts = token.split(':')
tokens[parts[0]] = {
pattern: new RegExp(parts[1]),
optional: parts[2] === 'optional',
multiple: parts[2] === 'multiple',
repeated: parts[2] === 'repeated'
}
})
return tokens
}
-10
View File
@@ -1,10 +0,0 @@
/* 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 }
}
+15
View File
@@ -0,0 +1,15 @@
interface MaskToken {
pattern: RegExp
multiple?: boolean
optional?: boolean
repeated?: boolean
transform?: (char: string) => string
}
export type MaskTokens = Record<string, MaskToken>
export const tokens: MaskTokens = {
'#': { pattern: /[0-9]/ },
'@': { pattern: /[a-zA-Z]/ },
'*': { pattern: /[a-zA-Z0-9]/ }
}
-40
View File
@@ -1,40 +0,0 @@
/* global HTMLInputElement */
function event (name, inputType = null) {
const event = document.createEvent('Event')
event.initEvent(name, true, true)
if (inputType) {
event.inputType = inputType
}
return event
}
function findInputElement (el) {
return (el instanceof HTMLInputElement) ? el : el.querySelector('input') || el
}
function fixInputSelection (el, position, digit) {
while (position && position < el.value.length && el.value.charAt(position - 1) !== digit) {
position++
}
const selectionRange = el.type ? el.type.match(/^(text|search|password|tel|url)$/i) : !el.type
if (selectionRange && 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
}
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { reactive } from 'vue'
import { MaskaDetail, vMaska } from '../../src'
const binded = reactive<Partial<MaskaDetail>>({})
</script>
<template>
<input v-maska="binded" data-maska="#-#" value="123" />
<div>{{ binded.masked }}</div>
</template>
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { reactive } from 'vue'
import { MaskaDetail, vMaska } from '../../src'
const binded = reactive<Partial<MaskaDetail>>({})
</script>
<template>
<input v-maska="binded" data-maska="#-#" />
<div>{{ binded.masked }}</div>
</template>
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { reactive } from 'vue'
import { MaskaDetail, vMaska } from '../../src'
const binded = reactive<Partial<MaskaDetail>>({})
</script>
<template>
<input v-maska="binded" data-maska="#-#" />
<div>{{ binded.unmasked }}</div>
</template>
+12
View File
@@ -0,0 +1,12 @@
<script setup lang="ts">
import { reactive } from 'vue'
import { MaskaDetail, vMaska } from '../../src'
const binded = reactive<Partial<MaskaDetail>>({})
</script>
<template>
<input v-maska="binded" data-maska="#-#-#" />
<div v-if="binded.completed">Completed</div>
<div v-else>Uncompleted</div>
</template>
+21
View File
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { reactive } from 'vue'
import { vMaska, MaskaDetail, MaskInputOptions } from '../../src'
const binded = reactive<Partial<MaskaDetail>>({})
const config = reactive<MaskInputOptions>({
mask: 'A A',
tokens: {
A: {
pattern: /[A-Z]/,
multiple: true,
transform: (chr) => chr.toUpperCase()
}
}
})
</script>
<template>
<input v-maska:[config]="binded" />
<div>{{ binded.masked }}</div>
</template>
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts">
import { vMaska } from '../../src'
</script>
<template>
<input v-maska data-maska="#-#" data-maska-eager />
</template>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
import { ref } from 'vue'
import { vMaska } from '../../src'
const mask = ref('["#--#", "#-#--#"]')
</script>
<template>
<input v-maska :data-maska="mask" />
</template>
+13
View File
@@ -0,0 +1,13 @@
<script setup lang="ts">
import { MaskaDetail, vMaska } from '../../src'
const emit = defineEmits(['mask'])
const onMaska = (e: CustomEvent<MaskaDetail>) => {
emit('mask', e.detail)
}
</script>
<template>
<input v-maska data-maska="#-#" @maska="onMaska" />
</template>
+12
View File
@@ -0,0 +1,12 @@
<script setup lang="ts">
import { reactive } from 'vue'
import { MaskInputOptions, vMaska } from '../../src'
const config = reactive<MaskInputOptions>({
preProcess: (val) => val.toUpperCase()
})
</script>
<template>
<input v-maska:[config] data-maska="A A" data-maska-tokens="A:[A-Z]:multiple" />
</template>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import { vMaska } from '../../src'
export default {
data() {
return {
binded: {
masked: ''
}
}
},
directives: {
maska: vMaska
}
}
</script>
<template>
<input v-maska="binded" data-maska="#-#" />
<div>{{ binded.masked }}</div>
</template>
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts">
import { vMaska } from '../../src'
</script>
<template>
<input v-maska data-maska="#-#" />
</template>
+152
View File
@@ -0,0 +1,152 @@
import { nextTick } from 'vue'
import { expect, test } from 'vitest'
import { mount } from '@vue/test-utils'
import BindInitial from './components/BindInitial.vue'
import BindMasked from './components/BindMasked.vue'
import BindUnmasked from './components/BindUnmasked.vue'
import Completed from './components/Completed.vue'
import Config from './components/Config.vue'
import DataAttr from './components/DataAttr.vue'
import Dynamic from './components/Dynamic.vue'
import Events from './components/Events.vue'
import Hooks from './components/Hooks.vue'
import Options from './components/Options.vue'
import Simple from './components/Simple.vue'
test('simple directive', async () => {
const wrapper = mount(Simple)
expect(wrapper.exists).toBeTruthy()
const input = wrapper.get('input')
await input.setValue('123')
expect(input.element.value).toBe('1-2')
})
test('data-attr', async () => {
const wrapper = mount(DataAttr)
const input = wrapper.get('input')
await input.setValue('1')
expect(input.element.value).toBe('1-')
await input.setValue('123')
expect(input.element.value).toBe('1-2')
})
test('dynamic mask', async () => {
const wrapper = mount(Dynamic)
const input = wrapper.get('input')
await input.setValue('12')
expect(input.element.value).toBe('1--2')
await input.setValue('123')
expect(input.element.value).toBe('1-2--3')
})
test('initial value', async () => {
const wrapper = mount(BindInitial)
const input = wrapper.get('input')
await nextTick()
expect(input.element.value).toBe('1-2')
expect(wrapper.get('div').element.textContent).toBe('1-2')
})
test('bind masked', async () => {
const wrapper = mount(BindMasked)
const input = wrapper.get('input')
await input.setValue('123')
expect(input.element.value).toBe('1-2')
expect(wrapper.get('div').element.textContent).toBe('1-2')
})
test('bind unmasked', async () => {
const wrapper = mount(BindUnmasked)
const input = wrapper.get('input')
await input.setValue('123')
expect(input.element.value).toBe('1-2')
expect(wrapper.get('div').element.textContent).toBe('12')
})
test('bind completed', async () => {
const wrapper = mount(Completed)
const input = wrapper.get('input')
await input.setValue('12')
await nextTick()
expect(input.element.value).toBe('1-2')
expect(wrapper.get('div').element.textContent).toBe('Uncompleted')
await input.setValue('123')
await nextTick()
expect(input.element.value).toBe('1-2-3')
expect(wrapper.get('div').element.textContent).toBe('Completed')
})
test('config and bind', async () => {
const wrapper = mount(Config)
const input = wrapper.get('input')
await input.setValue('1')
expect(input.element.value).toBe('')
expect(wrapper.get('div').element.textContent).toBe('')
await input.setValue('ab')
expect(input.element.value).toBe('AB')
expect(wrapper.get('div').element.textContent).toBe('AB')
await input.setValue('ab cd ')
expect(input.element.value).toBe('AB CD')
expect(wrapper.get('div').element.textContent).toBe('AB CD')
await input.setValue('ab cd1')
expect(input.element.value).toBe('AB CD')
expect(wrapper.get('div').element.textContent).toBe('AB CD')
})
test('hooks', async () => {
const wrapper = mount(Hooks)
const input = wrapper.get('input')
await input.setValue('1')
expect(input.element.value).toBe('')
await input.setValue('ab')
expect(input.element.value).toBe('AB')
await input.setValue('ab cd ')
expect(input.element.value).toBe('AB CD')
await input.setValue('ab cd1')
expect(input.element.value).toBe('AB CD')
})
test('events', async () => {
const wrapper = mount(Events)
const input = wrapper.get('input')
await input.setValue('1')
expect(wrapper.emitted()).toHaveProperty('mask')
expect(wrapper.emitted('mask')).toHaveLength(1)
expect(wrapper.emitted('mask')[0][0]).toHaveProperty('completed', false)
await input.setValue('12')
expect(wrapper.emitted('mask')).toHaveLength(2)
expect(wrapper.emitted('mask')[1][0]).toHaveProperty('completed', true)
})
test('options api component', async () => {
const wrapper = mount(Options)
const input = wrapper.get('input')
await input.setValue('123')
expect(input.element.value).toBe('1-2')
expect(wrapper.get('div').element.textContent).toBe('1-2')
})
File diff suppressed because it is too large Load Diff
-224
View File
@@ -1,224 +0,0 @@
import mask from './../src/mask'
import tokens from './../src/tokens'
import Maska from './../src/maska'
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('1-23A #-##!A', () => {
expect(mask('123', '#-##!A', tokens)).toBe('1-23A')
})
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 #!!#', () => {
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')
expect(mask('123abc ', '# (A*)', tokens)).toBe('1 (ABC)')
})
test('Raw 123abc ##(A*)', () => {
expect(mask('123abc', '##(A*)', tokens, false)).toBe('12ABC')
})
test('Dynamic floats', () => {
expect(mask('1', '["# cm", "#.# cm", "#.## cm"]', tokens)).toBe('1 cm')
expect(mask('12', '["# cm", "#.# cm", "#.## cm"]', tokens)).toBe('1.2 cm')
expect(mask('123', '["# cm", "#.# cm", "#.## cm"]', tokens)).toBe('1.23 cm')
})
test('Dynamic CPF/CNPJ', () => {
expect(mask('12345678901', '["###.###.###-##", "##.###.###/####-##"]', tokens)).toBe('123.456.789-01')
expect(mask('12345678901234', '["###.###.###-##", "##.###.###/####-##"]', tokens)).toBe('12.345.678/9012-34')
})
test('Dynamic boundaries', () => {
expect(mask('12', '["!###", "!###-##", "!###-##-##"]', tokens)).toBe('#12')
expect(mask('1234', '["!###", "!###-##", "!###-##-##"]', tokens)).toBe('#12-34')
expect(mask('1234567', '["!###", "!###-##", "!###-##-##"]', tokens)).toBe('#12-34-56')
expect(mask('123', '["###", "###-##", "###.##.##"]', tokens)).toBe('123')
expect(mask('12345', '["###", "###-##", "###.##.##"]', tokens)).toBe('123-45')
expect(mask('12345678', '["###", "###-##", "###.##.##"]', tokens)).toBe('123.45.67')
})
test('Custom transform: odd number -> 1, even number -> 0', () => {
// isOdd
const transform = (numberLike) => String(Number(numberLike) % 2)
expect((mask('1234567890', '#*', {
...tokens,
...{
'#': {
pattern: /[0-9]/,
transform
}
}
}))).toBe('1010101010')
})
function transliterate(char) {
const rule = Object.entries({
a: 'а',
b: 'в',
k: 'к',
m: 'м',
h: 'н',
o: 'о',
p: 'р',
c: 'с',
t: 'т',
y: 'у',
x: 'х',
}).reduce((acc, [from, to]) => {
acc[from] = to
acc[from.toLocaleUpperCase()] = to.toLocaleUpperCase()
return acc
}, {})
return rule[char]
}
test('Custom transform: transliterate abkTYX -> авкТУХ', () => {
expect(mask('abkTYX', 'T*', {
...tokens,
...{
'T': {
pattern: /[a-zA-Z]/,
transform: transliterate
}
}
})).toBe('авкТУХ')
})
test('Custom transform with `uppercase` and `lowercase` enabled: abkTYX -> АВКТУХ, abkTYX -> авктух', () => {
expect(mask('abkTYX', 'T*', {
...tokens,
...{
'T': {
pattern: /[a-zA-Z]/,
transform: transliterate,
uppercase: true,
}
}
})).toBe('АВКТУХ')
expect(mask('abkTYX', 'T*', {
...tokens,
...{
'T': {
pattern: /[a-zA-Z]/,
transform: transliterate,
lowercase: true,
}
}
})).toBe('авктух')
})
test('Custom value preprocessing', () => {
const elem = document.createElement('input')
document.body.appendChild(elem)
const mask = new Maska(elem, {
mask: 'S*',
preprocessor: function(val) {
return val.toLocaleUpperCase()
}
})
elem.value = "abkTYX"
elem.dispatchEvent(new Event('input', {bubbles: true}))
expect(elem.value).toBe("ABKTYX")
})
+790
View File
@@ -0,0 +1,790 @@
import { expect, test } from 'vitest'
import { Mask } from '../src/mask'
test('null mask', () => {
// @ts-ignore
const mask = new Mask({ mask: null })
expect(mask.masked('1a')).toBe('')
})
test('undefined mask', () => {
const mask = new Mask({ mask: undefined })
expect(mask.masked('1a')).toBe('')
})
test('@ @ mask', () => {
const mask = new Mask({ mask: '@ @' })
expect(mask.masked('1')).toBe('')
expect(mask.masked('a')).toBe('a')
expect(mask.masked('ab')).toBe('a b')
expect(mask.masked('abc')).toBe('a b')
expect(mask.masked('1abc')).toBe('a b')
expect(mask.unmasked('1abc')).toBe('ab')
expect(mask.completed('a')).toBe(false)
expect(mask.completed('ab')).toBe(true)
})
test('@ @ eager mask', () => {
const mask = new Mask({ mask: '@ @', eager: true })
expect(mask.masked('1')).toBe('')
expect(mask.masked('a')).toBe('a ')
expect(mask.masked('ab')).toBe('a b')
expect(mask.masked('abc')).toBe('a b')
expect(mask.masked('1abc')).toBe('a b')
expect(mask.unmasked('1abc')).toBe('ab')
})
test('#.# mask', () => {
const mask = new Mask({ mask: '#.#' })
expect(mask.masked('1')).toBe('1')
expect(mask.masked('1.')).toBe('1.')
expect(mask.masked('12')).toBe('1.2')
expect(mask.masked('123')).toBe('1.2')
expect(mask.masked('a123')).toBe('1.2')
expect(mask.unmasked('a123')).toBe('12')
})
test('#.# eager mask', () => {
const mask = new Mask({ mask: '#.#', eager: true })
expect(mask.masked('1')).toBe('1.')
expect(mask.masked('1.')).toBe('1.')
expect(mask.masked('1 ')).toBe('1.')
expect(mask.masked('12')).toBe('1.2')
expect(mask.masked('123')).toBe('1.2')
expect(mask.masked('a123')).toBe('1.2')
expect(mask.unmasked('a123')).toBe('12')
})
test('@@-## mask', () => {
const mask = new Mask({ mask: '@@-##' })
expect(mask.masked('12')).toBe('')
expect(mask.masked('ab')).toBe('ab')
expect(mask.masked('ab12')).toBe('ab-12')
expect(mask.masked('ab-12')).toBe('ab-12')
expect(mask.masked('abc123')).toBe('ab-12')
expect(mask.masked('a1b2a1b2')).toBe('ab-21')
expect(mask.unmasked('a1b2a1b2')).toBe('ab21')
})
test('@@-## eager mask', () => {
const mask = new Mask({ mask: '@@-##', eager: true })
expect(mask.masked('12')).toBe('')
expect(mask.masked('ab')).toBe('ab-')
expect(mask.masked('ab12')).toBe('ab-12')
expect(mask.masked('ab-12')).toBe('ab-12')
expect(mask.masked('abc123')).toBe('ab-12')
expect(mask.masked('a1b2a1b2')).toBe('ab-21')
expect(mask.unmasked('a1b2a1b2')).toBe('ab21')
})
test('(#) mask', () => {
const mask = new Mask({ mask: '(#)' })
expect(mask.masked('1')).toBe('(1')
expect(mask.masked('(1')).toBe('(1')
expect(mask.masked('1 ')).toBe('(1)')
expect(mask.masked('12')).toBe('(1)')
expect(mask.masked('a12')).toBe('(1)')
expect(mask.unmasked('a123')).toBe('1')
})
test('(#) eager mask', () => {
const mask = new Mask({ mask: '(#)', eager: true })
expect(mask.masked('1')).toBe('(1)')
expect(mask.masked('(1')).toBe('(1)')
expect(mask.masked('12')).toBe('(1)')
expect(mask.masked('a12')).toBe('(1)')
expect(mask.unmasked('a123')).toBe('1')
})
test('#-#--# mask', () => {
const mask = new Mask({ mask: '#-#--#' })
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe('1-2')
expect(mask.masked('123')).toBe('1-2--3')
expect(mask.masked('a1234')).toBe('1-2--3')
expect(mask.unmasked('a1234')).toBe('123')
})
test('#-#--# eager mask', () => {
const mask = new Mask({ mask: '#-#--#', eager: true })
expect(mask.masked('1')).toBe('1-')
expect(mask.masked('12')).toBe('1-2--')
expect(mask.masked('123')).toBe('1-2--3')
expect(mask.masked('a1234')).toBe('1-2--3')
expect(mask.unmasked('a1234')).toBe('123')
})
test('!##.# mask', () => {
const mask = new Mask({ mask: '!##.#' })
expect(mask.masked('1')).toBe('#1')
expect(mask.masked('#1')).toBe('#1')
expect(mask.masked('12')).toBe('#1.2')
expect(mask.masked('1.2')).toBe('#1.2')
expect(mask.masked('#1.2')).toBe('#1.2')
expect(mask.masked('123')).toBe('#1.2')
expect(mask.masked('a123')).toBe('#1.2')
expect(mask.unmasked('a123')).toBe('12')
})
test('!##.# eager mask', () => {
const mask = new Mask({ mask: '!##.#', eager: true })
expect(mask.masked('1')).toBe('#1.')
expect(mask.masked('#1')).toBe('#1.')
expect(mask.masked('12')).toBe('#1.2')
expect(mask.masked('1.2')).toBe('#1.2')
expect(mask.masked('#1.2')).toBe('#1.2')
expect(mask.masked('123')).toBe('#1.2')
expect(mask.masked('a123')).toBe('#1.2')
expect(mask.unmasked('a123')).toBe('12')
})
test('0#.# mask', () => {
const mask = new Mask({ mask: '0#.#' })
expect(mask.masked('1')).toBe('01')
expect(mask.masked('01')).toBe('01')
expect(mask.masked('12')).toBe('01.2')
expect(mask.masked('1.2')).toBe('01.2')
expect(mask.masked('01.2')).toBe('01.2')
expect(mask.masked('123')).toBe('01.2')
expect(mask.masked('a123')).toBe('01.2')
expect(mask.unmasked('a123')).toBe('12')
})
test('0#.# eager mask', () => {
const mask = new Mask({ mask: '0#.#', eager: true })
expect(mask.masked('1')).toBe('01.')
expect(mask.masked('01')).toBe('00.1')
expect(mask.masked('12')).toBe('01.2')
expect(mask.masked('1.2')).toBe('01.2')
expect(mask.masked('01.2')).toBe('00.1')
expect(mask.masked('123')).toBe('01.2')
expect(mask.masked('a123')).toBe('01.2')
expect(mask.unmasked('a123')).toBe('12')
})
test('#.#!* mask', () => {
const mask = new Mask({ mask: '#.#!*' })
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe('1.2')
expect(mask.masked('1.2')).toBe('1.2')
expect(mask.masked('12*')).toBe('1.2*')
expect(mask.masked('1.2*')).toBe('1.2*')
expect(mask.masked('1.2 ')).toBe('1.2*')
expect(mask.masked('123')).toBe('1.2*')
expect(mask.masked('a123')).toBe('1.2*')
expect(mask.unmasked('a123')).toBe('12')
})
test('#.#!* eager mask', () => {
const mask = new Mask({ mask: '#.#!*', eager: true })
expect(mask.masked('1')).toBe('1.')
expect(mask.masked('12')).toBe('1.2*')
expect(mask.masked('1.2')).toBe('1.2*')
expect(mask.masked('12*')).toBe('1.2*')
expect(mask.masked('1.2*')).toBe('1.2*')
expect(mask.masked('123')).toBe('1.2*')
expect(mask.masked('a123')).toBe('1.2*')
expect(mask.unmasked('a123')).toBe('12')
})
test('#.#!** mask', () => {
const mask = new Mask({ mask: '#.#!**' })
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe('1.2')
expect(mask.masked('1.2')).toBe('1.2')
expect(mask.masked('12*')).toBe('1.2*')
expect(mask.masked('1.2*')).toBe('1.2*')
expect(mask.masked('1.2 ')).toBe('1.2*')
expect(mask.masked('123')).toBe('1.2*3')
expect(mask.masked('12*3')).toBe('1.2*3')
expect(mask.masked('a123')).toBe('1.2*3')
expect(mask.unmasked('a123')).toBe('123')
})
test('#.#!** eager mask', () => {
const mask = new Mask({ mask: '#.#!**', eager: true })
expect(mask.masked('1')).toBe('1.')
expect(mask.masked('12')).toBe('1.2*')
expect(mask.masked('1.2')).toBe('1.2*')
expect(mask.masked('12*')).toBe('1.2*')
expect(mask.masked('1.2*')).toBe('1.2*')
expect(mask.masked('1.2*3')).toBe('1.2*3')
expect(mask.masked('123')).toBe('1.2*3')
expect(mask.masked('a123')).toBe('1.2*3')
expect(mask.unmasked('a123')).toBe('123')
})
test('0#!-# mask', () => {
const mask = new Mask({ mask: '0#!-#' })
expect(mask.masked('a')).toBe('0')
expect(mask.masked('0')).toBe('0')
expect(mask.masked('1')).toBe('01')
expect(mask.masked('12')).toBe('01-2')
expect(mask.masked('01-2')).toBe('01-2')
expect(mask.masked('123')).toBe('01-2')
expect(mask.masked('a123')).toBe('01-2')
expect(mask.unmasked('a123')).toBe('12')
})
test('!0#!-# eager mask', () => {
const mask = new Mask({ mask: '!0#!-#', eager: true })
expect(mask.masked('a')).toBe('0')
expect(mask.masked('0')).toBe('00-')
expect(mask.masked('1')).toBe('01-')
expect(mask.masked('12')).toBe('01-2')
expect(mask.masked('01-2')).toBe('00-1')
expect(mask.masked('123')).toBe('01-2')
expect(mask.masked('a123')).toBe('01-2')
expect(mask.unmasked('a123')).toBe('12')
})
test('#2 ## mask', () => {
const mask = new Mask({ mask: '#2 ##' })
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe('12')
expect(mask.masked('12 ')).toBe('12 ')
expect(mask.masked('13')).toBe('12 3')
expect(mask.masked('123')).toBe('12 3')
expect(mask.masked('134')).toBe('12 34')
expect(mask.masked('1234')).toBe('12 34')
expect(mask.masked('1345')).toBe('12 34')
expect(mask.masked('12345')).toBe('12 34')
expect(mask.masked('a1')).toBe('1')
expect(mask.masked('a13')).toBe('12 3')
expect(mask.unmasked('12345')).toBe('134')
})
test('#2 ## eager mask', () => {
const mask = new Mask({ mask: '#2 ##', eager: true })
expect(mask.masked('1')).toBe('12 ')
expect(mask.masked('12')).toBe('12 2')
expect(mask.masked('12 ')).toBe('12 2')
expect(mask.masked('13')).toBe('12 3')
expect(mask.masked('123')).toBe('12 23')
expect(mask.masked('134')).toBe('12 34')
expect(mask.masked('1234')).toBe('12 23')
expect(mask.masked('1345')).toBe('12 34')
expect(mask.masked('12345')).toBe('12 23')
expect(mask.masked('a1')).toBe('12 ')
expect(mask.masked('a13')).toBe('12 3')
expect(mask.unmasked('12345')).toBe('134')
})
test('(#) 3## mask', () => {
const mask = new Mask({ mask: '(#) 3##' })
expect(mask.masked('1')).toBe('(1')
expect(mask.masked('12')).toBe('(1) 32')
expect(mask.masked('123')).toBe('(1) 323')
expect(mask.masked('1234')).toBe('(1) 323')
expect(mask.masked('13')).toBe('(1) 3')
expect(mask.masked('134')).toBe('(1) 34')
expect(mask.masked('(1) 23')).toBe('(1) 323')
expect(mask.masked('(1) 34')).toBe('(1) 34')
expect(mask.unmasked('1')).toBe('1')
expect(mask.unmasked('1234')).toBe('123')
expect(mask.unmasked('(1) 3')).toBe('1')
expect(mask.unmasked('(1) 32')).toBe('12')
})
test('(#) 3## eager mask', () => {
const mask = new Mask({ mask: '(#) 3##', eager: true })
expect(mask.masked('1')).toBe('(1) 3')
expect(mask.masked('12')).toBe('(1) 32')
expect(mask.masked('123')).toBe('(1) 323')
expect(mask.masked('1234')).toBe('(1) 323')
expect(mask.masked('13')).toBe('(1) 33')
expect(mask.masked('134')).toBe('(1) 334')
expect(mask.masked('(1) 23')).toBe('(1) 323')
expect(mask.masked('(1) 34')).toBe('(1) 334')
expect(mask.unmasked('1')).toBe('1')
expect(mask.unmasked('1234')).toBe('123')
expect(mask.unmasked('(1) 3')).toBe('1')
expect(mask.unmasked('(1) 32')).toBe('12')
})
test('(1) 2# mask', () => {
const mask = new Mask({ mask: '(1) 2#' })
expect(mask.masked(' ')).toBe('(1) ')
expect(mask.masked('.')).toBe('(1) 2')
expect(mask.masked('1')).toBe('(1')
expect(mask.masked('12')).toBe('(1) 2')
expect(mask.masked('123')).toBe('(1) 23')
expect(mask.masked('1234')).toBe('(1) 23')
expect(mask.masked('13')).toBe('(1) 23')
expect(mask.masked('(1) 23')).toBe('(1) 23')
expect(mask.masked('(1) 34')).toBe('(1) 23')
expect(mask.masked('2')).toBe('(1) 2')
expect(mask.masked('23')).toBe('(1) 23')
expect(mask.masked('3')).toBe('(1) 23')
expect(mask.masked('4')).toBe('(1) 24')
expect(mask.unmasked('1')).toBe('')
expect(mask.unmasked('12')).toBe('')
expect(mask.unmasked('123')).toBe('3')
expect(mask.unmasked('(1) 23')).toBe('3')
})
test('(1) 2## eager mask', () => {
const mask = new Mask({ mask: '(1) 2##', eager: true })
expect(mask.masked(' ')).toBe('(1) 2')
expect(mask.masked('.')).toBe('(1) 2')
expect(mask.masked('1')).toBe('(1) 21')
expect(mask.masked('12')).toBe('(1) 212')
expect(mask.masked('123')).toBe('(1) 212')
expect(mask.masked('13')).toBe('(1) 213')
expect(mask.masked('(1) 23')).toBe('(1) 212')
expect(mask.masked('(1) 34')).toBe('(1) 213')
expect(mask.masked('2')).toBe('(1) 22')
expect(mask.masked('23')).toBe('(1) 223')
expect(mask.masked('3')).toBe('(1) 23')
expect(mask.unmasked('1')).toBe('')
expect(mask.unmasked('12')).toBe('')
expect(mask.unmasked('123')).toBe('3')
expect(mask.unmasked('(1) 23')).toBe('3')
})
test('12## mask', () => {
const mask = new Mask({ mask: '12##' })
expect(mask.masked('.')).toBe('12')
expect(mask.masked('1')).toBe('1')
expect(mask.masked('2')).toBe('12')
expect(mask.masked('3')).toBe('123')
expect(mask.masked('12')).toBe('12')
expect(mask.masked('123')).toBe('123')
expect(mask.masked('13')).toBe('123')
expect(mask.masked('134')).toBe('1234')
expect(mask.unmasked('1')).toBe('')
expect(mask.unmasked('12')).toBe('')
expect(mask.unmasked('123')).toBe('3')
expect(mask.unmasked('3')).toBe('3')
expect(mask.unmasked('(1) 23')).toBe('12')
})
test('12## eager mask', () => {
const mask = new Mask({ mask: '12##', eager: true })
expect(mask.masked(' ')).toBe('12')
expect(mask.masked('.')).toBe('12')
expect(mask.masked('1')).toBe('121')
expect(mask.masked('2')).toBe('122')
expect(mask.masked('3')).toBe('123')
expect(mask.masked('12')).toBe('1212')
expect(mask.masked('123')).toBe('1212')
expect(mask.masked('13')).toBe('1213')
expect(mask.masked('134')).toBe('1213')
expect(mask.unmasked('1')).toBe('')
expect(mask.unmasked('12')).toBe('')
expect(mask.unmasked('123')).toBe('3')
expect(mask.unmasked('3')).toBe('3')
expect(mask.unmasked('(1) 23')).toBe('12')
})
test('#!!# mask', () => {
const mask = new Mask({ mask: '#!!#' })
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe('1!2')
expect(mask.masked('1!2')).toBe('1!2')
expect(mask.masked('123')).toBe('1!2')
expect(mask.masked('a123')).toBe('1!2')
expect(mask.unmasked('a123')).toBe('12')
})
test('#!!# eager mask', () => {
const mask = new Mask({ mask: '#!!#', eager: true })
expect(mask.masked('1')).toBe('1!')
expect(mask.masked('12')).toBe('1!2')
expect(mask.masked('1!2')).toBe('1!2')
expect(mask.masked('123')).toBe('1!2')
expect(mask.masked('a123')).toBe('1!2')
expect(mask.unmasked('a123')).toBe('12')
})
test('+1 (###) ###-##-## mask', () => {
const mask = new Mask({ mask: '+1 (###) ###-##-##' })
expect(mask.masked('999')).toBe('+1 (999')
expect(mask.masked('999123')).toBe('+1 (999) 123')
expect(mask.masked('19991234567')).toBe('+1 (999) 123-45-67')
expect(mask.masked('+19991234567')).toBe('+1 (999) 123-45-67')
expect(mask.masked('9991234567')).toBe('+1 (999) 123-45-67')
expect(mask.masked('a9991234567')).toBe('+1 (999) 123-45-67')
expect(mask.unmasked('+19991234567')).toBe('9991234567')
})
test('+1 (###) ###-##-## eager mask', () => {
const mask = new Mask({ mask: '+1 (###) ###-##-##', eager: true })
expect(mask.masked('99')).toBe('+1 (99')
expect(mask.masked('999')).toBe('+1 (999) ')
expect(mask.masked('99912')).toBe('+1 (999) 12')
expect(mask.masked('999123')).toBe('+1 (999) 123-')
expect(mask.masked('19991234567')).toBe('+1 (199) 912-34-56')
expect(mask.masked('+19991234567')).toBe('+1 (199) 912-34-56')
expect(mask.masked('9991234567')).toBe('+1 (999) 123-45-67')
expect(mask.masked('a9991234567')).toBe('+1 (999) 123-45-67')
expect(mask.unmasked('+19991234567')).toBe('9991234567')
})
test('1 (###) ###-##-## eager mask', () => {
const mask = new Mask({ mask: '1 (###) ###-##-##', eager: true })
expect(mask.masked('19991234567')).toBe('1 (199) 912-34-56')
expect(mask.masked('+19991234567')).toBe('1 (199) 912-34-56')
expect(mask.masked('9991234567')).toBe('1 (999) 123-45-67')
expect(mask.masked('a9991234567')).toBe('1 (999) 123-45-67')
expect(mask.unmasked('19991234567')).toBe('9991234567')
expect(mask.unmasked('+19991234567')).toBe('1999123456')
})
test('transform mask', () => {
const mask = new Mask({
mask: 'ZZzz',
tokens: {
Z: { pattern: /[a-zA-Z]/, transform: (char) => char.toUpperCase() },
z: { pattern: /[a-zA-Z]/, transform: (char) => char.toLowerCase() }
}
})
expect(mask.masked('abcd')).toBe('ABcd')
expect(mask.masked('ABCD')).toBe('ABcd')
expect(mask.masked('1AB2CD')).toBe('ABcd')
expect(mask.unmasked('1AB2CD')).toBe('ABcd')
})
test('transform strict mask', () => {
const mask = new Mask({
mask: 'ZZzz',
tokens: {
Z: { pattern: /[A-Z]/, transform: (char) => char.toUpperCase() },
z: { pattern: /[a-z]/, transform: (char) => char.toLowerCase() }
}
})
expect(mask.masked('abcd')).toBe('ABcd')
expect(mask.masked('ABCD')).toBe('ABcd')
expect(mask.masked('1AB2CD')).toBe('ABcd')
expect(mask.unmasked('1AB2CD')).toBe('ABcd')
})
test('IP mask', () => {
const mask = new Mask({
mask: '#00.#00.#00.#00',
tokens: {
0: { pattern: /[\d]/, optional: true }
}
})
expect(mask.masked('127.0.0.1')).toBe('127.0.0.1')
expect(mask.masked('254254254254')).toBe('254.254.254.254')
expect(mask.masked('1.23.456.7890')).toBe('1.23.456.789')
expect(mask.masked('a1.23.456.7890')).toBe('1.23.456.789')
expect(mask.unmasked('a254.254.254.254')).toBe('254254254254')
})
test('repeated simple mask', () => {
const mask = new Mask({
mask: '9',
tokens: {
9: { pattern: /[\d]/, repeated: true }
}
})
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe('12')
expect(mask.masked('12 ')).toBe('12')
expect(mask.unmasked('a12')).toBe('12')
})
test('repeated 99.9 mask', () => {
const mask = new Mask({
mask: '99.9',
tokens: {
9: { pattern: /[\d]/, repeated: true }
}
})
expect(mask.masked('12')).toBe('12')
expect(mask.masked('123')).toBe('12.3')
expect(mask.masked('1234')).toBe('12.34')
expect(mask.masked('12345')).toBe('12.345')
expect(mask.masked('123456')).toBe('12.345.6')
expect(mask.masked('1234567')).toBe('12.345.67')
expect(mask.masked('12345678')).toBe('12.345.678')
expect(mask.masked('123456789')).toBe('12.345.678.9')
expect(mask.masked('a123456789')).toBe('12.345.678.9')
expect(mask.unmasked('a123456789')).toBe('123456789')
})
test('repeated 99.9 eager mask', () => {
const mask = new Mask({
mask: '99.9',
eager: true,
tokens: {
9: { pattern: /[\d]/, repeated: true }
}
})
expect(mask.masked('12')).toBe('12.')
expect(mask.masked('123')).toBe('12.3')
expect(mask.masked('1234')).toBe('12.34')
expect(mask.masked('12345')).toBe('12.345.')
expect(mask.masked('123456')).toBe('12.345.6')
expect(mask.masked('1234567')).toBe('12.345.67')
expect(mask.masked('12345678')).toBe('12.345.678.')
expect(mask.masked('123456789')).toBe('12.345.678.9')
expect(mask.unmasked('123456789')).toBe('123456789')
})
test('#-## reversed mask', () => {
const mask = new Mask({ mask: '#-##', reversed: true })
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe('12')
expect(mask.masked('123')).toBe('1-23')
expect(mask.masked('1234')).toBe('2-34')
expect(mask.masked('a1234')).toBe('2-34')
expect(mask.unmasked('a1234')).toBe('234')
})
test('#-## reversed eager mask', () => {
const mask = new Mask({ mask: '#-##', reversed: true, eager: true })
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe('-12')
expect(mask.masked('123')).toBe('1-23')
expect(mask.masked('1234')).toBe('2-34')
expect(mask.masked('a1234')).toBe('2-34')
expect(mask.unmasked('a1234')).toBe('234')
})
test('repeated reversed mask', () => {
const mask = new Mask({
mask: '9 99#,##',
reversed: true,
tokens: {
9: { pattern: /[\d]/, repeated: true }
}
})
expect(mask.masked('12')).toBe('12')
expect(mask.masked('123')).toBe('1,23')
expect(mask.masked('1234')).toBe('12,34')
expect(mask.masked('12345')).toBe('123,45')
expect(mask.masked('123456')).toBe('1 234,56')
expect(mask.masked('1234567')).toBe('12 345,67')
expect(mask.masked('12345678')).toBe('123 456,78')
expect(mask.masked('123456789')).toBe('1 234 567,89')
expect(mask.unmasked('123456789')).toBe('123456789')
})
test('repeated reversed eager mask', () => {
const mask = new Mask({
mask: '9 99#,##',
reversed: true,
eager: true,
tokens: {
9: { pattern: /[\d]/, repeated: true }
}
})
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe(',12')
expect(mask.masked('123')).toBe('1,23')
expect(mask.masked('1234')).toBe('12,34')
expect(mask.masked('12345')).toBe(' 123,45')
expect(mask.masked('123456')).toBe('1 234,56')
expect(mask.masked('1234567')).toBe('12 345,67')
expect(mask.masked('12345678')).toBe(' 123 456,78')
expect(mask.masked('123456789')).toBe('1 234 567,89')
expect(mask.unmasked('123456789')).toBe('123456789')
})
test('multiple numbers mask', () => {
const mask = new Mask({
mask: '+ +',
tokens: { '+': { pattern: /\d/, multiple: true } }
})
expect(mask.masked('1')).toBe('1')
expect(mask.masked('1 2')).toBe('1 2')
expect(mask.masked('1 2 3')).toBe('1 23')
expect(mask.masked('12')).toBe('12')
expect(mask.masked('12 ')).toBe('12 ')
expect(mask.masked('12 3')).toBe('12 3')
expect(mask.masked('12 34')).toBe('12 34')
expect(mask.masked('12 34 ')).toBe('12 34')
expect(mask.masked('12 34 5')).toBe('12 345')
expect(mask.masked('a12 34 5')).toBe('12 345')
expect(mask.masked('12.34.5')).toBe('12 345')
expect(mask.unmasked('a1 2 3')).toBe('123')
expect(mask.unmasked('12 3')).toBe('123')
expect(mask.unmasked('12 34 5')).toBe('12345')
})
test('multiple letters mask', () => {
const mask = new Mask({
mask: '+ +',
tokens: { '+': { pattern: /[a-zA-Z]/, multiple: true } }
})
expect(mask.masked('a')).toBe('a')
expect(mask.masked('ab')).toBe('ab')
expect(mask.masked('a b')).toBe('a b')
expect(mask.masked('a b с')).toBe('a b')
expect(mask.masked('ab ')).toBe('ab ')
expect(mask.masked('ab c')).toBe('ab c')
expect(mask.masked('ab cd')).toBe('ab cd')
expect(mask.masked('ab cd ')).toBe('ab cd')
expect(mask.masked('ab cd e')).toBe('ab cde')
expect(mask.masked('1ab cd e')).toBe('ab cde')
expect(mask.masked('ab.cd.e')).toBe('ab cde')
expect(mask.unmasked('1a b c')).toBe('abc')
expect(mask.unmasked('ab c')).toBe('abc')
expect(mask.unmasked('ab cd e')).toBe('abcde')
})
test('dynamic empty mask', () => {
const mask = new Mask({ mask: [] })
expect(mask.masked('1')).toBe('')
})
test('dynamic single mask', () => {
const mask = new Mask({ mask: ['#-#'] })
expect(mask.masked('123')).toBe('1-2')
expect(mask.unmasked('123')).toBe('12')
})
test('dynamic mask', () => {
const mask = new Mask({ mask: ['###.###.###-##', '##.###.###/####-##'] })
expect(mask.masked('12345678901')).toBe('123.456.789-01')
expect(mask.masked('123456789012')).toBe('12.345.678/9012')
expect(mask.masked('12345678901234')).toBe('12.345.678/9012-34')
expect(mask.masked('a123456789012345')).toBe('12.345.678/9012-34')
expect(mask.unmasked('a123456789012345')).toBe('12345678901234')
expect(mask.completed('12345678901')).toBe(false)
expect(mask.completed('12345678901234')).toBe(true)
})
test('dynamic function mask', () => {
const mask = new Mask({
mask: (value) => (value.startsWith('1') ? '#-#--#' : '## ##')
})
expect(mask.masked('12')).toBe('1-2')
expect(mask.masked('1234')).toBe('1-2--3')
expect(mask.masked('23')).toBe('23')
expect(mask.masked('23456')).toBe('23 45')
expect(mask.masked('a23456')).toBe('23 45')
expect(mask.unmasked('a123')).toBe('123')
expect(mask.unmasked('a2345')).toBe('2345')
expect(mask.completed('12')).toBe(false)
expect(mask.completed('123')).toBe(true)
expect(mask.completed('234')).toBe(false)
expect(mask.completed('2345')).toBe(true)
})
test('dynamic escaped mask', () => {
const mask = new Mask({ mask: ['!###', '!###-##', '!###.##.##'] })
expect(mask.masked('12')).toBe('#12')
expect(mask.masked('1234')).toBe('#12-34')
expect(mask.masked('12345')).toBe('#12.34.5')
expect(mask.masked('1234567')).toBe('#12.34.56')
})
test('tokens replaced', () => {
const mask = new Mask({
mask: 'Z-#',
tokens: { Z: { pattern: /[0-9]/ } },
tokensReplace: true
})
expect(mask.masked('1')).toBe('1')
expect(mask.masked('12')).toBe('1-#')
expect(mask.masked('123')).toBe('1-#')
expect(mask.masked('1-#')).toBe('1-#')
expect(mask.unmasked('12')).toBe('1')
})
+67
View File
@@ -0,0 +1,67 @@
import { expect, test } from 'vitest'
import { parseMask, parseOpts, parseTokens } from '../src/parser'
test('mask: empty', () => {
expect(parseMask('')).toBe('')
})
test('mask: string', () => {
expect(parseMask('#-#')).toBe('#-#')
})
test('mask: array', () => {
expect(parseMask('["#", "##"]')).toEqual(expect.arrayContaining(['#', '##']))
})
test('opts: empty', () => {
expect(parseOpts('')).toBe(true)
})
test('opts: true', () => {
expect(parseOpts('true')).toBe(true)
})
test('opts: truthy', () => {
expect(parseOpts('1')).toBe(true)
})
test('opts: false', () => {
expect(parseOpts('false')).toBe(false)
})
test('opts: falsy', () => {
expect(parseOpts('0')).toBe(false)
})
test('tokens: json', () => {
expect(parseTokens('{ "Z": { "pattern": "[0-9]" } }')).toEqual(
expect.objectContaining({ Z: { pattern: '[0-9]' } })
)
})
test('tokens: code', () => {
expect(parseTokens('Z:[0-9]')).toEqual(
expect.objectContaining({
Z: { pattern: /[0-9]/, multiple: false, optional: false, repeated: false }
})
)
expect(parseTokens('Z:[0-9]:multiple')).toEqual(
expect.objectContaining({
Z: { pattern: /[0-9]/, multiple: true, optional: false, repeated: false }
})
)
expect(parseTokens('Z:[0-9]:optional')).toEqual(
expect.objectContaining({
Z: { pattern: /[0-9]/, multiple: false, optional: true, repeated: false }
})
)
expect(parseTokens('Z:[0-9]:repeated')).toEqual(
expect.objectContaining({
Z: { pattern: /[0-9]/, multiple: false, optional: false, repeated: true }
})
)
})
+5
View File
@@ -0,0 +1,5 @@
import matchers from '@testing-library/jest-dom/matchers'
import { expect } from 'vitest'
// src: https://markus.oberlehner.net/blog/using-testing-library-jest-dom-with-vitest/
expect.extend(matchers)
+16 -7
View File
@@ -1,9 +1,18 @@
{
"compilerOptions": {
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "types"
},
"include": ["src/index.js"],
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"module": "ESNext",
"moduleResolution": "node",
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"types": ["vite/client"]
},
"include": ["src"]
}
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
base: '',
build: {
outDir: 'docs/dist',
rollupOptions: {
input: {
demo: 'src/demo/demo.ts'
},
output: {
entryFileNames: '[name].js',
assetFileNames: '[name][extname]'
}
}
},
plugins: [vue()]
})
+37
View File
@@ -0,0 +1,37 @@
/// <reference types="vitest" />
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import banner from 'vite-plugin-banner'
import dts from 'vite-plugin-dts'
import pkg from './package.json'
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'Maska',
fileName: (format) =>
format !== 'es' ? `${pkg.name}.${format}.js` : `${pkg.name}.js`
}
},
plugins: [
vue(),
dts({
outputDir: 'dist/types',
exclude: 'src/demo'
}),
banner(
`/*! ${pkg.name} v${pkg.version} | (c) ${pkg.author} | Released under the ${pkg.license} license */`
)
],
test: {
setupFiles: 'test/setup.ts',
environment: 'happy-dom',
coverage: {
provider: 'c8',
reporter: ['text', 'json-summary']
}
}
})
-4453
View File
File diff suppressed because it is too large Load Diff