UNPKG

22.5 kBJavaScriptView Raw
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 typeof define === 'function' && define.amd ? define(factory) :
4 (global.vueMaskedinput = factory());
5}(this, (function () { 'use strict';
6
7'use strict';
8
9function extend(dest, src) {
10 if (src) {
11 var props = Object.keys(src);
12 for (var i = 0, l = props.length; i < l ; i++) {
13 dest[props[i]] = src[props[i]];
14 }
15 }
16 return dest
17}
18
19function copy(obj) {
20 return extend({}, obj)
21}
22
23/**
24 * Merge an object defining format characters into the defaults.
25 * Passing null/undefined for en existing format character removes it.
26 * Passing a definition for an existing format character overrides it.
27 * @param {?Object} formatCharacters.
28 */
29function mergeFormatCharacters(formatCharacters) {
30 var merged = copy(DEFAULT_FORMAT_CHARACTERS);
31 if (formatCharacters) {
32 var chars = Object.keys(formatCharacters);
33 for (var i = 0, l = chars.length; i < l ; i++) {
34 var char = chars[i];
35 if (formatCharacters[char] == null) {
36 delete merged[char];
37 }
38 else {
39 merged[char] = formatCharacters[char];
40 }
41 }
42 }
43 return merged
44}
45
46var ESCAPE_CHAR = '\\';
47
48var DIGIT_RE = /^\d$/;
49var LETTER_RE = /^[A-Za-z]$/;
50var ALPHANNUMERIC_RE = /^[\dA-Za-z]$/;
51
52var DEFAULT_PLACEHOLDER_CHAR = '_';
53var DEFAULT_FORMAT_CHARACTERS = {
54 '*': {
55 validate: function(char) { return ALPHANNUMERIC_RE.test(char) }
56 },
57 '1': {
58 validate: function(char) { return DIGIT_RE.test(char) }
59 },
60 'a': {
61 validate: function(char) { return LETTER_RE.test(char) }
62 },
63 'A': {
64 validate: function(char) { return LETTER_RE.test(char) },
65 transform: function(char) { return char.toUpperCase() }
66 },
67 '#': {
68 validate: function(char) { return ALPHANNUMERIC_RE.test(char) },
69 transform: function(char) { return char.toUpperCase() }
70 }
71};
72
73/**
74 * @param {string} source
75 * @patam {?Object} formatCharacters
76 */
77function Pattern(source, formatCharacters, placeholderChar, isRevealingMask) {
78 if (!(this instanceof Pattern)) {
79 return new Pattern(source, formatCharacters, placeholderChar)
80 }
81
82 /** Placeholder character */
83 this.placeholderChar = placeholderChar || DEFAULT_PLACEHOLDER_CHAR;
84 /** Format character definitions. */
85 this.formatCharacters = formatCharacters || DEFAULT_FORMAT_CHARACTERS;
86 /** Pattern definition string with escape characters. */
87 this.source = source;
88 /** Pattern characters after escape characters have been processed. */
89 this.pattern = [];
90 /** Length of the pattern after escape characters have been processed. */
91 this.length = 0;
92 /** Index of the first editable character. */
93 this.firstEditableIndex = null;
94 /** Index of the last editable character. */
95 this.lastEditableIndex = null;
96 /** Lookup for indices of editable characters in the pattern. */
97 this._editableIndices = {};
98 /** If true, only the pattern before the last valid value character shows. */
99 this.isRevealingMask = isRevealingMask || false;
100
101 this._parse();
102}
103
104Pattern.prototype._parse = function parse() {
105 var this$1 = this;
106
107 var sourceChars = this.source.split('');
108 var patternIndex = 0;
109 var pattern = [];
110
111 for (var i = 0, l = sourceChars.length; i < l; i++) {
112 var char = sourceChars[i];
113 if (char === ESCAPE_CHAR) {
114 if (i === l - 1) {
115 throw new Error('InputMask: pattern ends with a raw ' + ESCAPE_CHAR)
116 }
117 char = sourceChars[++i];
118 }
119 else if (char in this$1.formatCharacters) {
120 if (this$1.firstEditableIndex === null) {
121 this$1.firstEditableIndex = patternIndex;
122 }
123 this$1.lastEditableIndex = patternIndex;
124 this$1._editableIndices[patternIndex] = true;
125 }
126
127 pattern.push(char);
128 patternIndex++;
129 }
130
131 if (this.firstEditableIndex === null) {
132 throw new Error(
133 'InputMask: pattern "' + this.source + '" does not contain any editable characters.'
134 )
135 }
136
137 this.pattern = pattern;
138 this.length = pattern.length;
139};
140
141/**
142 * @param {Array<string>} value
143 * @return {Array<string>}
144 */
145Pattern.prototype.formatValue = function format(value) {
146 var this$1 = this;
147
148 var valueBuffer = new Array(this.length);
149 var valueIndex = 0;
150
151 for (var i = 0, l = this.length; i < l ; i++) {
152 if (this$1.isEditableIndex(i)) {
153 if (this$1.isRevealingMask &&
154 value.length <= valueIndex &&
155 !this$1.isValidAtIndex(value[valueIndex], i)) {
156 break
157 }
158 valueBuffer[i] = (value.length > valueIndex && this$1.isValidAtIndex(value[valueIndex], i)
159 ? this$1.transform(value[valueIndex], i)
160 : this$1.placeholderChar);
161 valueIndex++;
162 }
163 else {
164 valueBuffer[i] = this$1.pattern[i];
165 // Also allow the value to contain static values from the pattern by
166 // advancing its index.
167 if (value.length > valueIndex && value[valueIndex] === this$1.pattern[i]) {
168 valueIndex++;
169 }
170 }
171 }
172
173 return valueBuffer
174};
175
176/**
177 * @param {number} index
178 * @return {boolean}
179 */
180Pattern.prototype.isEditableIndex = function isEditableIndex(index) {
181 return !!this._editableIndices[index]
182};
183
184/**
185 * @param {string} char
186 * @param {number} index
187 * @return {boolean}
188 */
189Pattern.prototype.isValidAtIndex = function isValidAtIndex(char, index) {
190 return this.formatCharacters[this.pattern[index]].validate(char)
191};
192
193Pattern.prototype.transform = function transform(char, index) {
194 var format = this.formatCharacters[this.pattern[index]];
195 return typeof format.transform == 'function' ? format.transform(char) : char
196};
197
198function InputMask(options) {
199 if (!(this instanceof InputMask)) { return new InputMask(options) }
200 options = extend({
201 formatCharacters: null,
202 pattern: null,
203 isRevealingMask: false,
204 placeholderChar: DEFAULT_PLACEHOLDER_CHAR,
205 selection: {start: 0, end: 0},
206 value: ''
207 }, options);
208
209 if (options.pattern == null) {
210 throw new Error('InputMask: you must provide a pattern.')
211 }
212
213 if (typeof options.placeholderChar !== 'string' || options.placeholderChar.length > 1) {
214 throw new Error('InputMask: placeholderChar should be a single character or an empty string.')
215 }
216
217 this.placeholderChar = options.placeholderChar;
218 this.formatCharacters = mergeFormatCharacters(options.formatCharacters);
219 this.setPattern(options.pattern, {
220 value: options.value,
221 selection: options.selection,
222 isRevealingMask: options.isRevealingMask
223 });
224}
225
226// Editing
227
228/**
229 * Applies a single character of input based on the current selection.
230 * @param {string} char
231 * @return {boolean} true if a change has been made to value or selection as a
232 * result of the input, false otherwise.
233 */
234InputMask.prototype.input = function input(char) {
235 var this$1 = this;
236
237 // Ignore additional input if the cursor's at the end of the pattern
238 if (this.selection.start === this.selection.end &&
239 this.selection.start === this.pattern.length) {
240 return false
241 }
242
243 var selectionBefore = copy(this.selection);
244 var valueBefore = this.getValue();
245
246 var inputIndex = this.selection.start;
247
248 // If the cursor or selection is prior to the first editable character, make
249 // sure any input given is applied to it.
250 if (inputIndex < this.pattern.firstEditableIndex) {
251 inputIndex = this.pattern.firstEditableIndex;
252 }
253
254 // Bail out or add the character to input
255 if (this.pattern.isEditableIndex(inputIndex)) {
256 if (!this.pattern.isValidAtIndex(char, inputIndex)) {
257 return false
258 }
259 this.value[inputIndex] = this.pattern.transform(char, inputIndex);
260 }
261
262 // If multiple characters were selected, blank the remainder out based on the
263 // pattern.
264 var end = this.selection.end - 1;
265 while (end > inputIndex) {
266 if (this$1.pattern.isEditableIndex(end)) {
267 this$1.value[end] = this$1.placeholderChar;
268 }
269 end--;
270 }
271
272 // Advance the cursor to the next character
273 this.selection.start = this.selection.end = inputIndex + 1;
274
275 // Skip over any subsequent static characters
276 while (this.pattern.length > this.selection.start &&
277 !this.pattern.isEditableIndex(this.selection.start)) {
278 this$1.selection.start++;
279 this$1.selection.end++;
280 }
281
282 // History
283 if (this._historyIndex != null) {
284 // Took more input after undoing, so blow any subsequent history away
285 this._history.splice(this._historyIndex, this._history.length - this._historyIndex);
286 this._historyIndex = null;
287 }
288 if (this._lastOp !== 'input' ||
289 selectionBefore.start !== selectionBefore.end ||
290 this._lastSelection !== null && selectionBefore.start !== this._lastSelection.start) {
291 this._history.push({value: valueBefore, selection: selectionBefore, lastOp: this._lastOp});
292 }
293 this._lastOp = 'input';
294 this._lastSelection = copy(this.selection);
295
296 return true
297};
298
299/**
300 * Attempts to delete from the value based on the current cursor position or
301 * selection.
302 * @return {boolean} true if the value or selection changed as the result of
303 * backspacing, false otherwise.
304 */
305InputMask.prototype.backspace = function backspace() {
306 var this$1 = this;
307
308 // If the cursor is at the start there's nothing to do
309 if (this.selection.start === 0 && this.selection.end === 0) {
310 return false
311 }
312
313 var selectionBefore = copy(this.selection);
314 var valueBefore = this.getValue();
315
316 // No range selected - work on the character preceding the cursor
317 if (this.selection.start === this.selection.end) {
318 if (this.pattern.isEditableIndex(this.selection.start - 1)) {
319 this.value[this.selection.start - 1] = this.placeholderChar;
320 }
321 this.selection.start--;
322 this.selection.end--;
323 }
324 // Range selected - delete characters and leave the cursor at the start of the selection
325 else {
326 var end = this.selection.end - 1;
327 while (end >= this.selection.start) {
328 if (this$1.pattern.isEditableIndex(end)) {
329 this$1.value[end] = this$1.placeholderChar;
330 }
331 end--;
332 }
333 this.selection.end = this.selection.start;
334 }
335
336 // History
337 if (this._historyIndex != null) {
338 // Took more input after undoing, so blow any subsequent history away
339 this._history.splice(this._historyIndex, this._history.length - this._historyIndex);
340 }
341 if (this._lastOp !== 'backspace' ||
342 selectionBefore.start !== selectionBefore.end ||
343 this._lastSelection !== null && selectionBefore.start !== this._lastSelection.start) {
344 this._history.push({value: valueBefore, selection: selectionBefore, lastOp: this._lastOp});
345 }
346 this._lastOp = 'backspace';
347 this._lastSelection = copy(this.selection);
348
349 return true
350};
351
352/**
353 * Attempts to paste a string of input at the current cursor position or over
354 * the top of the current selection.
355 * Invalid content at any position will cause the paste to be rejected, and it
356 * may contain static parts of the mask's pattern.
357 * @param {string} input
358 * @return {boolean} true if the paste was successful, false otherwise.
359 */
360InputMask.prototype.paste = function paste(input) {
361 var this$1 = this;
362
363 // This is necessary because we're just calling input() with each character
364 // and rolling back if any were invalid, rather than checking up-front.
365 var initialState = {
366 value: this.value.slice(),
367 selection: copy(this.selection),
368 _lastOp: this._lastOp,
369 _history: this._history.slice(),
370 _historyIndex: this._historyIndex,
371 _lastSelection: copy(this._lastSelection)
372 };
373
374 // If there are static characters at the start of the pattern and the cursor
375 // or selection is within them, the static characters must match for a valid
376 // paste.
377 if (this.selection.start < this.pattern.firstEditableIndex) {
378 for (var i = 0, l = this.pattern.firstEditableIndex - this.selection.start; i < l; i++) {
379 if (input.charAt(i) !== this$1.pattern.pattern[i]) {
380 return false
381 }
382 }
383
384 // Continue as if the selection and input started from the editable part of
385 // the pattern.
386 input = input.substring(this.pattern.firstEditableIndex - this.selection.start);
387 this.selection.start = this.pattern.firstEditableIndex;
388 }
389
390 for (i = 0, l = input.length;
391 i < l && this.selection.start <= this.pattern.lastEditableIndex;
392 i++) {
393 var valid = this$1.input(input.charAt(i));
394 // Allow static parts of the pattern to appear in pasted input - they will
395 // already have been stepped over by input(), so verify that the value
396 // deemed invalid by input() was the expected static character.
397 if (!valid) {
398 if (this$1.selection.start > 0) {
399 // XXX This only allows for one static character to be skipped
400 var patternIndex = this$1.selection.start - 1;
401 if (!this$1.pattern.isEditableIndex(patternIndex) &&
402 input.charAt(i) === this$1.pattern.pattern[patternIndex]) {
403 continue
404 }
405 }
406 extend(this$1, initialState);
407 return false
408 }
409 }
410
411 return true
412};
413
414// History
415
416InputMask.prototype.undo = function undo() {
417 // If there is no history, or nothing more on the history stack, we can't undo
418 if (this._history.length === 0 || this._historyIndex === 0) {
419 return false
420 }
421
422 var historyItem;
423 if (this._historyIndex == null) {
424 // Not currently undoing, set up the initial history index
425 this._historyIndex = this._history.length - 1;
426 historyItem = this._history[this._historyIndex];
427 // Add a new history entry if anything has changed since the last one, so we
428 // can redo back to the initial state we started undoing from.
429 var value = this.getValue();
430 if (historyItem.value !== value ||
431 historyItem.selection.start !== this.selection.start ||
432 historyItem.selection.end !== this.selection.end) {
433 this._history.push({value: value, selection: copy(this.selection), lastOp: this._lastOp, startUndo: true});
434 }
435 }
436 else {
437 historyItem = this._history[--this._historyIndex];
438 }
439
440 this.value = historyItem.value.split('');
441 this.selection = historyItem.selection;
442 this._lastOp = historyItem.lastOp;
443 return true
444};
445
446InputMask.prototype.redo = function redo() {
447 if (this._history.length === 0 || this._historyIndex == null) {
448 return false
449 }
450 var historyItem = this._history[++this._historyIndex];
451 // If this is the last history item, we're done redoing
452 if (this._historyIndex === this._history.length - 1) {
453 this._historyIndex = null;
454 // If the last history item was only added to start undoing, remove it
455 if (historyItem.startUndo) {
456 this._history.pop();
457 }
458 }
459 this.value = historyItem.value.split('');
460 this.selection = historyItem.selection;
461 this._lastOp = historyItem.lastOp;
462 return true
463};
464
465// Getters & setters
466
467InputMask.prototype.setPattern = function setPattern(pattern, options) {
468 options = extend({
469 selection: {start: 0, end: 0},
470 value: ''
471 }, options);
472 this.pattern = new Pattern(pattern, this.formatCharacters, this.placeholderChar, options.isRevealingMask);
473 this.setValue(options.value);
474 this.emptyValue = this.pattern.formatValue([]).join('');
475 this.selection = options.selection;
476 this._resetHistory();
477};
478
479InputMask.prototype.setSelection = function setSelection(selection) {
480 var this$1 = this;
481
482 this.selection = copy(selection);
483 if (this.selection.start === this.selection.end) {
484 if (this.selection.start < this.pattern.firstEditableIndex) {
485 this.selection.start = this.selection.end = this.pattern.firstEditableIndex;
486 return true
487 }
488 // Set selection to the first editable, non-placeholder character before the selection
489 // OR to the beginning of the pattern
490 var index = this.selection.start;
491 while (index >= this.pattern.firstEditableIndex) {
492 if (this$1.pattern.isEditableIndex(index - 1) &&
493 this$1.value[index - 1] !== this$1.placeholderChar ||
494 index === this$1.pattern.firstEditableIndex) {
495 this$1.selection.start = this$1.selection.end = index;
496 break
497 }
498 index--;
499 }
500 return true
501 }
502 return false
503};
504
505InputMask.prototype.setValue = function setValue(value) {
506 if (value == null) {
507 value = '';
508 }
509 this.value = this.pattern.formatValue(value.split(''));
510};
511
512InputMask.prototype.getValue = function getValue() {
513 return this.value.join('')
514};
515
516InputMask.prototype.getRawValue = function getRawValue() {
517 var this$1 = this;
518
519 var rawValue = [];
520 for (var i = 0; i < this.value.length; i++) {
521 if (this$1.pattern._editableIndices[i] === true) {
522 rawValue.push(this$1.value[i]);
523 }
524 }
525 return rawValue.join('')
526};
527
528InputMask.prototype._resetHistory = function _resetHistory() {
529 this._history = [];
530 this._historyIndex = null;
531 this._lastOp = null;
532 this._lastSelection = copy(this.selection);
533};
534
535InputMask.Pattern = Pattern;
536
537var lib$1 = InputMask;
538
539var KEYCODE_Z = 90;
540var KEYCODE_Y = 89;
541
542function getSelection (el) {
543 var start, end, rangeEl, clone;
544 if (el.selectionStart !== undefined) {
545 start = el.selectionStart;
546 end = el.selectionEnd;
547 }
548 else {
549 try {
550 el.focus();
551 rangeEl = el.createTextRange();
552 clone = rangeEl.duplicate();
553
554 rangeEl.moveToBookmark(document.selection.createRange().getBookmark());
555 clone.setEndPoint('EndToStart', rangeEl);
556
557 start = clone.text.length;
558 end = start + rangeEl.text.length;
559 }
560 catch (e) {}
561 }
562
563 return { start: start, end: end }
564}
565
566function isUndo(e) {
567 return (e.ctrlKey || e.metaKey) && e.keyCode === (e.shiftKey ? KEYCODE_Y : KEYCODE_Z)
568}
569
570function isRedo(e) {
571 return (e.ctrlKey || e.metaKey) && e.keyCode === (e.shiftKey ? KEYCODE_Z : KEYCODE_Y)
572}
573
574function setSelection(el, selection) {
575 var rangeEl;
576
577 try {
578 if (el.selectionStart !== undefined) {
579 el.focus();
580 el.setSelectionRange(selection.start, selection.end);
581 }
582 else {
583 el.focus();
584 rangeEl = el.createTextRange();
585 rangeEl.collapse(true);
586 rangeEl.moveStart('character', selection.start);
587 rangeEl.moveEnd('character', selection.end - selection.start);
588 rangeEl.select();
589 }
590 }
591 catch (e) {}
592}
593
594var index = {
595 name: 'masked-input',
596
597 render: function render(h) {
598 return h('input', {
599 ref: 'input',
600 domProps: {
601 value: this.value
602 },
603 on: {
604 change: this._change,
605 keydown: this._keydown,
606 keypress: this._keypress,
607 paste: this._paste
608 }
609 })
610 },
611
612 props: {
613 pattern: {
614 type: String,
615 required: true
616 },
617 value: {
618 type: String
619 },
620 formatCharacters: {
621 type: Object,
622 default: function () {
623 return {
624 'w': {
625 validate: function validate(char) { return /\w/.test(char) },
626 },
627 'W': {
628 validate: function validate(char) { return /\w/.test(char) },
629 transform: function transform(char) { return char.toUpperCase() }
630 }
631 }
632 }
633 },
634 placeholder: {
635 type: String
636 },
637 placeholderChar: {
638 type: String
639 },
640 hideUnderline: {
641 type: Boolean
642 }
643 },
644
645 watch: {
646 pattern: function pattern() {
647 this.init();
648 },
649 value: function value(newValue) {
650 if (this.mask) { this.mask.setValue(newValue); }
651 },
652 },
653
654 mounted: function mounted() {
655 this.init();
656 },
657
658 methods: {
659 init: function init() {
660 var this$1 = this;
661
662 var options = {
663 pattern: this.pattern,
664 value: this.value,
665 formatCharacters: this.formatCharacters
666 };
667 if (this.placeholderChar) {
668 options.placeholderChar = this.props.placeholderChar;
669 }
670 this.mask = new lib$1(options);
671 this.$refs.input.placeholder = this.placeholder ? this.placeholder : this.hideUnderline ? '' : this.mask.emptyValue;
672
673 if (this.$refs.input.value === '') {
674 this.$emit('input', '', '');
675 } else {
676 [].concat( this.$refs.input.value ).map(function (i) {
677 if(this$1.mask.input(i)) {
678 this$1.$refs.input.value = this$1.mask.getValue();
679 setTimeout(this$1._updateInputSelection, 0);
680 }
681 });
682 }
683 },
684
685 _change: function _change(e) {
686 var maskValue = this.mask.getValue();
687 if (this.$refs.input.value !== maskValue) {
688 // Cut or delete operations will have shortened the value
689 if (this.$refs.input.value.length < maskValue.length) {
690 var sizeDiff = maskValue.length - this.$refs.input.value.length;
691 this._updateMaskSelection();
692 this.mask.selection.end = this.mask.selection.start + sizeDiff;
693 this.mask.backspace();
694 }
695 this._updateValue(e);
696 }
697 },
698
699 _keydown: function _keydown(e) {
700 if (isUndo(e)) {
701 e.preventDefault();
702 if (this.mask.undo()) {
703 this._updateValue(e);
704 }
705 return
706 }
707 else if (isRedo(e)) {
708 e.preventDefault();
709 if (this.mask.redo()) {
710 this._updateValue(e);
711 }
712 return
713 }
714
715 if (e.key === 'Backspace') {
716 e.preventDefault();
717 this._updateMaskSelection();
718 if (this.mask.backspace()) {
719 this._updateValue(e);
720 }
721 if (this.$refs.input.value === '') {
722 this.$emit('input', '', '');
723 }
724 }
725 },
726
727 _keypress: function _keypress(e) {
728 if (e.metaKey || e.altKey || e.ctrlKey || e.key === 'Enter') { return }
729 e.preventDefault();
730 this._updateMaskSelection();
731 if (this.mask.input((e.key || e.data))) {
732 this._updateValue(e);
733 }
734 },
735
736 _paste: function _paste(e) {
737 e.preventDefault();
738 this._updateMaskSelection();
739 if (this.mask.paste(e.clipboardData.getData('Text'))) {
740 this._updateValue(e);
741 }
742 },
743
744 _updateMaskSelection: function _updateMaskSelection() {
745 this.mask.selection = getSelection(this.$refs.input);
746 },
747
748 _updateInputSelection: function _updateInputSelection() {
749 setSelection(this.$refs.input, this.mask.selection);
750 },
751
752 _getDisplayValue: function _getDisplayValue() {
753 var value = this.mask.getValue();
754 return value === this.mask.emptyValue ? '' : value
755 },
756
757 _updateValue: function _updateValue(e) {
758 this.$refs.input.value = this._getDisplayValue();
759 this.$emit('input', this.$refs.input.value, this.mask.getRawValue());
760 this._updateInputSelection();
761 if (this.change) {
762 this.change(e);
763 }
764 }
765 }
766};
767
768return index;
769
770})));