﻿/*
* Copyright (c) 2007 Josh Bush (digitalbush.com)
* 
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:

* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE. 
*/

/*
* Version: 1.1
* Release: 2007-09-08
*/
(function ($) {
	//Helper Functions for Caret positioning
	function getCaretPosition(ctl) {
		var res = { begin: 0, end: 0 };
		if (ctl.setSelectionRange) {
			res.begin = ctl.selectionStart;
			res.end = ctl.selectionEnd;
		} else if (document.selection && document.selection.createRange) {
			var range = document.selection.createRange();
			res.begin = 0 - range.duplicate().moveStart('character', -100000);
			res.end = res.begin + range.text.length;
		}
		return res;
	};

	function setCaretPosition(ctl, pos) {
		if (ctl.setSelectionRange) {
			ctl.focus();
			ctl.setSelectionRange(pos, pos);
		} else if (ctl.createTextRange) {
			var range = ctl.createTextRange();
			range.collapse(true);
			range.moveEnd('character', pos);
			range.moveStart('character', pos);
			range.select();
		}
	};

	//Predefined character definitions
	var charMap = {
		'9': "[0-9]",
		'a': "[A-Za-z]",
		'*': "[A-Za-z0-9]"
	};

	//Helper method to inject character definitions
	$.mask = {
		addPlaceholder: function (c, r) {
			charMap[c] = r;
		}
	};

	$.fn.unmask = function () {
		return this.trigger("unmask");
	};

	//Main Method
	$.fn.mask = function (mask, settings) {
		settings = $.extend({
			placeholder: "_",
			completed: null
		}, settings);

		//Build Regex for format validation
		var reString = "^";
		for (var i = 0; i < mask.length; i++)
			reString += (charMap[mask.charAt(i)] || ("\\" + mask.charAt(i)));
		reString += "$";
		var re = new RegExp(reString);

		return this.each(function () {
			var input = $(this);
			var buffer = new Array(mask.length);
			var locked = new Array(mask.length);

			//Build buffer layout from mask
			for (var i = 0; i < mask.length; i++) {
				locked[i] = charMap[mask.charAt(i)] == null;
				buffer[i] = locked[i] ? mask.charAt(i) : settings.placeholder;
			}

			/*Event Bindings*/
			function focusEvent() {
				checkVal();
				writeBuffer();
				setTimeout(function () {
					setCaretPosition(input[0], 0);
				}, 0);
			};
			input.bind("focus", focusEvent);

			input.bind("blur", checkVal);

			//Paste events for IE and Mozilla thanks to Kristinn Sigmundsson
			if ($.browser.msie)
				this.onpaste = function () { setTimeout(checkVal, 0); };
			else if ($.browser.mozilla)
				this.addEventListener('input', checkVal, false);

			var ignore = false;  //Variable for ignoring control keys

			function keydownEvent(e) {
				var pos = getCaretPosition(this);
				var k = e.keyCode;
				ignore = (k < 16 || (k > 16 && k < 32) || (k > 32 && k < 41));

				//delete selection before proceeding
				if ((pos.begin - pos.end) != 0 && (!ignore || k == 8 || k == 46)) {
					clearBuffer(pos.begin, pos.end);
				}
				//backspace and delete get special treatment
				if (k == 8) {//backspace					
					while (pos.begin-- >= 0) {
						if (!locked[pos.begin]) {
							buffer[pos.begin] = settings.placeholder;
							if ($.browser.opera) {
								//Opera won't let you cancel the backspace, so we'll let it backspace over a dummy character.								
								writeBuffer(pos.begin);
								setCaretPosition(this, pos.begin + 1);
							} else {
								writeBuffer();
								setCaretPosition(this, pos.begin);
							}
							return false;
						}
					}
				} else if (k == 46) {//delete
					clearBuffer(pos.begin, pos.begin + 1);
					writeBuffer();
					setCaretPosition(this, pos.begin);
					return false;
				} else if (k == 27) {
					clearBuffer(0, mask.length);
					writeBuffer();
					setCaretPosition(this, 0);
					return false;
				}

			};
			input.bind("keydown", keydownEvent);

			function keypressEvent(e) {
				if (ignore) {
					ignore = false;
					return;
				}
				e = e || window.event;
				var k = e.charCode || e.keyCode || e.which;

				var pos = getCaretPosition(this);
				var caretPos = pos.begin;

				if (e.ctrlKey || e.altKey) {//Ignore
					return true;
				} else if ((k >= 41 && k <= 122) || k == 32 || k > 186) {//typeable characters
					while (pos.begin < mask.length) {
						var reString = charMap[mask.charAt(pos.begin)];
						var match;
						if (reString) {
							var reChar = new RegExp(reString);
							match = String.fromCharCode(k).match(reChar);
						} else {//we're on a mask char, go forward and try again
							pos.begin += 1;
							pos.end = pos.begin;
							caretPos += 1;
							continue;
						}

						if (match)
							buffer[pos.begin] = String.fromCharCode(k);
						else
							return false; //reject char

						while (++caretPos < mask.length) {//seek forward to next typable position
							if (!locked[caretPos])
								break;
						}
						break;
					}
				} else
					return false;

				writeBuffer();
				if (settings.completed && caretPos >= buffer.length)
					settings.completed.call(input);
				else
					setCaretPosition(this, caretPos);

				return false;
			};
			input.bind("keypress", keypressEvent);

			/*Helper Methods*/
			function clearBuffer(start, end) {
				for (var i = start; i < end; i++) {
					if (!locked[i])
						buffer[i] = settings.placeholder;
				}
			};

			function writeBuffer(pos) {
				var s = "";
				for (var i = 0; i < mask.length; i++) {
					s += buffer[i];
					if (i == pos)
						s += settings.placeholder;
				}
				input.val(s);
				return s;
			};

			function checkVal() {
				//try to place charcters where they belong
				var test = input.val();
				var pos = 0;
				for (var i = 0; i < mask.length; i++) {
					if (!locked[i]) {
						while (pos++ < test.length) {
							//Regex Test each char here.
							var reChar = new RegExp(charMap[mask.charAt(i)]);
							if (test.charAt(pos - 1).match(reChar)) {
								buffer[i] = test.charAt(pos - 1);
								break;
							}
						}
					}
				}
				var s = writeBuffer();
				if (!s.match(re)) {
					input.val("");
					clearBuffer(0, mask.length);
				}
			};

			input.one("unmask", function () {
				input.unbind("focus", focusEvent);
				input.unbind("blur", checkVal);
				input.unbind("keydown", keydownEvent);
				input.unbind("keypress", keypressEvent);
				if ($.browser.msie)
					this.onpaste = null;
				else if ($.browser.mozilla)
					this.removeEventListener('input', checkVal, false);
			});

		});
	};
})(jQuery);
