Skip to content
This repository has been archived by the owner on Sep 4, 2024. It is now read-only.

Added support for add/remove of custom placeholders, inline RegExp array masks, and functions as masks #467

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLocalVue, mount } from '@vue/test-utils';
import VueMask, { VueMaskDirective, VueMaskPlugin } from '../index';
import { timeRangeMask } from '../utils/timeRangeMask';

describe('plugin/directive registration', () => {
let Vue;
Expand Down Expand Up @@ -67,4 +68,92 @@ describe('directive usage', () => {
await wrapper.vm.$nextTick();
expect(wrapper.vm.$el.value).toBe('11.11.2011');
});

it('should accept an array of regular expressions directly', async () => {
const wrapper = mountWithMask({
data: () => ({ mask: ['(', /\d/, /\d/, /\d/, ') ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/], value: '5555551234' }),
template: '<input v-mask="mask" v-model="value"/>',
});
expect(wrapper.vm.$el.value).toBe('(555) 555-1234');
});

it('should allow for add/removal of global mask placeholders', async () => {
const localVue = createLocalVue();
localVue.use(VueMask, {
placeholders: {
'#': null,
D: /\d/,
},
});
const wrapper = mount({
data: () => ({ mask: '###-DDD-###-DDD', value: '123456' }),
template: '<input v-mask="mask" v-model="value"/>',
}, { localVue });
expect(wrapper.vm.$el.value).toBe('###-123-###-456');
});

it('should allow placeholders for uppercase and lowercase characters', async () => {
const localVue = createLocalVue();
localVue.use(VueMask, {
placeholders: {
u: /[A-Z]/,
l: /[a-z]/,
},
});
const wrapper = mount({
data: () => ({ mask: '###-###-###-ul-ul', value: '123123123AbAb' }),
template: '<input v-mask="mask" v-model="value"/>',
}, { localVue });
expect(wrapper.vm.$el.value).toBe('123-123-123-Ab-Ab');
});

it('should allow placeholders for cyrillic characters', async () => {
const localVue = createLocalVue();
localVue.use(VueMask, {
placeholders: {
Я: /[\wа-яА-Я]/,
},
});
const wrapper = mount({
data: () => ({ mask: 'ЯЯЯЯЯЯ ЯЯЯЯ', value: 'Доброеутро' }),
template: '<input v-mask="mask" v-model="value"/>',
}, { localVue });
expect(wrapper.vm.$el.value).toBe('Доброе утро');
});

it('should be possible to create a mask for accepting a valid time range', async () => {
const wrapper = mountWithMask({
data: () => ({
mask: timeRangeMask,
value: '02532137',
}),
template: '<input v-mask="mask" v-model="value"/>',
});
expect(wrapper.vm.$el.value).toBe('02:53-21:37');
});

it('should be possible to create a mask for rejecting an invalid time range', async () => {
const wrapper = mountWithMask({
data: () => ({
mask: timeRangeMask,
value: '23599999',
}),
template: '<input v-mask="mask" v-model="value"/>',
});
expect(wrapper.vm.$el.value).toBe('23:59-');
});

it('should have the ability to give two or multiple choices', async () => {
const localVue = createLocalVue();
localVue.use(VueMask, {
placeholders: {
P: /(6|7)/,
},
});
const wrapper = mount({
data: () => ({ mask: '0P-##-##-##-##', value: '0755555555' }),
template: '<input v-mask="mask" v-model="value"/>',
}, { localVue });
expect(wrapper.vm.$el.value).toBe('07-55-55-55-55');
});
});
152 changes: 106 additions & 46 deletions src/directive.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/* eslint-disable no-param-reassign */
// eslint-disable-next-line import/no-extraneous-dependencies
import conformToMask from 'text-mask-core/src/conformToMask';
import stringMaskToRegExpMask from './stringMaskToRegExpMask';
import { stringMaskToRegExpMask, arrayMaskToRegExpMask } from './maskToRegExpMask';
import { trigger, queryInputElementInside } from './utils';
import { isAndroid, isChrome } from './utils/env';
import createOptions from './createOptions';
import { defaultMaskReplacers } from './constants';

const options = createOptions();

Expand Down Expand Up @@ -41,60 +42,119 @@ function updateValue(el, force = false) {

/**
* Fires on handler update
* @param {HTMLInputElement} el
* @param {String} mask
* @param {HTMLInputElement} el
* @param {String|Array.<String|RegExp>|Function} inputMask
*/
function updateMask(el, mask) {
options.partiallyUpdate(el, { mask: stringMaskToRegExpMask(mask) });
}
function updateMask(el, inputMask, maskReplacers) {
let mask = null;

if (Array.isArray(inputMask)) {
mask = arrayMaskToRegExpMask(inputMask, maskReplacers);
} else if (typeof inputMask === 'function') {
mask = inputMask;
} else {
mask = stringMaskToRegExpMask(inputMask, maskReplacers);
}

options.partiallyUpdate(el, { mask });
}

/**
* Vue directive definition
* Merge custom mask replacers with default mask replacers
* @param {Object<string, RegExp>} maskReplacers
* @param {Objext<string, RegExp>} baseMaskReplacers
* @return {Object} The extended mask replacers
*/
export default {
function extendMaskReplacers(maskReplacers, baseMaskReplacers = defaultMaskReplacers) {
if (maskReplacers === null || Array.isArray(maskReplacers) || typeof maskReplacers !== 'object') {
return baseMaskReplacers;
}

/**
* Called only once, when the directive is first bound to the element.
* This is where you can do one-time setup work.
*
* @param {(HTMLInputElement|HTMLElement)} el
* @param {?String} value
*/
bind(el, { value }) {
el = queryInputElementInside(el);
return Object.keys(maskReplacers).reduce((extendedMaskReplacers, key) => {
const value = maskReplacers[key];

updateMask(el, value);
updateValue(el);
},
if (value !== null && !(value instanceof RegExp)) {
return extendedMaskReplacers;
}

/**
* Called after the containing component has updated,
* but possibly before its children have updated.
* The directive’s value may or may not have changed,
* but you can skip unnecessary updates by comparing the
* binding’s current and old values.
*
* @param {(HTMLInputElement|HTMLElement)} el
* @param {?String} value
* @param {?String} oldValue
*/
componentUpdated(el, { value, oldValue }) {
el = queryInputElementInside(el);
return { ...extendedMaskReplacers, [key]: value };
}, baseMaskReplacers);
}

const isMaskChanged = value !== oldValue;
/**
* Convert a mask into a string for comparison
* @param {String|Array.<String|RegExp>} mask
*/
function maskToString(mask) {
const maskArray = Array.isArray(mask) ? mask : [mask];
const filteredMaskArray = maskArray.filter((part) => typeof part === 'string' || part instanceof RegExp);
return filteredMaskArray.toString();
}

// update mask first if changed
if (isMaskChanged) {
updateMask(el, value);
}
/**
* Create the Vue directive
* @param {Object} directiveOptions
* @param {Object.<string, RegExp>} directiveOptions.placeholders
* @return {Object} The Vue directive
*/
export function createDirective(directiveOptions = {}) {
const instanceMaskReplacers = extendMaskReplacers(
directiveOptions && directiveOptions.placeholders,
);

// update value
updateValue(el, isMaskChanged);
},
/**
* Vue directive definition
*/
return {

/**
* Called only once, when the directive is first bound to the element.
* This is where you can do one-time setup work.
*
* @param {(HTMLInputElement|HTMLElement)} el
* @param {?String} value
*/
bind(el, { value }) {
el = queryInputElementInside(el);

updateMask(el, value, instanceMaskReplacers);
updateValue(el);
},

/**
* Called after the containing component has updated,
* but possibly before its children have updated.
* The directive’s value may or may not have changed,
* but you can skip unnecessary updates by comparing the
* binding’s current and old values.
*
* @param {(HTMLInputElement|HTMLElement)} el
* @param {?String} value
* @param {?String} oldValue
*/
componentUpdated(el, { value, oldValue }) {
el = queryInputElementInside(el);

const isMaskChanged = typeof value === 'function'
|| maskToString(oldValue) !== maskToString(value);

// update mask first if changed
if (isMaskChanged) {
updateMask(el, value, instanceMaskReplacers);
}

// update value
updateValue(el, isMaskChanged);
},

unbind(el) {
el = queryInputElementInside(el);
options.remove(el);
},
};
}

unbind(el) {
el = queryInputElementInside(el);
options.remove(el);
},
};
/**
* Default directive definition
*/
export default createDirective();
50 changes: 50 additions & 0 deletions src/maskToRegExpMask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { defaultMaskReplacers, NEXT_CHAR_OPTIONAL } from './constants';
import { castToRegexp, makeRegexpOptional } from './utils/regexp';

function maskToRegExpMask(mask, maskReplacers = defaultMaskReplacers) {
return mask
.map((char, index, array) => {
const maskChar = maskReplacers[char] || char;
const previousChar = array[index - 1];
const previousMaskChar = maskReplacers[previousChar] || previousChar;
if (maskChar === NEXT_CHAR_OPTIONAL) {
return null;
}
if (previousMaskChar === NEXT_CHAR_OPTIONAL) {
return makeRegexpOptional(castToRegexp(maskChar));
}
return maskChar;
})
.filter(Boolean);
}

/**
* Converts mask from `v-mask` string format to `text-mask-core` format
* @param {String} stringMask
* @return {RegExp[]}
*/
export function stringMaskToRegExpMask(stringMask, maskReplacers = defaultMaskReplacers) {
return maskToRegExpMask(stringMask.split(''), maskReplacers);
}

/**
* Converts mask from `v-mask` array format to `text-mask-core` format
* @param {Array.<String|RegExp>} arrayMask
* @return {RegExp[]}
*/
export function arrayMaskToRegExpMask(arrayMask, maskReplacers = defaultMaskReplacers) {
const flattenedMask = arrayMask
.map((part) => {
if (part instanceof RegExp) {
return part;
}
if (typeof part === 'string') {
return part.split('');
}
return null;
})
.filter(Boolean)
.reduce((mask, part) => mask.concat(part), []);

return maskToRegExpMask(flattenedMask, maskReplacers);
}
8 changes: 5 additions & 3 deletions src/plugin.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import directive from './directive';
import { createDirective } from './directive';

/**
* Vue plugin definition
* @param {Vue} Vue
* @param {Object} options
* @param {Object.<string, RegExp>} options.placeholders
*/
export default (Vue) => {
Vue.directive('mask', directive);
export default (Vue, options = {}) => {
Vue.directive('mask', createDirective(options));
};
25 changes: 0 additions & 25 deletions src/stringMaskToRegExpMask.js

This file was deleted.

26 changes: 26 additions & 0 deletions src/utils/timeRangeMask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Generate a time mask based on input value (23:59)
* @param {string} value
*/
export function timeMask(value) {
const hours = [
/[0-2]/,
value.charAt(0) === '2' ? /[0-3]/ : /[0-9]/,
];
const minutes = [/[0-5]/, /[0-9]/];
return value.length > 2
? [...hours, ':', ...minutes]
: hours;
}

/**
* Generate a time range mask based on input value (00:00-23:59)
* @param {string} value
*/
export function timeRangeMask(value) {
const numbers = value.replace(/[^0-9]/g, '');
if (numbers.length > 4) {
return [...timeMask(numbers.substring(0, 4)), '-', ...timeMask(numbers.substring(4))];
}
return [...timeMask(numbers)];
}
Loading