From ce18d9ce4312b00d931f0472a3fdfc631727b3c1 Mon Sep 17 00:00:00 2001 From: Rodrigo Azevedo Date: Mon, 13 Nov 2017 11:01:26 -0200 Subject: [PATCH 1/2] Some fix when using isRevealingMask: true - Create remove method and use it in backspace and input in a selection replace with placeholders on isRevealingMask: false and delete on true - Won't set placeholders when do backspace or input in a selection - Just move de selection to the next editable on input not after the input --- lib/index.js | 110 +++++++++++++++++++++++++++++++++----------------- test/index.js | 21 +++++----- 2 files changed, 83 insertions(+), 48 deletions(-) diff --git a/lib/index.js b/lib/index.js index 3c7c31f..b8a5dec 100644 --- a/lib/index.js +++ b/lib/index.js @@ -138,6 +138,10 @@ Pattern.prototype.formatValue = function format(value) { var valueBuffer = new Array(this.length) var valueIndex = 0 + if (this.isRevealingMask && value.length === 0) { + return valueBuffer + } + for (var i = 0, l = this.length; i < l; i++) { if (this.isEditableIndex(i)) { if (this.isRevealingMask && @@ -233,40 +237,44 @@ InputMask.prototype.input = function input(char) { 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 + // Find next editable index + var nextEditableIndex = inputIndex + while (!this.pattern.isEditableIndex(nextEditableIndex)) { + if (nextEditableIndex > this.pattern.lastEditableIndex) { + return false + } + nextEditableIndex++ } - // Bail out or add the character to input - if (this.pattern.isEditableIndex(inputIndex)) { - if (!this.pattern.isValidAtIndex(char, inputIndex)) { - return false + if (!this.pattern.isValidAtIndex(char, nextEditableIndex)) { + return false + } + + // Add statics until next editable + while (inputIndex < nextEditableIndex) { + this.value[inputIndex] = this.pattern.pattern[inputIndex] + inputIndex++ + } + + this.value[inputIndex] = this.pattern.transform(char, inputIndex) + + // If this is the last editable index fill with the rest + if (inputIndex === this.pattern.lastEditableIndex) { + while (inputIndex + 1 < this.pattern.length - 1) { + inputIndex++ + this.value[inputIndex] = this.pattern.pattern[inputIndex] } - 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 - } - end-- + if (inputIndex + 1 < this.selection.end) { + this.remove(inputIndex + 1, this.selection.end - 1) } // 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.selection.start++ - this.selection.end++ - } - // History if (this._historyIndex != null) { // Took more input after undoing, so blow any subsequent history away @@ -299,26 +307,26 @@ InputMask.prototype.backspace = function backspace() { var selectionBefore = copy(this.selection) var valueBefore = this.getValue() - // No range selected - work on the character preceding the cursor + // No range selected if (this.selection.start === this.selection.end) { - if (this.pattern.isEditableIndex(this.selection.start - 1)) { - this.value[this.selection.start - 1] = this.placeholderChar + var previousEditableIndex = this.selection.start - 1 + + while (!this.pattern.isEditableIndex(previousEditableIndex)) { + if (previousEditableIndex === 0) { + break + } + previousEditableIndex-- } - this.selection.start-- - this.selection.end-- + + this.remove(previousEditableIndex, this.selection.end) + this.selection.start = previousEditableIndex } - // 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-- - } - this.selection.end = this.selection.start + this.remove(this.selection.start, this.selection.end - 1) } + this.selection.end = this.selection.start + // History if (this._historyIndex != null) { // Took more input after undoing, so blow any subsequent history away @@ -381,7 +389,7 @@ InputMask.prototype.paste = function paste(input) { if (!valid) { if (this.selection.start > 0) { // XXX This only allows for one static character to be skipped - var patternIndex = this.selection.start - 1 + var patternIndex = this.selection.start if (!this.pattern.isEditableIndex(patternIndex) && input.charAt(i) === this.pattern.pattern[patternIndex]) { continue @@ -395,6 +403,34 @@ InputMask.prototype.paste = function paste(input) { return true } +InputMask.prototype.remove = function remove(start, end) { + if (this.pattern.isRevealingMask) { + this.value.splice(start, end - start) + + var index = start + while (index < this.value.length) { + if (!this.pattern.isEditableIndex(index)) { + this.value.splice(index, 0, this.pattern.pattern[index]) + index++ + } + else if (this.pattern.isValidAtIndex(this.value[index], index)) { + index++ + } + else { + this.value.splice(index, 1) + } + } + } + else { + while (end >= start) { + if (this.pattern.isEditableIndex(end)) { + this.value[end] = this.placeholderChar + } + end-- + } + } +} + // History InputMask.prototype.undo = function undo() { diff --git a/test/index.js b/test/index.js index 34f473e..1c90019 100644 --- a/test/index.js +++ b/test/index.js @@ -143,7 +143,7 @@ test('Escaping placeholder characters', function(t) { }) test('Basic input', function(t) { - t.plan(23) + t.plan(26) var mask = new InputMask({ pattern: '1111 1111 1111 1111' @@ -156,18 +156,21 @@ test('Basic input', function(t) { t.true(mask.input('2'), 'Valid input accepted') t.true(mask.input('3'), 'Valid input accepted') t.true(mask.input('4'), 'Valid input accepted') - t.deepEqual(mask.selection, {start: 5, end: 5}, 'Skipped over blank') + t.deepEqual(mask.selection, {start: 4, end: 4}, 'Keep in position') t.true(mask.input('1'), 'Valid input accepted') + t.deepEqual(mask.selection, {start: 6, end: 6}, 'Skipped over blank after input') t.true(mask.input('2'), 'Valid input accepted') t.true(mask.input('3'), 'Valid input accepted') t.true(mask.input('4'), 'Valid input accepted') - t.deepEqual(mask.selection, {start: 10, end: 10}, 'Skipped over blank') + t.deepEqual(mask.selection, {start: 9, end: 9}, 'Keep in position') t.true(mask.input('1'), 'Valid input accepted') + t.deepEqual(mask.selection, {start: 11, end: 11}, 'Skipped over blank') t.true(mask.input('2'), 'Valid input accepted') t.true(mask.input('3'), 'Valid input accepted') t.true(mask.input('4'), 'Valid input accepted') - t.deepEqual(mask.selection, {start: 15, end: 15}, 'Skipped over blank') + t.deepEqual(mask.selection, {start: 14, end: 14}, 'Keep in position') t.true(mask.input('1'), 'Valid input accepted') + t.deepEqual(mask.selection, {start: 16, end: 16}, 'Skipped over blank') t.true(mask.input('2'), 'Valid input accepted') t.true(mask.input('3'), 'Valid input accepted') t.true(mask.input('4'), 'Valid input accepted') @@ -256,7 +259,7 @@ test('Skipping multiple static characters', function(t) { }) test('Basic backspacing', function(t) { - t.plan(24) + t.plan(21) var mask = new InputMask({ pattern: '1111 1111 1111 1111', @@ -268,22 +271,18 @@ test('Basic backspacing', function(t) { t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') - // Backspacking doesn't automatically skip characters, as we can't tell when - // the user intends to start making input again, so it just steps over static - // parts of the mask when you backspace with the cursor ahead of them. - t.true(mask.backspace(), 'Skipped over blank') + // Backspacking automatically skip characters, and goes to the + // previous valid editable character t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') t.equal(mask.getValue(), '1234 1234 ____ ____', 'Intermediate value') t.deepEqual(mask.selection, {start: 10, end: 10}, 'Cursor remains in front of last deleted character') - t.true(mask.backspace(), 'Skipped over blank') t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') - t.true(mask.backspace(), 'Skipped over blank') t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') t.true(mask.backspace(), 'Valid backspace accepted') From 9cca2b09c28f874a9123b5854dbe4b6142916903 Mon Sep 17 00:00:00 2001 From: Rodrigo Azevedo Date: Tue, 14 Nov 2017 00:45:30 -0200 Subject: [PATCH 2/2] Add optional character support --- lib/index.js | 247 ++++++++++++++++++++++++++++++++++++++++---------- test/index.js | 27 +++++- 2 files changed, 222 insertions(+), 52 deletions(-) diff --git a/lib/index.js b/lib/index.js index b8a5dec..2f30157 100644 --- a/lib/index.js +++ b/lib/index.js @@ -38,6 +38,9 @@ function mergeFormatCharacters(formatCharacters) { } var ESCAPE_CHAR = '\\' +var OPTIONAL_CHAR = '?' +var STATIC_CHAR = 'static' +var EDITABLE_CHAR = 'editable' var DIGIT_RE = /^\d$/ var LETTER_RE = /^[A-Za-z]$/ @@ -87,8 +90,6 @@ function Pattern(source, formatCharacters, placeholderChar, isRevealingMask) { 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 @@ -102,21 +103,32 @@ Pattern.prototype._parse = function parse() { for (var i = 0, l = sourceChars.length; i < l; i++) { var char = sourceChars[i] + var type = STATIC_CHAR + 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 === OPTIONAL_CHAR) { + pattern[patternIndex - 1].optional = true + continue + } else if (char in this.formatCharacters) { if (this.firstEditableIndex === null) { this.firstEditableIndex = patternIndex } this.lastEditableIndex = patternIndex - this._editableIndices[patternIndex] = true + type = EDITABLE_CHAR } - pattern.push(char) + pattern.push({ + char: char, + type: type, + index: patternIndex, + optional: false + }) patternIndex++ } @@ -135,36 +147,79 @@ Pattern.prototype._parse = function parse() { * @return {Array} */ Pattern.prototype.formatValue = function format(value) { - var valueBuffer = new Array(this.length) + var valueBuffer = [] var valueIndex = 0 + var optionals = {} + if (this.isRevealingMask && value.length === 0) { return valueBuffer } for (var i = 0, l = this.length; i < l; i++) { - if (this.isEditableIndex(i)) { + var char = value[valueIndex] || null + + if (this.isOptionalIndex(i) && !optionals[i]) { + optionals[i] = { + valueIndex: valueIndex, + patternIndex: i, + pending: true + } + + valueBuffer[i] = null + valueIndex-- + } + else if (!!value[valueIndex + 1] && + i === this.lastEditableIndex && + pendingOptionals().length > 0) { + var optional = nextPendingOptional() + + valueBuffer = valueBuffer.slice(0, optional.patternIndex) + valueIndex = optional.valueIndex - 1 + i = optional.patternIndex - 1 + + optional.pending = false + } + else if (this.isEditableIndex(i)) { if (this.isRevealingMask && - value.length <= valueIndex && - !this.isValidAtIndex(value[valueIndex], i)) { + !this.isValidAtIndex(char, i)) { break } - valueBuffer[i] = (value.length > valueIndex && this.isValidAtIndex(value[valueIndex], i) - ? this.transform(value[valueIndex], i) + valueBuffer[i] = (char !== null && this.isValidAtIndex(char, i) + ? this.transform(char, i) : this.placeholderChar) - valueIndex++ } else { - valueBuffer[i] = this.pattern[i] + valueBuffer[i] = this.pattern[i].char // 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++ + if (char === null || char !== this.pattern[i].char) { + valueIndex-- } } + + if (this.isRevealingMask && + !value[valueIndex + 1] && + i < this.lastEditableIndex) { + break + } + + valueIndex++ } return valueBuffer + + function pendingOptionals() { + return Object.keys(optionals).filter(function(index) { + return optionals[index].pending === true + }) + } + + function nextPendingOptional() { + var lastIndex = Math.max.apply(null, pendingOptionals()) + + return optionals[lastIndex] + } } /** @@ -172,7 +227,23 @@ Pattern.prototype.formatValue = function format(value) { * @return {boolean} */ Pattern.prototype.isEditableIndex = function isEditableIndex(index) { - return !!this._editableIndices[index] + if (!this.pattern[index]) { + return false + } + + return this.pattern[index].type === EDITABLE_CHAR +} + +/** + * @param {number} index + * @return {boolean} + */ +Pattern.prototype.isOptionalIndex = function isOptionalIndex(index) { + if (!this.pattern[index]) { + return false + } + + return this.pattern[index].optional } /** @@ -181,12 +252,24 @@ Pattern.prototype.isEditableIndex = function isEditableIndex(index) { * @return {boolean} */ Pattern.prototype.isValidAtIndex = function isValidAtIndex(char, index) { - return this.formatCharacters[this.pattern[index]].validate(char) + if (this.pattern[index].type == EDITABLE_CHAR) { + return this.formatCharacters[this.pattern[index].char].validate(char) + } + + return this.pattern[index].char === char } Pattern.prototype.transform = function transform(char, index) { - var format = this.formatCharacters[this.pattern[index]] - return typeof format.transform == 'function' ? format.transform(char) : char + if (this.pattern[index].type == EDITABLE_CHAR) { + var format = this.formatCharacters[this.pattern[index].char] + return typeof format.transform == 'function' ? format.transform(char) : char + } + + return char +} + +Pattern.prototype.charAt = function charAt(index) { + return this.pattern[index].char } function InputMask(options) { @@ -228,8 +311,17 @@ function InputMask(options) { 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 + (this.selection.start === this.pattern.length || + this.selection.start > this.pattern.lastEditableIndex)) { + var pendingOptional = this.pendingOptional() + + if (pendingOptional === -1) { + return false + } + + this._fillOptional() + this.selection.start-- + this.selection.end-- } var selectionBefore = copy(this.selection) @@ -239,7 +331,11 @@ InputMask.prototype.input = function input(char) { // Find next editable index var nextEditableIndex = inputIndex - while (!this.pattern.isEditableIndex(nextEditableIndex)) { + while (!this.pattern.isEditableIndex(nextEditableIndex) || this.pattern.isOptionalIndex(nextEditableIndex)) { + if (!this.pattern.isEditableIndex(nextEditableIndex) && + this.pattern.isValidAtIndex(char, nextEditableIndex)) { + break + } if (nextEditableIndex > this.pattern.lastEditableIndex) { return false } @@ -252,17 +348,23 @@ InputMask.prototype.input = function input(char) { // Add statics until next editable while (inputIndex < nextEditableIndex) { - this.value[inputIndex] = this.pattern.pattern[inputIndex] + if (this.pattern.isOptionalIndex(inputIndex)) { + this.value[inputIndex].value = null + } + else { + this.value[inputIndex].value = this.pattern.charAt(inputIndex) + } inputIndex++ } - this.value[inputIndex] = this.pattern.transform(char, inputIndex) + this.value[inputIndex].value = this.pattern.transform(char, inputIndex) // If this is the last editable index fill with the rest - if (inputIndex === this.pattern.lastEditableIndex) { - while (inputIndex + 1 < this.pattern.length - 1) { - inputIndex++ - this.value[inputIndex] = this.pattern.pattern[inputIndex] + if (inputIndex === this.pattern.lastEditableIndex && + this.pendingOptional() === -1) { + var index = inputIndex + while (index < this.pattern.length - 1) { + this.value[++index].value = this.pattern.charAt(index) } } @@ -368,7 +470,7 @@ InputMask.prototype.paste = function paste(input) { // 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]) { + if (input.charAt(i) !== this.pattern.charAt(i)) { return false } } @@ -391,7 +493,7 @@ InputMask.prototype.paste = function paste(input) { // XXX This only allows for one static character to be skipped var patternIndex = this.selection.start if (!this.pattern.isEditableIndex(patternIndex) && - input.charAt(i) === this.pattern.pattern[patternIndex]) { + input.charAt(i) === this.pattern.charAt(patternIndex)) { continue } } @@ -405,26 +507,31 @@ InputMask.prototype.paste = function paste(input) { InputMask.prototype.remove = function remove(start, end) { if (this.pattern.isRevealingMask) { - this.value.splice(start, end - start) + for (var i = start; i < end; i++) { + this.value[i].value = null + } - var index = start - while (index < this.value.length) { - if (!this.pattern.isEditableIndex(index)) { - this.value.splice(index, 0, this.pattern.pattern[index]) - index++ + var indexInput = start + for (i = end; i < this.value.length; i++) { + if (!this.pattern.isEditableIndex(indexInput)) { + this.value[indexInput].value = this.pattern.charAt(indexInput) + indexInput++ } - else if (this.pattern.isValidAtIndex(this.value[index], index)) { - index++ - } - else { - this.value.splice(index, 1) + + if (this.pattern.isEditableIndex(i) && + this.pattern.isValidAtIndex(this.value[i].value, indexInput)) { + this.value[indexInput].value = this.value[i].value + + indexInput++ } + + this.value[i].value = null } } else { while (end >= start) { if (this.pattern.isEditableIndex(end)) { - this.value[end] = this.placeholderChar + this.value[end].value = this.placeholderChar } end-- } @@ -457,7 +564,7 @@ InputMask.prototype.undo = function undo() { historyItem = this._history[--this._historyIndex] } - this.value = historyItem.value.split('') + this.setValue(historyItem.value) this.selection = historyItem.selection this._lastOp = historyItem.lastOp return true @@ -476,7 +583,7 @@ InputMask.prototype.redo = function redo() { this._history.pop() } } - this.value = historyItem.value.split('') + this.setValue(historyItem.value) this.selection = historyItem.selection this._lastOp = historyItem.lastOp return true @@ -490,6 +597,7 @@ InputMask.prototype.setPattern = function setPattern(pattern, options) { value: '' }, options) this.pattern = new Pattern(pattern, this.formatCharacters, this.placeholderChar, options.isRevealingMask) + this.value = this.pattern.pattern.slice() this.setValue(options.value) this.emptyValue = this.pattern.formatValue([]).join('') this.selection = options.selection @@ -508,7 +616,7 @@ InputMask.prototype.setSelection = function setSelection(selection) { var index = this.selection.start while (index >= this.pattern.firstEditableIndex) { if (this.pattern.isEditableIndex(index - 1) && - this.value[index - 1] !== this.placeholderChar || + this.value[index - 1].value !== this.placeholderChar || index === this.pattern.firstEditableIndex) { this.selection.start = this.selection.end = index break @@ -524,23 +632,66 @@ InputMask.prototype.setValue = function setValue(value) { if (value == null) { value = '' } - this.value = this.pattern.formatValue(value.split('')) + + var formatedValue = this.pattern.formatValue(value.split('')) + + this.value = this.value.map(function(pattern, index) { + pattern.value = formatedValue[index] || null + return pattern + }) } InputMask.prototype.getValue = function getValue() { - return this.value.join('') + return this.value.map(function(pattern) { + return pattern.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]) + if (this.pattern.isEditableIndex(i) === true) { + rawValue.push(this.value[i].value) } } return rawValue.join('') } +InputMask.prototype.pendingOptional = function pendingOptional() { + var pendings = this.value.filter(function(pattern) { + return pattern.optional && pattern.value == null + }).map(function(pattern) { + return pattern.index + }) + + if (pendings.length === 0) { + return -1 + } + + return Math.max.apply(null, pendings) +} + +InputMask.prototype._fillOptional = function _fillOptional() { + var pendingIndex = this.pendingOptional() + + var indexInput = pendingIndex + for (var i = pendingIndex + 1; i < this.value.length; i++) { + if (!this.pattern.isEditableIndex(indexInput)) { + this.value[indexInput].value = this.pattern.charAt(indexInput) + indexInput++ + } + + if (this.pattern.isEditableIndex(i) && + this.pattern.isValidAtIndex(this.value[i].value, indexInput)) { + this.value[indexInput].value = this.value[i].value + + indexInput++ + } + + this.value[i].value = null + } +} + InputMask.prototype._resetHistory = function _resetHistory() { this._history = [] this._historyIndex = null diff --git a/test/index.js b/test/index.js index 1c90019..f33e929 100644 --- a/test/index.js +++ b/test/index.js @@ -45,7 +45,7 @@ test('formatValueToPattern', function(t) { }) test('Constructor options', function(t) { - t.plan(24) + t.plan(25) t.throws(function() { new InputMask() }, /InputMask: you must provide a pattern./, @@ -118,9 +118,11 @@ test('Constructor options', function(t) { mask = new InputMask({pattern: '111-1111 x 111', value: '47', isRevealingMask: true}) t.equal(mask.getValue(), '47', 'no mask characters or placeholders are revealed') mask = new InputMask({pattern: '111-1111 x 111', value: '476', isRevealingMask: true}) - t.equal(mask.getValue(), '476-', 'mask is revealed up to the next editable character') - mask = new InputMask({pattern: '111-1111 x 111', value: '47 3191', isRevealingMask: true}) - t.equal(mask.getValue(), '47_-3191 x ', 'mask is revealed up to the last value') + t.equal(mask.getValue(), '476', 'mask is revealed just after input the next editable character') + mask = new InputMask({pattern: '111-1111 x 111', value: '47631911', isRevealingMask: true}) + t.equal(mask.getValue(), '476-3191 x 1', 'mask is revealed after input next valid editable input') + mask = new InputMask({pattern: '111-1111 x', value: '4763191', isRevealingMask: true}) + t.equal(mask.getValue(), '476-3191 x', 'mask is revealed after input last editable character') }) test('Formatting characters', function(t) { @@ -451,3 +453,20 @@ test('History', function(t) { t.deepEqual([mask.getValue(), mask.selection], ['abc123', {start: 6, end: 6}]) t.false(mask.redo(), 'invalid redo - nothing more to redo') }) + +test('Optional Char', function(t) { + t.plan(6) + + var mask = new InputMask({pattern: '(11) 1?1111-1?111'}) + t.true(mask.pattern.isOptionalIndex(5)) + t.false(mask.pattern.isOptionalIndex(6)) + + t.equal(mask.getValue(), '(__) ____-___') + + mask.setValue('114433350') + t.equal(mask.getValue(), '(11) 4433-350') + mask.setValue('1144333501') + t.equal(mask.getValue(), '(11) 4433-3501') + mask.setValue('11994348849') + t.equal(mask.getValue(), '(11) 99434-8849') +})