vue-maskedinput
Version:
Masked input Vue component
771 lines (675 loc) • 22.5 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.vueMaskedinput = factory());
}(this, (function () { 'use strict';
'use strict';
function extend(dest, src) {
if (src) {
var props = Object.keys(src);
for (var i = 0, l = props.length; i < l ; i++) {
dest[props[i]] = src[props[i]];
}
}
return dest
}
function copy(obj) {
return extend({}, obj)
}
/**
* Merge an object defining format characters into the defaults.
* Passing null/undefined for en existing format character removes it.
* Passing a definition for an existing format character overrides it.
* @param {?Object} formatCharacters.
*/
function mergeFormatCharacters(formatCharacters) {
var merged = copy(DEFAULT_FORMAT_CHARACTERS);
if (formatCharacters) {
var chars = Object.keys(formatCharacters);
for (var i = 0, l = chars.length; i < l ; i++) {
var char = chars[i];
if (formatCharacters[char] == null) {
delete merged[char];
}
else {
merged[char] = formatCharacters[char];
}
}
}
return merged
}
var ESCAPE_CHAR = '\\';
var DIGIT_RE = /^\d$/;
var LETTER_RE = /^[A-Za-z]$/;
var ALPHANNUMERIC_RE = /^[\dA-Za-z]$/;
var DEFAULT_PLACEHOLDER_CHAR = '_';
var DEFAULT_FORMAT_CHARACTERS = {
'*': {
validate: function(char) { return ALPHANNUMERIC_RE.test(char) }
},
'1': {
validate: function(char) { return DIGIT_RE.test(char) }
},
'a': {
validate: function(char) { return LETTER_RE.test(char) }
},
'A': {
validate: function(char) { return LETTER_RE.test(char) },
transform: function(char) { return char.toUpperCase() }
},
'#': {
validate: function(char) { return ALPHANNUMERIC_RE.test(char) },
transform: function(char) { return char.toUpperCase() }
}
};
/**
* @param {string} source
* @patam {?Object} formatCharacters
*/
function Pattern(source, formatCharacters, placeholderChar, isRevealingMask) {
if (!(this instanceof Pattern)) {
return new Pattern(source, formatCharacters, placeholderChar)
}
/** Placeholder character */
this.placeholderChar = placeholderChar || DEFAULT_PLACEHOLDER_CHAR;
/** Format character definitions. */
this.formatCharacters = formatCharacters || DEFAULT_FORMAT_CHARACTERS;
/** Pattern definition string with escape characters. */
this.source = source;
/** Pattern characters after escape characters have been processed. */
this.pattern = [];
/** Length of the pattern after escape characters have been processed. */
this.length = 0;
/** Index of the first editable character. */
this.firstEditableIndex = null;
/** Index of the last editable character. */
this.lastEditableIndex = null;
/** Lookup for indices of editable characters in the pattern. */
this._editableIndices = {};
/** If true, only the pattern before the last valid value character shows. */
this.isRevealingMask = isRevealingMask || false;
this._parse();
}
Pattern.prototype._parse = function parse() {
var this$1 = this;
var sourceChars = this.source.split('');
var patternIndex = 0;
var pattern = [];
for (var i = 0, l = sourceChars.length; i < l; i++) {
var char = sourceChars[i];
if (char === ESCAPE_CHAR) {
if (i === l - 1) {
throw new Error('InputMask: pattern ends with a raw ' + ESCAPE_CHAR)
}
char = sourceChars[++i];
}
else if (char in this$1.formatCharacters) {
if (this$1.firstEditableIndex === null) {
this$1.firstEditableIndex = patternIndex;
}
this$1.lastEditableIndex = patternIndex;
this$1._editableIndices[patternIndex] = true;
}
pattern.push(char);
patternIndex++;
}
if (this.firstEditableIndex === null) {
throw new Error(
'InputMask: pattern "' + this.source + '" does not contain any editable characters.'
)
}
this.pattern = pattern;
this.length = pattern.length;
};
/**
* @param {Array<string>} value
* @return {Array<string>}
*/
Pattern.prototype.formatValue = function format(value) {
var this$1 = this;
var valueBuffer = new Array(this.length);
var valueIndex = 0;
for (var i = 0, l = this.length; i < l ; i++) {
if (this$1.isEditableIndex(i)) {
if (this$1.isRevealingMask &&
value.length <= valueIndex &&
!this$1.isValidAtIndex(value[valueIndex], i)) {
break
}
valueBuffer[i] = (value.length > valueIndex && this$1.isValidAtIndex(value[valueIndex], i)
? this$1.transform(value[valueIndex], i)
: this$1.placeholderChar);
valueIndex++;
}
else {
valueBuffer[i] = this$1.pattern[i];
// Also allow the value to contain static values from the pattern by
// advancing its index.
if (value.length > valueIndex && value[valueIndex] === this$1.pattern[i]) {
valueIndex++;
}
}
}
return valueBuffer
};
/**
* @param {number} index
* @return {boolean}
*/
Pattern.prototype.isEditableIndex = function isEditableIndex(index) {
return !!this._editableIndices[index]
};
/**
* @param {string} char
* @param {number} index
* @return {boolean}
*/
Pattern.prototype.isValidAtIndex = function isValidAtIndex(char, index) {
return this.formatCharacters[this.pattern[index]].validate(char)
};
Pattern.prototype.transform = function transform(char, index) {
var format = this.formatCharacters[this.pattern[index]];
return typeof format.transform == 'function' ? format.transform(char) : char
};
function InputMask(options) {
if (!(this instanceof InputMask)) { return new InputMask(options) }
options = extend({
formatCharacters: null,
pattern: null,
isRevealingMask: false,
placeholderChar: DEFAULT_PLACEHOLDER_CHAR,
selection: {start: 0, end: 0},
value: ''
}, options);
if (options.pattern == null) {
throw new Error('InputMask: you must provide a pattern.')
}
if (typeof options.placeholderChar !== 'string' || options.placeholderChar.length > 1) {
throw new Error('InputMask: placeholderChar should be a single character or an empty string.')
}
this.placeholderChar = options.placeholderChar;
this.formatCharacters = mergeFormatCharacters(options.formatCharacters);
this.setPattern(options.pattern, {
value: options.value,
selection: options.selection,
isRevealingMask: options.isRevealingMask
});
}
// Editing
/**
* Applies a single character of input based on the current selection.
* @param {string} char
* @return {boolean} true if a change has been made to value or selection as a
* result of the input, false otherwise.
*/
InputMask.prototype.input = function input(char) {
var this$1 = this;
// Ignore additional input if the cursor's at the end of the pattern
if (this.selection.start === this.selection.end &&
this.selection.start === this.pattern.length) {
return false
}
var selectionBefore = copy(this.selection);
var valueBefore = this.getValue();
var inputIndex = this.selection.start;
// If the cursor or selection is prior to the first editable character, make
// sure any input given is applied to it.
if (inputIndex < this.pattern.firstEditableIndex) {
inputIndex = this.pattern.firstEditableIndex;
}
// Bail out or add the character to input
if (this.pattern.isEditableIndex(inputIndex)) {
if (!this.pattern.isValidAtIndex(char, inputIndex)) {
return false
}
this.value[inputIndex] = this.pattern.transform(char, inputIndex);
}
// If multiple characters were selected, blank the remainder out based on the
// pattern.
var end = this.selection.end - 1;
while (end > inputIndex) {
if (this$1.pattern.isEditableIndex(end)) {
this$1.value[end] = this$1.placeholderChar;
}
end--;
}
// Advance the cursor to the next character
this.selection.start = this.selection.end = inputIndex + 1;
// Skip over any subsequent static characters
while (this.pattern.length > this.selection.start &&
!this.pattern.isEditableIndex(this.selection.start)) {
this$1.selection.start++;
this$1.selection.end++;
}
// History
if (this._historyIndex != null) {
// Took more input after undoing, so blow any subsequent history away
this._history.splice(this._historyIndex, this._history.length - this._historyIndex);
this._historyIndex = null;
}
if (this._lastOp !== 'input' ||
selectionBefore.start !== selectionBefore.end ||
this._lastSelection !== null && selectionBefore.start !== this._lastSelection.start) {
this._history.push({value: valueBefore, selection: selectionBefore, lastOp: this._lastOp});
}
this._lastOp = 'input';
this._lastSelection = copy(this.selection);
return true
};
/**
* Attempts to delete from the value based on the current cursor position or
* selection.
* @return {boolean} true if the value or selection changed as the result of
* backspacing, false otherwise.
*/
InputMask.prototype.backspace = function backspace() {
var this$1 = this;
// If the cursor is at the start there's nothing to do
if (this.selection.start === 0 && this.selection.end === 0) {
return false
}
var selectionBefore = copy(this.selection);
var valueBefore = this.getValue();
// No range selected - work on the character preceding the cursor
if (this.selection.start === this.selection.end) {
if (this.pattern.isEditableIndex(this.selection.start - 1)) {
this.value[this.selection.start - 1] = this.placeholderChar;
}
this.selection.start--;
this.selection.end--;
}
// Range selected - delete characters and leave the cursor at the start of the selection
else {
var end = this.selection.end - 1;
while (end >= this.selection.start) {
if (this$1.pattern.isEditableIndex(end)) {
this$1.value[end] = this$1.placeholderChar;
}
end--;
}
this.selection.end = this.selection.start;
}
// History
if (this._historyIndex != null) {
// Took more input after undoing, so blow any subsequent history away
this._history.splice(this._historyIndex, this._history.length - this._historyIndex);
}
if (this._lastOp !== 'backspace' ||
selectionBefore.start !== selectionBefore.end ||
this._lastSelection !== null && selectionBefore.start !== this._lastSelection.start) {
this._history.push({value: valueBefore, selection: selectionBefore, lastOp: this._lastOp});
}
this._lastOp = 'backspace';
this._lastSelection = copy(this.selection);
return true
};
/**
* Attempts to paste a string of input at the current cursor position or over
* the top of the current selection.
* Invalid content at any position will cause the paste to be rejected, and it
* may contain static parts of the mask's pattern.
* @param {string} input
* @return {boolean} true if the paste was successful, false otherwise.
*/
InputMask.prototype.paste = function paste(input) {
var this$1 = this;
// This is necessary because we're just calling input() with each character
// and rolling back if any were invalid, rather than checking up-front.
var initialState = {
value: this.value.slice(),
selection: copy(this.selection),
_lastOp: this._lastOp,
_history: this._history.slice(),
_historyIndex: this._historyIndex,
_lastSelection: copy(this._lastSelection)
};
// If there are static characters at the start of the pattern and the cursor
// or selection is within them, the static characters must match for a valid
// paste.
if (this.selection.start < this.pattern.firstEditableIndex) {
for (var i = 0, l = this.pattern.firstEditableIndex - this.selection.start; i < l; i++) {
if (input.charAt(i) !== this$1.pattern.pattern[i]) {
return false
}
}
// Continue as if the selection and input started from the editable part of
// the pattern.
input = input.substring(this.pattern.firstEditableIndex - this.selection.start);
this.selection.start = this.pattern.firstEditableIndex;
}
for (i = 0, l = input.length;
i < l && this.selection.start <= this.pattern.lastEditableIndex;
i++) {
var valid = this$1.input(input.charAt(i));
// Allow static parts of the pattern to appear in pasted input - they will
// already have been stepped over by input(), so verify that the value
// deemed invalid by input() was the expected static character.
if (!valid) {
if (this$1.selection.start > 0) {
// XXX This only allows for one static character to be skipped
var patternIndex = this$1.selection.start - 1;
if (!this$1.pattern.isEditableIndex(patternIndex) &&
input.charAt(i) === this$1.pattern.pattern[patternIndex]) {
continue
}
}
extend(this$1, initialState);
return false
}
}
return true
};
// History
InputMask.prototype.undo = function undo() {
// If there is no history, or nothing more on the history stack, we can't undo
if (this._history.length === 0 || this._historyIndex === 0) {
return false
}
var historyItem;
if (this._historyIndex == null) {
// Not currently undoing, set up the initial history index
this._historyIndex = this._history.length - 1;
historyItem = this._history[this._historyIndex];
// Add a new history entry if anything has changed since the last one, so we
// can redo back to the initial state we started undoing from.
var value = this.getValue();
if (historyItem.value !== value ||
historyItem.selection.start !== this.selection.start ||
historyItem.selection.end !== this.selection.end) {
this._history.push({value: value, selection: copy(this.selection), lastOp: this._lastOp, startUndo: true});
}
}
else {
historyItem = this._history[--this._historyIndex];
}
this.value = historyItem.value.split('');
this.selection = historyItem.selection;
this._lastOp = historyItem.lastOp;
return true
};
InputMask.prototype.redo = function redo() {
if (this._history.length === 0 || this._historyIndex == null) {
return false
}
var historyItem = this._history[++this._historyIndex];
// If this is the last history item, we're done redoing
if (this._historyIndex === this._history.length - 1) {
this._historyIndex = null;
// If the last history item was only added to start undoing, remove it
if (historyItem.startUndo) {
this._history.pop();
}
}
this.value = historyItem.value.split('');
this.selection = historyItem.selection;
this._lastOp = historyItem.lastOp;
return true
};
// Getters & setters
InputMask.prototype.setPattern = function setPattern(pattern, options) {
options = extend({
selection: {start: 0, end: 0},
value: ''
}, options);
this.pattern = new Pattern(pattern, this.formatCharacters, this.placeholderChar, options.isRevealingMask);
this.setValue(options.value);
this.emptyValue = this.pattern.formatValue([]).join('');
this.selection = options.selection;
this._resetHistory();
};
InputMask.prototype.setSelection = function setSelection(selection) {
var this$1 = this;
this.selection = copy(selection);
if (this.selection.start === this.selection.end) {
if (this.selection.start < this.pattern.firstEditableIndex) {
this.selection.start = this.selection.end = this.pattern.firstEditableIndex;
return true
}
// Set selection to the first editable, non-placeholder character before the selection
// OR to the beginning of the pattern
var index = this.selection.start;
while (index >= this.pattern.firstEditableIndex) {
if (this$1.pattern.isEditableIndex(index - 1) &&
this$1.value[index - 1] !== this$1.placeholderChar ||
index === this$1.pattern.firstEditableIndex) {
this$1.selection.start = this$1.selection.end = index;
break
}
index--;
}
return true
}
return false
};
InputMask.prototype.setValue = function setValue(value) {
if (value == null) {
value = '';
}
this.value = this.pattern.formatValue(value.split(''));
};
InputMask.prototype.getValue = function getValue() {
return this.value.join('')
};
InputMask.prototype.getRawValue = function getRawValue() {
var this$1 = this;
var rawValue = [];
for (var i = 0; i < this.value.length; i++) {
if (this$1.pattern._editableIndices[i] === true) {
rawValue.push(this$1.value[i]);
}
}
return rawValue.join('')
};
InputMask.prototype._resetHistory = function _resetHistory() {
this._history = [];
this._historyIndex = null;
this._lastOp = null;
this._lastSelection = copy(this.selection);
};
InputMask.Pattern = Pattern;
var lib$1 = InputMask;
var KEYCODE_Z = 90;
var KEYCODE_Y = 89;
function getSelection (el) {
var start, end, rangeEl, clone;
if (el.selectionStart !== undefined) {
start = el.selectionStart;
end = el.selectionEnd;
}
else {
try {
el.focus();
rangeEl = el.createTextRange();
clone = rangeEl.duplicate();
rangeEl.moveToBookmark(document.selection.createRange().getBookmark());
clone.setEndPoint('EndToStart', rangeEl);
start = clone.text.length;
end = start + rangeEl.text.length;
}
catch (e) {}
}
return { start: start, end: end }
}
function isUndo(e) {
return (e.ctrlKey || e.metaKey) && e.keyCode === (e.shiftKey ? KEYCODE_Y : KEYCODE_Z)
}
function isRedo(e) {
return (e.ctrlKey || e.metaKey) && e.keyCode === (e.shiftKey ? KEYCODE_Z : KEYCODE_Y)
}
function setSelection(el, selection) {
var rangeEl;
try {
if (el.selectionStart !== undefined) {
el.focus();
el.setSelectionRange(selection.start, selection.end);
}
else {
el.focus();
rangeEl = el.createTextRange();
rangeEl.collapse(true);
rangeEl.moveStart('character', selection.start);
rangeEl.moveEnd('character', selection.end - selection.start);
rangeEl.select();
}
}
catch (e) {}
}
var index = {
name: 'masked-input',
render: function render(h) {
return h('input', {
ref: 'input',
domProps: {
value: this.value
},
on: {
change: this._change,
keydown: this._keydown,
keypress: this._keypress,
paste: this._paste
}
})
},
props: {
pattern: {
type: String,
required: true
},
value: {
type: String
},
formatCharacters: {
type: Object,
default: function () {
return {
'w': {
validate: function validate(char) { return /\w/.test(char) },
},
'W': {
validate: function validate(char) { return /\w/.test(char) },
transform: function transform(char) { return char.toUpperCase() }
}
}
}
},
placeholder: {
type: String
},
placeholderChar: {
type: String
},
hideUnderline: {
type: Boolean
}
},
watch: {
pattern: function pattern() {
this.init();
},
value: function value(newValue) {
if (this.mask) { this.mask.setValue(newValue); }
},
},
mounted: function mounted() {
this.init();
},
methods: {
init: function init() {
var this$1 = this;
var options = {
pattern: this.pattern,
value: this.value,
formatCharacters: this.formatCharacters
};
if (this.placeholderChar) {
options.placeholderChar = this.props.placeholderChar;
}
this.mask = new lib$1(options);
this.$refs.input.placeholder = this.placeholder ? this.placeholder : this.hideUnderline ? '' : this.mask.emptyValue;
if (this.$refs.input.value === '') {
this.$emit('input', '', '');
} else {
[].concat( this.$refs.input.value ).map(function (i) {
if(this$1.mask.input(i)) {
this$1.$refs.input.value = this$1.mask.getValue();
setTimeout(this$1._updateInputSelection, 0);
}
});
}
},
_change: function _change(e) {
var maskValue = this.mask.getValue();
if (this.$refs.input.value !== maskValue) {
// Cut or delete operations will have shortened the value
if (this.$refs.input.value.length < maskValue.length) {
var sizeDiff = maskValue.length - this.$refs.input.value.length;
this._updateMaskSelection();
this.mask.selection.end = this.mask.selection.start + sizeDiff;
this.mask.backspace();
}
this._updateValue(e);
}
},
_keydown: function _keydown(e) {
if (isUndo(e)) {
e.preventDefault();
if (this.mask.undo()) {
this._updateValue(e);
}
return
}
else if (isRedo(e)) {
e.preventDefault();
if (this.mask.redo()) {
this._updateValue(e);
}
return
}
if (e.key === 'Backspace') {
e.preventDefault();
this._updateMaskSelection();
if (this.mask.backspace()) {
this._updateValue(e);
}
if (this.$refs.input.value === '') {
this.$emit('input', '', '');
}
}
},
_keypress: function _keypress(e) {
if (e.metaKey || e.altKey || e.ctrlKey || e.key === 'Enter') { return }
e.preventDefault();
this._updateMaskSelection();
if (this.mask.input((e.key || e.data))) {
this._updateValue(e);
}
},
_paste: function _paste(e) {
e.preventDefault();
this._updateMaskSelection();
if (this.mask.paste(e.clipboardData.getData('Text'))) {
this._updateValue(e);
}
},
_updateMaskSelection: function _updateMaskSelection() {
this.mask.selection = getSelection(this.$refs.input);
},
_updateInputSelection: function _updateInputSelection() {
setSelection(this.$refs.input, this.mask.selection);
},
_getDisplayValue: function _getDisplayValue() {
var value = this.mask.getValue();
return value === this.mask.emptyValue ? '' : value
},
_updateValue: function _updateValue(e) {
this.$refs.input.value = this._getDisplayValue();
this.$emit('input', this.$refs.input.value, this.mask.getRawValue());
this._updateInputSelection();
if (this.change) {
this.change(e);
}
}
}
};
return index;
})));