UNPKG

vue-maskedinput

Version:
771 lines (675 loc) 22.5 kB
(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; })));