Permalink
Browse files

Merge pull request #38 from braintreeps/master

Add multi-pattern support.
2 parents 56be36b + 1239bc9 commit 4b94b66e1f4246df3875979dc6a460aa1e9d9d0e @jaridmargolin jaridmargolin committed Mar 20, 2014
Showing with 741 additions and 96 deletions.
  1. +2 −0 Gruntfile.js
  2. +8 −0 README.md
  3. +161 −30 lib/formatter.js
  4. +1 −1 lib/formatter.min.js
  5. +161 −30 lib/jquery.formatter.js
  6. +1 −1 lib/jquery.formatter.min.js
  7. +70 −21 src/formatter.js
  8. +73 −0 src/pattern-matcher.js
  9. +2 −2 src/pattern.js
  10. +22 −1 src/utils.js
  11. +67 −6 test/formatter.js
  12. +86 −0 test/pattern-matcher.js
  13. +30 −2 test/pattern.js
  14. +57 −2 test/utils.js
View
@@ -54,6 +54,7 @@ module.exports = function(grunt) {
'src/tmpls/intro.js',
'src/formatter.js',
'src/pattern.js',
+ 'src/pattern-matcher.js',
'src/inpt-sel.js',
'src/utils.js',
'src/tmpls/outro.js'
@@ -65,6 +66,7 @@ module.exports = function(grunt) {
'src/tmpls/jquery.intro.js',
'src/formatter.js',
'src/pattern.js',
+ 'src/pattern-matcher.js',
'src/inpt-sel.js',
'src/utils.js',
'src/tmpls/jquery.outro.js'
View
@@ -70,6 +70,14 @@ Opts
* a: [A-Za-z]
* \*: [A-Za-z0-9]
* **persistent**: \[False\] Boolean representing if the formatted characters are always visible (persistent), or if they appear as you type.
+* **patterns** (optional, replaces *pattern*): Array representing a priority ordered set of patterns that may apply dynamically based on the current input value. Each value in the array is an object, whose key is a regular expression string and value is a *pattern* (see above). The regular expression is tested against the unformatted input value. You may use the special key `'*'` to catch all input values.
+```
+[
+ { '^\d{5}$': 'zip: {{99999}}' },
+ { '^.{6,8}$: 'postal code: {{********}}' },
+ { '*': 'unknown: {{**********}}' }
+]
+```
View
@@ -45,16 +45,21 @@ function Formatter(el, opts) {
// Merge opts with defaults
self.opts = utils.extend({}, defaults, opts);
+ // 1 pattern is special case
+ if (typeof self.opts.pattern !== 'undefined') {
+ self.opts.patterns = self._specFromSinglePattern(self.opts.pattern);
+ delete self.opts.pattern;
+ }
+
// Make sure we have valid opts
- if (typeof self.opts.pattern === 'undefined') {
- throw new TypeError('Must provide a pattern');
+ if (typeof self.opts.patterns === 'undefined') {
+ throw new TypeError('Must provide a pattern or array of patterns');
}
- // Get info about the given pattern
- var parsed = pattern.parse(self.opts.pattern);
- self.mLength = parsed.mLength;
- self.chars = parsed.chars;
- self.inpts = parsed.inpts;
+ self.patternMatcher = patternMatcher(self.opts.patterns);
+
+ // Upate pattern with initial value
+ self._updatePattern();
// Init values
self.hldrs = {};
@@ -100,13 +105,11 @@ Formatter.addInptType = function (chr, reg) {
//
// @public
-// Handler called on all keyDown strokes. All keys trigger
-// this handler. Only process delete keys.
+// Apply the given pattern to the current input without moving caret.
//
Formatter.prototype.resetPattern = function (str) {
// Update opts to hold new pattern
- str = str || this.opts.pattern;
- this.opts.pattern = str;
+ this.opts.patterns = str ? this._specFromSinglePattern(str) : this.opts.patterns;
// Get current state
this.sel = inptSel.get(this.el);
@@ -118,14 +121,34 @@ Formatter.prototype.resetPattern = function (str) {
// Remove all formatted chars from val
this._removeChars();
+ this.patternMatcher = patternMatcher(this.opts.patterns);
+
// Update pattern
- var parsed = pattern.parse(str);
- this.mLength = parsed.mLength;
- this.chars = parsed.chars;
- this.inpts = parsed.inpts;
+ var newPattern = this.patternMatcher.getPattern(this.val);
+ this.mLength = newPattern.mLength;
+ this.chars = newPattern.chars;
+ this.inpts = newPattern.inpts;
// Format on start
- this._processKey('', false);
+ this._processKey('', false, true);
+};
+
+//
+// @private
+// Determine correct format pattern based on input val
+//
+Formatter.prototype._updatePattern = function () {
+ // Determine appropriate pattern
+ var newPattern = this.patternMatcher.getPattern(this.val);
+
+ // Only update the pattern if there is an appropriate pattern for the value.
+ // Otherwise, leave the current pattern (and likely delete the latest character.)
+ if (newPattern) {
+ // Get info about the given pattern
+ this.mLength = newPattern.mLength;
+ this.chars = newPattern.chars;
+ this.inpts = newPattern.inpts;
+ }
};
//
@@ -202,7 +225,7 @@ Formatter.prototype._focus = function () {
// @private
// Using the provided key information, alter el value.
//
-Formatter.prototype._processKey = function (chars, delKey) {
+Formatter.prototype._processKey = function (chars, delKey,ingoreCaret) {
// Get current state
this.sel = inptSel.get(this.el);
this.val = this.el.value;
@@ -222,8 +245,17 @@ Formatter.prototype._processKey = function (chars, delKey) {
// or Backspace and not at start
} else if (delKey && this.sel.begin - 1 >= 0) {
- this.val = utils.removeChars(this.val, this.sel.end -1, this.sel.end);
- this.delta = -1;
+
+ // Always have a delta of at least -1 for the character being deleted.
+ this.delta -= 1;
+
+ // Count number of additional format chars to be deleted. (A group of multiple format chars should be deleted like one value char.)
+ while (this.chars[this.focus-1]) {
+ this.delta--;
+ this.focus--;
+ }
+
+ this.val = utils.removeChars(this.val, this.sel.end + this.delta, this.sel.end);
// or Backspace and at start - exit
} else if (delKey) {
@@ -238,7 +270,7 @@ Formatter.prototype._processKey = function (chars, delKey) {
}
// Format el.value (also handles updating caret position)
- this._formatValue();
+ this._formatValue(ingoreCaret);
};
//
@@ -273,24 +305,31 @@ Formatter.prototype._nextPos = function () {
//
// @private
// Alter element value to display characters matching the provided
-// instance pattern. Also responsible for updatin
+// instance pattern. Also responsible for updating
//
-Formatter.prototype._formatValue = function () {
+Formatter.prototype._formatValue = function (ignoreCaret) {
// Set caret pos
this.newPos = this.sel.end + this.delta;
// Remove all formatted chars from val
this._removeChars();
- // Validate inpts
+
+ // Switch to first matching pattern based on val
+ this._updatePattern();
+
+ // Validate inputs
this._validateInpts();
+
// Add formatted characters
this._addChars();
- // Set vakye and adhere to maxLength
+ // Set value and adhere to maxLength
this.el.value = this.val.substr(0, this.mLength);
// Set new caret position
- inptSel.set(this.el, this.newPos);
+ if ((typeof ignoreCaret) === 'undefined' || ignoreCaret === false) {
+ inptSel.set(this.el, this.newPos);
+ }
};
//
@@ -378,11 +417,12 @@ Formatter.prototype._addChars = function () {
this.focus++;
}
} else {
- // Avoid caching val.length, as it changes during manipulations
+ // Avoid caching val.length and this.focus, as they may change in _addChar.
for (var j = 0; j <= this.val.length; j++) {
- // When moving backwards there are some race conditions where we
- // dont want to add the character
- if (this.delta <= 0 && (j == this.focus)) { return true; }
+ // When moving backwards, i.e. delting characters, don't add format characters past focus point.
+ if ( (this.delta <= 0 && j === this.focus && this.chars[j] === undefined) || (this.focus === 0) ) { return true; }
+
+ // Place character in current position of the formatted string.
this._addChar(j);
}
}
@@ -419,6 +459,16 @@ Formatter.prototype._addChar = function (i) {
this.val = utils.addChars(this.val, chr, i);
};
+//
+// @private
+// Create a patternSpec for passing into patternMatcher that
+// has exactly one catch all pattern.
+//
+Formatter.prototype._specFromSinglePattern = function (patternStr) {
+ return [{ '*': patternStr }];
+};
+
+
// Define module
var pattern = {};
@@ -472,7 +522,7 @@ pattern.parse = function (pattern) {
// Process match or add chars
for (i; i < pLength; i++) {
- if (i == matches[mCount].index) {
+ if (mCount < matches.length && i == matches[mCount].index) {
processMatch(matches[mCount][1]);
} else {
info.chars[i - (mCount * DELIM_SIZE)] = pattern.charAt(i);
@@ -483,6 +533,65 @@ pattern.parse = function (pattern) {
info.mLength = i - (mCount * DELIM_SIZE);
return info;
};
+
+//
+// Parse a matcher string into a RegExp. Accepts valid regular
+// expressions and the catchall '*'.
+// @private
+//
+var parseMatcher = function (matcher) {
+ if (matcher === '*') {
+ return /.*/;
+ }
+ return new RegExp(matcher);
+};
+
+//
+// Parse a pattern spec and return a function that returns a pattern
+// based on user input. The first matching pattern will be chosen.
+// Pattern spec format:
+// Array [
+// Object: { Matcher(RegExp String) : Pattern(Pattern String) },
+// ...
+// ]
+function patternMatcher (patternSpec) {
+ var matchers = [],
+ patterns = [];
+
+ // Iterate over each pattern in order.
+ utils.forEach(patternSpec, function (patternMatcher) {
+ // Process single property object to obtain pattern and matcher.
+ utils.forEach(patternMatcher, function (patternStr, matcherStr) {
+ var parsedPattern = pattern.parse(patternStr),
+ regExpMatcher = parseMatcher(matcherStr);
+
+ matchers.push(regExpMatcher);
+ patterns.push(parsedPattern);
+
+ // Stop after one iteration.
+ return false;
+ });
+ });
+
+ var getPattern = function (input) {
+ var matchedIndex;
+ utils.forEach(matchers, function (matcher, index) {
+ if (matcher.test(input)) {
+ matchedIndex = index;
+ return false;
+ }
+ });
+
+ return matchedIndex === undefined ? null : patterns[matchedIndex];
+ };
+
+ return {
+ getPattern: getPattern,
+ patterns: patterns,
+ matchers: matchers
+ };
+}
+
// Define module
var inptSel = {};
@@ -649,6 +758,28 @@ utils.isSpecialKey = function (k) {
utils.isModifier = function (evt) {
return evt.ctrlKey || evt.altKey || evt.metaKey;
};
+
+//
+// Iterates over each property of object or array.
+//
+utils.forEach = function (collection, callback, thisArg) {
+ if (collection.hasOwnProperty("length")) {
+ for (var index = 0, len = collection.length; index < len; index++) {
+ if (callback.call(thisArg, collection[index], index, collection) === false) {
+ break;
+ }
+ }
+ } else {
+ for (var key in collection) {
+ if (collection.hasOwnProperty(key)) {
+ if (callback.call(thisArg, collection[key], key, collection) === false) {
+ break;
+ }
+ }
+ }
+ }
+};
+
return Formatter;
});
Oops, something went wrong.

0 comments on commit 4b94b66

Please sign in to comment.