From 90457a9a698ff4109d111478501600eabe06ac92 Mon Sep 17 00:00:00 2001 From: c01nd01r Date: Thu, 15 Dec 2016 18:42:20 +0300 Subject: [PATCH] Add browser support --- lib/index.js | 895 ++++++++++++++++++++++++++------------------------- 1 file changed, 451 insertions(+), 444 deletions(-) diff --git a/lib/index.js b/lib/index.js index 3c7c31f..bb0b2ec 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,517 +1,524 @@ -'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] +;(function () { + '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]] } - else { - merged[char] = formatCharacters[char] + } + 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) + 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() } + } } - /** 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 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] + /** + * @param {string} source + * @patam {?Object} formatCharacters + */ + function Pattern(source, formatCharacters, placeholderChar, isRevealingMask) { + if (!(this instanceof Pattern)) { + return new Pattern(source, formatCharacters, placeholderChar) } - else if (char in this.formatCharacters) { - if (this.firstEditableIndex === null) { - this.firstEditableIndex = patternIndex + + /** 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 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.formatCharacters) { + if (this.firstEditableIndex === null) { + this.firstEditableIndex = patternIndex + } + this.lastEditableIndex = patternIndex + this._editableIndices[patternIndex] = true } - this.lastEditableIndex = patternIndex - this._editableIndices[patternIndex] = true + + pattern.push(char) + patternIndex++ } - pattern.push(char) - patternIndex++ - } + if (this.firstEditableIndex === null) { + throw new Error( + 'InputMask: pattern "' + this.source + '" does not contain any editable characters.' + ) + } - if (this.firstEditableIndex === null) { - throw new Error( - 'InputMask: pattern "' + this.source + '" does not contain any editable characters.' - ) + this.pattern = pattern + this.length = pattern.length } - this.pattern = pattern - this.length = pattern.length -} - -/** - * @param {Array} value - * @return {Array} - */ -Pattern.prototype.formatValue = function format(value) { - var valueBuffer = new Array(this.length) - var valueIndex = 0 - - for (var i = 0, l = this.length; i < l; i++) { - if (this.isEditableIndex(i)) { - if (this.isRevealingMask && - value.length <= valueIndex && - !this.isValidAtIndex(value[valueIndex], i)) { - break - } - valueBuffer[i] = (value.length > valueIndex && this.isValidAtIndex(value[valueIndex], i) - ? this.transform(value[valueIndex], i) - : this.placeholderChar) - valueIndex++ - } - else { - valueBuffer[i] = this.pattern[i] - // Also allow the value to contain static values from the pattern by - // advancing its index. - if (value.length > valueIndex && value[valueIndex] === this.pattern[i]) { + /** + * @param {Array} value + * @return {Array} + */ + Pattern.prototype.formatValue = function format(value) { + var valueBuffer = new Array(this.length) + var valueIndex = 0 + + for (var i = 0, l = this.length; i < l; i++) { + if (this.isEditableIndex(i)) { + if (this.isRevealingMask && + value.length <= valueIndex && + !this.isValidAtIndex(value[valueIndex], i)) { + break + } + valueBuffer[i] = (value.length > valueIndex && this.isValidAtIndex(value[valueIndex], i) + ? this.transform(value[valueIndex], i) + : this.placeholderChar) valueIndex++ } + else { + valueBuffer[i] = this.pattern[i] + // Also allow the value to contain static values from the pattern by + // advancing its index. + if (value.length > valueIndex && value[valueIndex] === this.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.') + return valueBuffer } - if (typeof options.placeholderChar !== 'string' || options.placeholderChar.length > 1) { - throw new Error('InputMask: placeholderChar should be a single character or an empty string.') + /** + * @param {number} index + * @return {boolean} + */ + Pattern.prototype.isEditableIndex = function isEditableIndex(index) { + return !!this._editableIndices[index] } - 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) { - // 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 + /** + * @param {string} char + * @param {number} index + * @return {boolean} + */ + Pattern.prototype.isValidAtIndex = function isValidAtIndex(char, index) { + return this.formatCharacters[this.pattern[index]].validate(char) } - 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 + Pattern.prototype.transform = function transform(char, index) { + var format = this.formatCharacters[this.pattern[index]] + return typeof format.transform == 'function' ? format.transform(char) : char } - // Bail out or add the character to input - if (this.pattern.isEditableIndex(inputIndex)) { - if (!this.pattern.isValidAtIndex(char, inputIndex)) { - return false + 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.') } - 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.pattern.isEditableIndex(end)) { - this.value[end] = this.placeholderChar + if (typeof options.placeholderChar !== 'string' || options.placeholderChar.length > 1) { + throw new Error('InputMask: placeholderChar should be a single character or an empty string.') } - end-- - } - // Advance the cursor to the next character - this.selection.start = this.selection.end = inputIndex + 1 + 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) { + // 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 + } - // Skip over any subsequent static characters - while (this.pattern.length > this.selection.start && - !this.pattern.isEditableIndex(this.selection.start)) { - this.selection.start++ - this.selection.end++ - } + var selectionBefore = copy(this.selection) + var valueBefore = this.getValue() - // 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() { - // If the cursor is at the start there's nothing to do - if (this.selection.start === 0 && this.selection.end === 0) { - return false - } + var inputIndex = this.selection.start - var selectionBefore = copy(this.selection) - var valueBefore = this.getValue() + // 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 + } - // 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 + // 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) } - this.selection.start-- - this.selection.end-- - } - // Range selected - delete characters and leave the cursor at the start of the selection - else { + + // If multiple characters were selected, blank the remainder out based on the + // pattern. var end = this.selection.end - 1 - while (end >= this.selection.start) { + while (end > inputIndex) { if (this.pattern.isEditableIndex(end)) { this.value[end] = this.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) { - // 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) - } + // Advance the cursor to the next character + this.selection.start = this.selection.end = inputIndex + 1 - // 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.pattern.pattern[i]) { - return false - } + // Skip over any subsequent static characters + while (this.pattern.length > this.selection.start && + !this.pattern.isEditableIndex(this.selection.start)) { + this.selection.start++ + this.selection.end++ } - // 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 + // 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 } - for (i = 0, l = input.length; - i < l && this.selection.start <= this.pattern.lastEditableIndex; - i++) { - var valid = this.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.selection.start > 0) { - // XXX This only allows for one static character to be skipped - var patternIndex = this.selection.start - 1 - if (!this.pattern.isEditableIndex(patternIndex) && - input.charAt(i) === this.pattern.pattern[patternIndex]) { - continue + /** + * 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() { + // 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.pattern.isEditableIndex(end)) { + this.value[end] = this.placeholderChar } + end-- } - extend(this, initialState) - return false + 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 } - 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) { + // 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) + } -// History + // 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.pattern.pattern[i]) { + return false + } + } -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 - } + // 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 + } - 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}) + for (i = 0, l = input.length; + i < l && this.selection.start <= this.pattern.lastEditableIndex; + i++) { + var valid = this.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.selection.start > 0) { + // XXX This only allows for one static character to be skipped + var patternIndex = this.selection.start - 1 + if (!this.pattern.isEditableIndex(patternIndex) && + input.charAt(i) === this.pattern.pattern[patternIndex]) { + continue + } + } + extend(this, initialState) + return false + } } - } - else { - historyItem = this._history[--this._historyIndex] + + return true } - this.value = historyItem.value.split('') - this.selection = historyItem.selection - this._lastOp = historyItem.lastOp - return true -} + // History -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() + 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 } - 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) { - 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 + + InputMask.prototype.redo = function redo() { + if (this._history.length === 0 || this._historyIndex == null) { + return false } - // 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.pattern.isEditableIndex(index - 1) && - this.value[index - 1] !== this.placeholderChar || - index === this.pattern.firstEditableIndex) { - this.selection.start = this.selection.end = index - break + 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() } - index-- } + this.value = historyItem.value.split('') + this.selection = historyItem.selection + this._lastOp = historyItem.lastOp return true } - return false -} -InputMask.prototype.setValue = function setValue(value) { - if (value == null) { - value = '' + // 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) { + 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.pattern.isEditableIndex(index - 1) && + this.value[index - 1] !== this.placeholderChar || + index === this.pattern.firstEditableIndex) { + this.selection.start = this.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('')) } - this.value = this.pattern.formatValue(value.split('')) -} -InputMask.prototype.getValue = function getValue() { - return this.value.join('') -} + InputMask.prototype.getValue = function getValue() { + return this.value.join('') + } -InputMask.prototype.getRawValue = function getRawValue() { - var rawValue = [] - for (var i = 0; i < this.value.length; i++) { - if (this.pattern._editableIndices[i] === true) { - rawValue.push(this.value[i]) + InputMask.prototype.getRawValue = function getRawValue() { + var rawValue = [] + for (var i = 0; i < this.value.length; i++) { + if (this.pattern._editableIndices[i] === true) { + rawValue.push(this.value[i]) + } } + return rawValue.join('') } - return rawValue.join('') -} -InputMask.prototype._resetHistory = function _resetHistory() { - this._history = [] - this._historyIndex = null - this._lastOp = null - this._lastSelection = copy(this.selection) -} + InputMask.prototype._resetHistory = function _resetHistory() { + this._history = [] + this._historyIndex = null + this._lastOp = null + this._lastSelection = copy(this.selection) + } -InputMask.Pattern = Pattern + InputMask.Pattern = Pattern -module.exports = InputMask + if (typeof exports === 'object') { + module.exports = InputMask + } + else { + window.InputMask = InputMask + } +})()