|
|
/*! * v0.1.5 * Copyright (c) 2014 First Opinion * formatter.js is open sourced under the MIT license. * * thanks to digitalBush/jquery.maskedinput for some of the trickier * keycode handling */
//
// Uses CommonJS, AMD or browser globals to create a jQuery plugin.
//
// Similar to jqueryPlugin.js but also tries to
// work in a CommonJS environment.
// It is unlikely jQuery will run in a CommonJS
// environment. See jqueryPlugin.js if you do
// not want to add the extra CommonJS detection.
//
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module.
define(['jQuery'], factory); } else if (typeof exports === 'object') { factory(require('jQuery')); } else { // Browser globals
factory(root.jQuery); } }(this, function (jQuery) {
/* * pattern.js * * Utilities to parse str pattern and return info * */ var pattern = function () { // Define module
var pattern = {}; // Match information
var DELIM_SIZE = 4; // Our regex used to parse
var regexp = new RegExp('{{([^}]+)}}', 'g'); //
// Helper method to parse pattern str
//
var getMatches = function (pattern) { // Populate array of matches
var matches = [], match; while (match = regexp.exec(pattern)) { matches.push(match); } return matches; }; //
// Create an object holding all formatted characters
// with corresponding positions
//
pattern.parse = function (pattern) { // Our obj to populate
var info = { inpts: {}, chars: {} }; // Pattern information
var matches = getMatches(pattern), pLength = pattern.length; // Counters
var mCount = 0, iCount = 0, i = 0; // Add inpts, move to end of match, and process
var processMatch = function (val) { var valLength = val.length; for (var j = 0; j < valLength; j++) { info.inpts[iCount] = val.charAt(j); iCount++; } mCount++; i += val.length + DELIM_SIZE - 1; }; // Process match or add chars
for (i; i < pLength; i++) { if (mCount < matches.length && i === matches[mCount].index) { processMatch(matches[mCount][1]); } else { info.chars[i - mCount * DELIM_SIZE] = pattern.charAt(i); } } // Set mLength and return
info.mLength = i - mCount * DELIM_SIZE; return info; }; // Expose
return pattern; }(); /* * utils.js * * Independent helper methods (cross browser, etc..) * */ var utils = function () { // Define module
var utils = {}; // Useragent info for keycode handling
var uAgent = typeof navigator !== 'undefined' ? navigator.userAgent : null; //
// Shallow copy properties from n objects to destObj
//
utils.extend = function (destObj) { for (var i = 1; i < arguments.length; i++) { for (var key in arguments[i]) { destObj[key] = arguments[i][key]; } } return destObj; }; //
// Add a given character to a string at a defined pos
//
utils.addChars = function (str, chars, pos) { return str.substr(0, pos) + chars + str.substr(pos, str.length); }; //
// Remove a span of characters
//
utils.removeChars = function (str, start, end) { return str.substr(0, start) + str.substr(end, str.length); }; //
// Return true/false is num false between bounds
//
utils.isBetween = function (num, bounds) { bounds.sort(function (a, b) { return a - b; }); return num > bounds[0] && num < bounds[1]; }; //
// Helper method for cross browser event listeners
//
utils.addListener = function (el, evt, handler) { return typeof el.addEventListener !== 'undefined' ? el.addEventListener(evt, handler, false) : el.attachEvent('on' + evt, handler); }; //
// Helper method for cross browser implementation of preventDefault
//
utils.preventDefault = function (evt) { return evt.preventDefault ? evt.preventDefault() : evt.returnValue = false; }; //
// Helper method for cross browser implementation for grabbing
// clipboard data
//
utils.getClip = function (evt) { if (evt.clipboardData) { return evt.clipboardData.getData('Text'); } if (window.clipboardData) { return window.clipboardData.getData('Text'); } }; //
// Loop over object and checking for matching properties
//
utils.getMatchingKey = function (which, keyCode, keys) { // Loop over and return if matched.
for (var k in keys) { var key = keys[k]; if (which === key.which && keyCode === key.keyCode) { return k; } } }; //
// Returns true/false if k is a del keyDown
//
utils.isDelKeyDown = function (which, keyCode) { var keys = { 'backspace': { 'which': 8, 'keyCode': 8 }, 'delete': { 'which': 46, 'keyCode': 46 } }; return utils.getMatchingKey(which, keyCode, keys); }; //
// Returns true/false if k is a del keyPress
//
utils.isDelKeyPress = function (which, keyCode) { var keys = { 'backspace': { 'which': 8, 'keyCode': 8, 'shiftKey': false }, 'delete': { 'which': 0, 'keyCode': 46 } }; return utils.getMatchingKey(which, keyCode, keys); }; // //
// // Determine if keydown relates to specialKey
// //
// utils.isSpecialKeyDown = function (which, keyCode) {
// var keys = {
// 'tab': { 'which': 9, 'keyCode': 9 },
// 'enter': { 'which': 13, 'keyCode': 13 },
// 'end': { 'which': 35, 'keyCode': 35 },
// 'home': { 'which': 36, 'keyCode': 36 },
// 'leftarrow': { 'which': 37, 'keyCode': 37 },
// 'uparrow': { 'which': 38, 'keyCode': 38 },
// 'rightarrow': { 'which': 39, 'keyCode': 39 },
// 'downarrow': { 'which': 40, 'keyCode': 40 },
// 'F5': { 'which': 116, 'keyCode': 116 }
// };
// return utils.getMatchingKey(which, keyCode, keys);
// };
//
// Determine if keypress relates to specialKey
//
utils.isSpecialKeyPress = function (which, keyCode) { var keys = { 'tab': { 'which': 0, 'keyCode': 9 }, 'enter': { 'which': 13, 'keyCode': 13 }, 'end': { 'which': 0, 'keyCode': 35 }, 'home': { 'which': 0, 'keyCode': 36 }, 'leftarrow': { 'which': 0, 'keyCode': 37 }, 'uparrow': { 'which': 0, 'keyCode': 38 }, 'rightarrow': { 'which': 0, 'keyCode': 39 }, 'downarrow': { 'which': 0, 'keyCode': 40 }, 'F5': { 'which': 116, 'keyCode': 116 } }; return utils.getMatchingKey(which, keyCode, keys); }; //
// Returns true/false if modifier key is held down
//
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; } } } } }; // Expose
return utils; }(); /* * pattern-matcher.js * * Parses a pattern specification and determines appropriate pattern for an * input string * */ var patternMatcher = function (pattern, utils) { //
// 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 }; } // Expose
return patternMatcher; }(pattern, utils); /* * inpt-sel.js * * Cross browser implementation to get and set input selections * */ var inptSel = function () { // Define module
var inptSel = {}; //
// Get begin and end positions of selected input. Return 0's
// if there is no selectiion data
//
inptSel.get = function (el) { // If normal browser return with result
if (typeof el.selectionStart === 'number') { return { begin: el.selectionStart, end: el.selectionEnd }; } // Uh-Oh. We must be IE. Fun with TextRange!!
var range = document.selection.createRange(); // Determine if there is a selection
if (range && range.parentElement() === el) { var inputRange = el.createTextRange(), endRange = el.createTextRange(), length = el.value.length; // Create a working TextRange for the input selection
inputRange.moveToBookmark(range.getBookmark()); // Move endRange begin pos to end pos (hence endRange)
endRange.collapse(false); // If we are at the very end of the input, begin and end
// must both be the length of the el.value
if (inputRange.compareEndPoints('StartToEnd', endRange) > -1) { return { begin: length, end: length }; } // Note: moveStart usually returns the units moved, which
// one may think is -length, however, it will stop when it
// gets to the begin of the range, thus giving us the
// negative value of the pos.
return { begin: -inputRange.moveStart('character', -length), end: -inputRange.moveEnd('character', -length) }; } //Return 0's on no selection data
return { begin: 0, end: 0 }; }; //
// Set the caret position at a specified location
//
inptSel.set = function (el, pos) { // Normalize pos
if (typeof pos !== 'object') { pos = { begin: pos, end: pos }; } // If normal browser
if (el.setSelectionRange) { el.focus(); el.setSelectionRange(pos.begin, pos.end); } else if (el.createTextRange) { var range = el.createTextRange(); range.collapse(true); range.moveEnd('character', pos.end); range.moveStart('character', pos.begin); range.select(); } }; // Expose
return inptSel; }(); /* * formatter.js * * Class used to format input based on passed pattern * */ var formatter = function (patternMatcher, inptSel, utils) { // Defaults
var defaults = { persistent: false, repeat: false, placeholder: ' ' }; // Regexs for input validation
var inptRegs = { '9': /[0-9]/, 'a': /[A-Za-z]/, '*': /[A-Za-z0-9]/ }; //
// Class Constructor - Called with new Formatter(el, opts)
// Responsible for setting up required instance variables, and
// attaching the event listener to the element.
//
function Formatter(el, opts) { // Cache this
var self = this; // Make sure we have an element. Make accesible to instance
self.el = el; if (!self.el) { throw new TypeError('Must provide an existing element'); } // 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.patterns === 'undefined') { throw new TypeError('Must provide a pattern or array of patterns'); } self.patternMatcher = patternMatcher(self.opts.patterns); // Upate pattern with initial value
self._updatePattern(); // Init values
self.hldrs = {}; self.focus = 0; // Add Listeners
utils.addListener(self.el, 'keydown', function (evt) { self._keyDown(evt); }); utils.addListener(self.el, 'keypress', function (evt) { self._keyPress(evt); }); utils.addListener(self.el, 'paste', function (evt) { self._paste(evt); }); // Persistence
if (self.opts.persistent) { // Format on start
self._processKey('', false); self.el.blur(); // Add Listeners
utils.addListener(self.el, 'focus', function (evt) { self._focus(evt); }); utils.addListener(self.el, 'click', function (evt) { self._focus(evt); }); utils.addListener(self.el, 'touchstart', function (evt) { self._focus(evt); }); } } //
// @public
// Add new char
//
Formatter.addInptType = function (chr, reg) { inptRegs[chr] = reg; }; //
// @public
// Apply the given pattern to the current input without moving caret.
//
Formatter.prototype.resetPattern = function (str) { // Update opts to hold new pattern
this.opts.patterns = str ? this._specFromSinglePattern(str) : this.opts.patterns; // Get current state
this.sel = inptSel.get(this.el); this.val = this.el.value; // Init values
this.delta = 0; // Remove all formatted chars from val
this._removeChars(); this.patternMatcher = patternMatcher(this.opts.patterns); // Update pattern
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, 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; } }; //
// @private
// Handler called on all keyDown strokes. All keys trigger
// this handler. Only process delete keys.
//
Formatter.prototype._keyDown = function (evt) { // The first thing we need is the character code
var k = evt.which || evt.keyCode; // If delete key
if (k && utils.isDelKeyDown(evt.which, evt.keyCode)) { // Process the keyCode and prevent default
this._processKey(null, k); return utils.preventDefault(evt); } }; //
// @private
// Handler called on all keyPress strokes. Only processes
// character keys (as long as no modifier key is in use).
//
Formatter.prototype._keyPress = function (evt) { // The first thing we need is the character code
var k, isSpecial; // Mozilla will trigger on special keys and assign the the value 0
// We want to use that 0 rather than the keyCode it assigns.
k = evt.which || evt.keyCode; isSpecial = utils.isSpecialKeyPress(evt.which, evt.keyCode); // Process the keyCode and prevent default
if (!utils.isDelKeyPress(evt.which, evt.keyCode) && !isSpecial && !utils.isModifier(evt)) { this._processKey(String.fromCharCode(k), false); return utils.preventDefault(evt); } }; //
// @private
// Handler called on paste event.
//
Formatter.prototype._paste = function (evt) { // Process the clipboard paste and prevent default
this._processKey(utils.getClip(evt), false); return utils.preventDefault(evt); }; //
// @private
// Handle called on focus event.
//
Formatter.prototype._focus = function () { // Wrapped in timeout so that we can grab input selection
var self = this; setTimeout(function () { // Grab selection
var selection = inptSel.get(self.el); // Char check
var isAfterStart = selection.end > self.focus, isFirstChar = selection.end === 0; // If clicked in front of start, refocus to start
if (isAfterStart || isFirstChar) { inptSel.set(self.el, self.focus); } }, 0); }; //
// @private
// Using the provided key information, alter el value.
//
Formatter.prototype._processKey = function (chars, delKey, ignoreCaret) { // Get current state
this.sel = inptSel.get(this.el); this.val = this.el.value; // Init values
this.delta = 0; // If chars were highlighted, we need to remove them
if (this.sel.begin !== this.sel.end) { this.delta = -1 * Math.abs(this.sel.begin - this.sel.end); this.val = utils.removeChars(this.val, this.sel.begin, this.sel.end); } else if (delKey && delKey === 46) { this._delete(); } else if (delKey && this.sel.begin - 1 >= 0) { // Always have a delta of at least -1 for the character being deleted.
this.val = utils.removeChars(this.val, this.sel.end - 1, this.sel.end); this.delta -= 1; } else if (delKey) { return true; } // If the key is not a del key, it should convert to a str
if (!delKey) { // Add char at position and increment delta
this.val = utils.addChars(this.val, chars, this.sel.begin); this.delta += chars.length; } // Format el.value (also handles updating caret position)
this._formatValue(ignoreCaret); }; //
// @private
// Deletes the character in front of it
//
Formatter.prototype._delete = function () { // Adjust focus to make sure its not on a formatted char
while (this.chars[this.sel.begin]) { this._nextPos(); } // As long as we are not at the end
if (this.sel.begin < this.val.length) { // We will simulate a delete by moving the caret to the next char
// and then deleting
this._nextPos(); this.val = utils.removeChars(this.val, this.sel.end - 1, this.sel.end); this.delta = -1; } }; //
// @private
// Quick helper method to move the caret to the next pos
//
Formatter.prototype._nextPos = function () { this.sel.end++; this.sel.begin++; }; //
// @private
// Alter element value to display characters matching the provided
// instance pattern. Also responsible for updating
//
Formatter.prototype._formatValue = function (ignoreCaret) { // Set caret pos
this.newPos = this.sel.end + this.delta; // Remove all formatted chars from val
this._removeChars(); // Switch to first matching pattern based on val
this._updatePattern(); // Validate inputs
this._validateInpts(); // Add formatted characters
this._addChars(); // Set value and adhere to maxLength
this.el.value = this.val.substr(0, this.mLength); // Set new caret position
if (typeof ignoreCaret === 'undefined' || ignoreCaret === false) { inptSel.set(this.el, this.newPos); } }; //
// @private
// Remove all formatted before and after a specified pos
//
Formatter.prototype._removeChars = function () { // Delta shouldn't include placeholders
if (this.sel.end > this.focus) { this.delta += this.sel.end - this.focus; } // Account for shifts during removal
var shift = 0; // Loop through all possible char positions
for (var i = 0; i <= this.mLength; i++) { // Get transformed position
var curChar = this.chars[i], curHldr = this.hldrs[i], pos = i + shift, val; // If after selection we need to account for delta
pos = i >= this.sel.begin ? pos + this.delta : pos; val = this.val.charAt(pos); // Remove char and account for shift
if (curChar && curChar === val || curHldr && curHldr === val) { this.val = utils.removeChars(this.val, pos, pos + 1); shift--; } } // All hldrs should be removed now
this.hldrs = {}; // Set focus to last character
this.focus = this.val.length; }; //
// @private
// Make sure all inpts are valid, else remove and update delta
//
Formatter.prototype._validateInpts = function () { // Loop over each char and validate
for (var i = 0; i < this.val.length; i++) { // Get char inpt type
var inptType = this.inpts[i]; // Checks
var isBadType = !inptRegs[inptType], isInvalid = !isBadType && !inptRegs[inptType].test(this.val.charAt(i)), inBounds = this.inpts[i]; // Remove if incorrect and inbounds
if ((isBadType || isInvalid) && inBounds) { this.val = utils.removeChars(this.val, i, i + 1); this.focusStart--; this.newPos--; this.delta--; i--; } } }; //
// @private
// Loop over val and add formatted chars as necessary
//
Formatter.prototype._addChars = function () { if (this.opts.persistent) { // Loop over all possible characters
for (var i = 0; i <= this.mLength; i++) { if (!this.val.charAt(i)) { // Add placeholder at pos
this.val = utils.addChars(this.val, this.opts.placeholder, i); this.hldrs[i] = this.opts.placeholder; } this._addChar(i); } // Adjust focus to make sure its not on a formatted char
while (this.chars[this.focus]) { this.focus++; } } else { // Avoid caching val.length, 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; } // Place character in current position of the formatted string.
this._addChar(j); } } }; //
// @private
// Add formattted char at position
//
Formatter.prototype._addChar = function (i) { // If char exists at position
var chr = this.chars[i]; if (!chr) { return true; } // If chars are added in between the old pos and new pos
// we need to increment pos and delta
if (utils.isBetween(i, [ this.sel.begin - 1, this.newPos + 1 ])) { this.newPos++; this.delta++; } // If character added before focus, incr
if (i <= this.focus) { this.focus++; } // Updateholder
if (this.hldrs[i]) { delete this.hldrs[i]; this.hldrs[i + 1] = this.opts.placeholder; } // Update value
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 }]; }; // Expose
return Formatter; }(patternMatcher, inptSel, utils);
// A really lightweight plugin wrapper around the constructor,
// preventing against multiple instantiations
var pluginName = 'formatter';
$.fn[pluginName] = function (options) {
// Initiate plugin if options passed
if (typeof options == 'object') { this.each(function () { if (!$.data(this, 'plugin_' + pluginName)) { $.data(this, 'plugin_' + pluginName, new formatter(this, options)); } }); }
// Add resetPattern method to plugin
this.resetPattern = function (str) { this.each(function () { var formatted = $.data(this, 'plugin_' + pluginName); // resetPattern for instance
if (formatted) { formatted.resetPattern(str); } }); // Chainable please
return this; };
// Chainable please
return this; };
$.fn[pluginName].addInptType = function (chr, regexp) { formatter.addInptType(chr, regexp); };
}));
|