Revision: 9342
Updated Code
at October 31, 2008 09:43 by kouphax
Updated Code
/*--------------------------------------------------------------------------------- * * InputMask jQuery Plugin * *--------------------------------------------------------------------------------- * * Taking alot of inspiration and code from * http://digitalbush.com/projects/masked-input-plugin this is a masked input * solution that should handle most cases. It uses annotations to determine the * actual mask. Mask characters include, * * % - Any digit or numeric sign * # - Any digit * @ - Any letter * * - Any letter or digit * ~ - Any sign (+/-) * ? - Currencies ($,£,€,¥) * * @author James Hughes * * ------------------------------------------------------------------------------- * 29/10/2008 - Initial Version * ------------------------------------------------------------------------------- * *///------------------------------------------------------------------------------ (function($){ /*----------------------------------------------------------------------------- * * availableMasks * *----------------------------------------------------------------------------- * * Available Character Masks. This can be extended or modified via the * $.mask.availableMasks config. * *///-------------------------------------------------------------------------- var availableMasks = { '%' : '[-+0-9]', // Any digit or numeric sign '#' : '[0-9]', // Any digit '@' : '[A-Za-z]', // Any letter '*' : '[A-Za-z0-9]', // Any letter or digit '~' : '[+-]', // Any sign (+/-) '?' : '[\$£€¥]' // Typical World Currencies } /*----------------------------------------------------------------------------- * * $.applyMasks * *----------------------------------------------------------------------------- * * Deteremines, based @Mask on annotations, all elements below either the * specified root or the document element that should have masks applied * * @plugin * * @param opts - options * *///-------------------------------------------------------------------------- $.applyMasks = function(root, opts){ $.annotated("@Mask", root || document).mask(opts); } /*----------------------------------------------------------------------------- * * $.fn.mask * *----------------------------------------------------------------------------- * * Applies the annotated masks to the passed in elements. Applicable options * * invalidHandler custom event fired when field blurs invalid * placeholder placeholder for mask characters. defaults to _ * alwaysShowMask determine if we always show the input mask * permitIncomplete determine if we allow the field to be partially * filled on blur. * selectOnFocus : true * *///-------------------------------------------------------------------------- $.fn.mask = function(opts){ /*------------------------------------------------------------------------- * * Apply Mask * *------------------------------------------------------------------------- * * This section discovers the required mask on a per field basis and * applies the behaviour to the field * *///---------------------------------------------------------------------- return this.each(function(){ /*--------------------------------------------------------------------- * * No Mask Annotation Failover * *--------------------------------------------------------------------- * * Most of this API is open to the public therefore open to the * irresponsible, ignorant, clueless and just plain stupid. We need * to cater for as much worst case edge cases as we can without * making the good people suffer. Exit if no mask defined on the * element. * *///------------------------------------------------------------------ if(!$(this).annotations("@Mask")[0]){ return undefined }; /*--------------------------------------------------------------------- * * Apply Options * *--------------------------------------------------------------------- * * Merge the default and custom options resulting in a specific * options map for this function call. * *///------------------------------------------------------------------ var o = $.extend({}, defaultOptions, opts); /*--------------------------------------------------------------------- * * Assign Buffers * *--------------------------------------------------------------------- * * Iterate over the jQuery collection of fields passed in and add * the intial buffer data to each. * *///------------------------------------------------------------------ $(this).each(function(){ $(this).data("mask-buffer", new MaskBuffer( $(this).annotations("@Mask")[0].data, o.placeholder )); }); /*--------------------------------------------------------------------- * * Handle Blur * *--------------------------------------------------------------------- * * When a masked field blurs we need to handle overall validation and * based on the confguration options hide and show the mask. If the * field is invalid the event will fire the custom handler. * *///------------------------------------------------------------------ function handleBlur(){ var buffer = $(this).data("mask-buffer"); if(!o.permitIncomplete){ var v = $(this).val(); if(((!v || v === "") || !buffer.test())){ buffer.reset(); $(this).val( o.alwaysShowMask ? buffer.get() : "" ); o.invalidHandler(this, v); } } } /*--------------------------------------------------------------------- * * Handle Key Press * *--------------------------------------------------------------------- * * In the keypress event we are responsible for handling the standard * keys and managing the buffer * *///------------------------------------------------------------------ function handleKeyPress(e){ var code = ($.browser.msie) ? e.which : e.charCode; if(code != 0 && !(e.ctrlKey || e.altKey)){ var buffer = $(this).data("mask-buffer"); var carat = $.carat.get(this); var ncp = buffer.nextMaskPosition(carat.start-1); if(ncp < buffer.size()){ var characterTest = new RegExp(availableMasks[buffer.getMaskValue(ncp)]); var value = String.fromCharCode(code); if(characterTest.test(value)){ buffer.set(ncp, value); $(this).val(buffer.get()); $.carat.set(this, new Carat(buffer.nextMaskPosition(ncp))); } } /* handle ourselves */ return false; } } /*--------------------------------------------------------------------- * * Handle Key Down * *--------------------------------------------------------------------- * * In the key down we are responsible for handling special characters * such as delete, backspace and escape. As well as clearing any * selections * *///------------------------------------------------------------------ function handleKeyDown(e){ /*----------------------------------------------------------------- * * Key Code Constants * *----------------------------------------------------------------- * * Constant representing the useful keycodes * *///-------------------------------------------------------------- var BACKSPACE = 8; var DELETE = 46; var ESCAPE = 27; var carat = $.carat.get(this); var code = e.keyCode; var buffer = $(this).data("mask-buffer"); if(carat.isSelection() && (code == BACKSPACE || code == DELETE)){ buffer.reset(carat.start, carat.end); } switch(code){ case BACKSPACE: while(carat.start-- >= 0){ if(availableMasks[buffer.getMaskValue(carat.start)]){ buffer.reset(carat.start); if($.browser.opera){ /* Opera can't cancel backspace, prevent deletion */ $(this).val(buffer.get().substring(0, carat.start) + " " + buffer.get().substring(carat.start)); $.carat.set(this, new Carat(carat.start++)); }else{ $(this).val(buffer.get()); $.carat.set(this, new Carat(Math.max(buffer.getStartingMaskPosition(), carat.start))); } return false; } } break; case DELETE: buffer.reset(carat.start); $(this).val(buffer.get()); $.carat.set(this, new Carat(Math.max(buffer.getStartingMaskPosition(), carat.start))); return false; case ESCAPE: buffer.reset(); $(this).val(buffer.get()); $.carat.set(this, new Carat(buffer.getStartingMaskPosition())); return false; } } /*--------------------------------------------------------------------- * * Handle Focus * *--------------------------------------------------------------------- * * On focus we want to set the value to the current buffer state and * determine where we set the carat * *///------------------------------------------------------------------ function handleFocus(){ $(this).val($(this).data("mask-buffer").get()); if(o.selectOnFocus){ $.carat.set(this, new Carat(0, $(this).val().length)); }else{ $.carat.set(this, new Carat(Math.max(start, this.value.indexOf(o.placeholder)))); } } /*--------------------------------------------------------------------- * * Handle Paste * *--------------------------------------------------------------------- * * Custom event used to handle onpaste events. When we paste data * into a masked field we loop over the buffer and only apply the * valid parts of the paste. * * This method is not ideal but it does the job for now. * *///------------------------------------------------------------------ function handlePaste(){ var currentValue = $(this).val().split(''); var buffer = $(this).data("mask-buffer"); for(var i = 0; i < buffer.size(); i++){ if(availableMasks[buffer.getMaskValue(i)]){ var re = new RegExp(availableMasks[buffer.getMaskValue(i)]); if(re.test(currentValue[i])){ buffer.set(i, currentValue[i]) }else{ buffer.reset(i); } } } $(this).val(buffer.get()); $.carat.set(this, new Carat((this.value.indexOf(o.placeholder) == -1)?buffer.size():this.value.indexOf(o.placeholder))); handleBlur.call(this); } /*--------------------------------------------------------------------- * * Bind Events * *--------------------------------------------------------------------- * * Bind the mask events to the current field. Include the custom * paste event. * *///------------------------------------------------------------------ $(this).bind('blur', handleBlur); $(this).bind('keypress', handleKeyPress); $(this).bind('focus', handleFocus); $(this).bind('keydown', handleKeyDown); if ($.browser.msie){ this.onpaste = function(){ setTimeout(handlePaste,0); }; }else if ($.browser.mozilla){ this.addEventListener('input', handlePaste, false); } /*--------------------------------------------------------------------- * * Unmask Event * *--------------------------------------------------------------------- * * Add the unmask event to be fired only once. This will remove all * the mask associated data and event listeners from the field * *///------------------------------------------------------------------ $(this).one('unmask', function(){ /* Unbond Events */ $(this).unbind('blur', handleBlur); $(this).unbind('keypress', handleKeyPress); $(this).unbind('focus', handleFocus); $(this).unbind('keydown', handleKeyDown); /* Remove Buffer Data */ $(this).removeData("mask-buffer"); /* Remove Custom Paste Event */ if ($.browser.msie){ this.onpaste = null; }else if ($.browser.mozilla){ this.removeEventListener('input',handlePaste,false); } }); /*--------------------------------------------------------------------- * * Initialize Field * *--------------------------------------------------------------------- * * Call the handlePaste event to intialize the field. This will * handle any existing text that is there and set the inital value of * the field. * *///------------------------------------------------------------------ handlePaste.call(this); }); } /*----------------------------------------------------------------------------- * * $.fn.unmask * *----------------------------------------------------------------------------- * * Remove any masks from the passed in fields. * *///-------------------------------------------------------------------------- $.fn.unmask = function(){ return this.trigger('unmask'); } /*----------------------------------------------------------------------------- * * Default Options * *----------------------------------------------------------------------------- * * This map represents the global default options that should be used * when applying constraints. They can be overridden via custom maps * passed into the functions. * *///-------------------------------------------------------------------------- var defaultOptions = { invalidHandler : function(el){}, placeholder : "_", alwaysShowMask : true, permitIncomplete : false, selectOnFocus : true }; /*----------------------------------------------------------------------------- * * Carat Object * *----------------------------------------------------------------------------- * * The Carat object encapsulates carat functionality such as setting and * getting selections and postions. * * @constructor * *///-------------------------------------------------------------------------- var Carat = function(s,e){ this.start = s || 0; this.end = e || s || 0; } Carat.prototype = { /*------------------------------------------------------------------------- * * isSelection * *------------------------------------------------------------------------- * * Determines if the current carat position is a selection or not * *///---------------------------------------------------------------------- isSelection:function(){ return this.start < this.end; }, start : this.start, end : this.end } /*----------------------------------------------------------------------------- * * getCaratPosition * *----------------------------------------------------------------------------- * * Based on the passed in input field this function will return the current * carat position as an object with a start and end property. This allows * for both single carat positions and whole selection ranges * * @param el element to extract carat position from * *///-------------------------------------------------------------------------- Carat.getCaratPosition = function(el){ if (el.setSelectionRange){ return new Carat(el.selectionStart, el.selectionEnd); }else if (document.selection && document.selection.createRange){ var range = document.selection.createRange(); var start = 0 - range.duplicate().moveStart('character', -100000); return new Carat(start, start + range.text.length); } } /*----------------------------------------------------------------------------- * * Set Carat Position * *----------------------------------------------------------------------------- * * Sets the postion of the carat on the passed in element. Can be a single * postion of a selection. * * @param el element to set carat position omn * @param from start position of carat * @param to end position of carat (optional) * *///-------------------------------------------------------------------------- Carat.setCaratPosition = function(el, c){ if(el.setSelectionRange){ el.focus(); el.setSelectionRange(c.start,c.end); }else if (el.createTextRange){ var range = el.createTextRange(); range.collapse(true); range.moveEnd('character', c.end); range.moveStart('character', c.start); range.select(); } } /*----------------------------------------------------------------------------- * * Mask Buffer Class * *----------------------------------------------------------------------------- * * The Mask Buffer Class houses all the internal buffer mechanisms asnd * provides a cleaner interface for working with buffers. * *///-------------------------------------------------------------------------- var MaskBuffer = function(m, p){ /*------------------------------------------------------------------------- * * isFixedCharacter * *------------------------------------------------------------------------- * * Determines if the postion within the passed in mask is a fixed * character or if it is a mask character * * @param m - the mask * @param postion - position to check * *///---------------------------------------------------------------------- this.isFixedCharacter = function(m, position){ return !availableMasks[m.charAt(position)]; } /*------------------------------------------------------------------------- * * Generate RegExp * *------------------------------------------------------------------------- * * Build up a complete mask regualr expression to validate the enitre * input upon blur and paste events. * *///---------------------------------------------------------------------- var parsedMask = $.map(m.split(""), function(it){ return availableMasks[it] || (/[A-Za-z0-9]/.test(it)?"":"\\") + it; }); this.fullRegEx = new RegExp("^" + parsedMask.join("") + "$"); /*------------------------------------------------------------------------- * * Initialize Object * *------------------------------------------------------------------------- * * Build the initial buffer, find the first mask character position and * store the values internall in this object * *///---------------------------------------------------------------------- this.start = m.length; this.buffer = new Array(m.length); for(var i = m.length-1; i >= 0; i--){ if(!this.isFixedCharacter(m,i)){ this.start = i; this.buffer[i] = p; }else{ this.buffer[i] = m.charAt(i); } } this.initial = $.map(this.buffer, function(e){return e}), this.mask = m; } /* Extend the object */ MaskBuffer.prototype = { /*------------------------------------------------------------------------- * * getInitialMask * *------------------------------------------------------------------------- * * Returns a copy of the initial mask array that was used to create * this buffer instance. A copy is returned to to prevent any pass by * reference overwritting. * *///---------------------------------------------------------------------- getInitialMask: function(){ return $.map(this.initial, function(e){return e}); // clone }, /*------------------------------------------------------------------------- * * getStartingMaskPosition * *------------------------------------------------------------------------- * * Returns the index of the first mask (i.e. non fixed) character in the * mask * *///---------------------------------------------------------------------- getStartingMaskPosition: function(){ return this.start; }, /*------------------------------------------------------------------------- * * nextMaskPostion * *------------------------------------------------------------------------- * * from the passed in index returns the postion of the next non fixed * mask character. * *///---------------------------------------------------------------------- nextMaskPosition : function(i){ var target = i||0; while(++target < this.mask.length){ if(!this.isFixedCharacter(this.mask,target)){ return target; } } return this.mask.length; }, /*------------------------------------------------------------------------- * * get * *------------------------------------------------------------------------- * * Returns, depending on the arguments passed, either the string value of * the current buffer state of the character at the passed index of the * buffer * *///---------------------------------------------------------------------- get: function(){ return (arguments.length === 0)?this.buffer.join(''):this.buffer[arguments[0]]; }, /*------------------------------------------------------------------------- * * getMaskValue * *------------------------------------------------------------------------- * * Returns the character of the mask at the current position * *///---------------------------------------------------------------------- getMaskValue: function(idx){ return this.mask.charAt(idx); }, /*------------------------------------------------------------------------- * * size * *------------------------------------------------------------------------- * * Returns the length of the buffer/mask/initial mask etc * *///---------------------------------------------------------------------- size: function(){ return this.buffer.length; }, /*------------------------------------------------------------------------- * * set * *------------------------------------------------------------------------- * * Sets either the enitre buffer or a specific character of the buffer. * * @param i Array|Number. The array to set the buffer to or the postion * of the character to set the value of * @param v Boolean|Character. If boolean then it makes determines if a * clone of the array is to be used. If character it is the * character to put in the current position of the buffer; * *///---------------------------------------------------------------------- set: function(i,v){ if(i.constructor === Array){ this.buffer = (v)?$.map(i, function(e){return e}):i; }else{ this.buffer[i] = v; } }, /*------------------------------------------------------------------------- * * reset * *------------------------------------------------------------------------- * * Resets, returns back to the inital mask, all/some/one part of the * current buffer depending on inputs * *///---------------------------------------------------------------------- reset: function(){ var start, end; switch(arguments.length){ case 0 : start = 0; end = this.buffer.length; break; case 1 : start = end = arguments[0]; break; case 2 : start = arguments[0]; end = arguments[1]; break; } for(var i = start; i <= end; i++){ this.buffer[i] = this.initial[i]; } }, /*------------------------------------------------------------------------- * * test * *------------------------------------------------------------------------- * * Test current buffer state against the complete RegExp of the field to * determine if it is invliad/incomplete * *///---------------------------------------------------------------------- test: function(){ return this.fullRegEx.test(this.buffer.join('')); } } /*----------------------------------------------------------------------------- * * Public API * *----------------------------------------------------------------------------- * * Extend the core jQuery object to allow access to the public functions * for the plugin functionality * *///-------------------------------------------------------------------------- $.extend({ /* $.fn.mask * $.fn.unmask * $.applyMasks */ mask : { //-- CONFIGURATION ACCESSORS ------------------------------------------ availableMasks : availableMasks, options : defaultOptions, /*--------------------------------------------------------------------- * * Unistall plugin * *--------------------------------------------------------------------- * * Unistall the entire plugin by deregistering all events and data * caches in the document but also delete the objects from memory. * *///------------------------------------------------------------------ uninstall: function(){ /* UNMASK */ $.annotated("@Mask", document).unmask(); /* GARBAGE COLLECT */ delete(availableMasks); delete($.applyMasks); delete($.fn.mask); delete($.fn.unmask); delete(defaultOptions); delete(Carat); delete(MaskBuffer); delete($.carat); delete($.mask); } }, carat : { get : Carat.getCaratPosition, set : Carat.setCaratPosition } }); })(jQuery);
Revision: 9341
Updated Code
at October 31, 2008 09:41 by kouphax
Updated Code
/*--------------------------------------------------------------------------------- * * InputMask jQuery Plugin * *--------------------------------------------------------------------------------- * * Taking alot of inspiration and code from * http://digitalbush.com/projects/masked-input-plugin this is a masked input * solution that should handle most cases. It uses annotations to determine the * actual mask. Mask characters include, * * % - Any digit or numeric sign * # - Any digit * @ - Any letter * * - Any letter or digit * ~ - Any sign (+/-) * ? - Currencies ($,£,� or ¥) * * @author James Hughes * * ------------------------------------------------------------------------------- * 29/10/2008 - Initial Version * ------------------------------------------------------------------------------- * *///------------------------------------------------------------------------------ (function($){ /*----------------------------------------------------------------------------- * * availableMasks * *----------------------------------------------------------------------------- * * Available Character Masks. This can be extended or modified via the * $.mask.availableMasks config. * *///-------------------------------------------------------------------------- var availableMasks = { '%' : '[-+0-9]', // Any digit or numeric sign '#' : '[0-9]', // Any digit '@' : '[A-Za-z]', // Any letter '*' : '[A-Za-z0-9]', // Any letter or digit '~' : '[+-]', // Any sign (+/-) '?' : '[\$£�¥]' // Typical World Currencies } /*----------------------------------------------------------------------------- * * $.applyMasks * *----------------------------------------------------------------------------- * * Deteremines, based @Mask on annotations, all elements below either the * specified root or the document element that should have masks applied * * @plugin * * @param opts - options * *///-------------------------------------------------------------------------- $.applyMasks = function(root, opts){ $.annotated("@Mask", root || document).mask(opts); } /*----------------------------------------------------------------------------- * * $.fn.mask * *----------------------------------------------------------------------------- * * Applies the annotated masks to the passed in elements. Applicable options * * invalidHandler custom event fired when field blurs invalid * placeholder placeholder for mask characters. defaults to _ * alwaysShowMask determine if we always show the input mask * permitIncomplete determine if we allow the field to be partially * filled on blur. * selectOnFocus : true * *///-------------------------------------------------------------------------- $.fn.mask = function(opts){ /*------------------------------------------------------------------------- * * Apply Mask * *------------------------------------------------------------------------- * * This section discovers the required mask on a per field basis and * applies the behaviour to the field * *///---------------------------------------------------------------------- return this.each(function(){ /*--------------------------------------------------------------------- * * No Mask Annotation Failover * *--------------------------------------------------------------------- * * Most of this API is open to the public therefore open to the * irresponsible, ignorant, clueless and just plain stupid. We need * to cater for as much worst case edge cases as we can without * making the good people suffer. Exit if no mask defined on the * element. * *///------------------------------------------------------------------ if(!$(this).annotations("@Mask")[0]){ return undefined }; /*--------------------------------------------------------------------- * * Apply Options * *--------------------------------------------------------------------- * * Merge the default and custom options resulting in a specific * options map for this function call. * *///------------------------------------------------------------------ var o = $.extend({}, defaultOptions, opts); /*--------------------------------------------------------------------- * * Assign Buffers * *--------------------------------------------------------------------- * * Iterate over the jQuery collection of fields passed in and add * the intial buffer data to each. * *///------------------------------------------------------------------ $(this).each(function(){ $(this).data("mask-buffer", new MaskBuffer( $(this).annotations("@Mask")[0].data, o.placeholder )); }); /*--------------------------------------------------------------------- * * Handle Blur * *--------------------------------------------------------------------- * * When a masked field blurs we need to handle overall validation and * based on the confguration options hide and show the mask. If the * field is invalid the event will fire the custom handler. * *///------------------------------------------------------------------ function handleBlur(){ var buffer = $(this).data("mask-buffer"); if(!o.permitIncomplete){ var v = $(this).val(); if(((!v || v === "") || !buffer.test())){ buffer.reset(); $(this).val( o.alwaysShowMask ? buffer.get() : "" ); o.invalidHandler(this, v); } } } /*--------------------------------------------------------------------- * * Handle Key Press * *--------------------------------------------------------------------- * * In the keypress event we are responsible for handling the standard * keys and managing the buffer * *///------------------------------------------------------------------ function handleKeyPress(e){ var code = ($.browser.msie) ? e.which : e.charCode; if(code != 0 && !(e.ctrlKey || e.altKey)){ var buffer = $(this).data("mask-buffer"); var carat = $.carat.get(this); var ncp = buffer.nextMaskPosition(carat.start-1); if(ncp < buffer.size()){ var characterTest = new RegExp(availableMasks[buffer.getMaskValue(ncp)]); var value = String.fromCharCode(code); if(characterTest.test(value)){ buffer.set(ncp, value); $(this).val(buffer.get()); $.carat.set(this, new Carat(buffer.nextMaskPosition(ncp))); } } /* handle ourselves */ return false; } } /*--------------------------------------------------------------------- * * Handle Key Down * *--------------------------------------------------------------------- * * In the key down we are responsible for handling special characters * such as delete, backspace and escape. As well as clearing any * selections * *///------------------------------------------------------------------ function handleKeyDown(e){ /*----------------------------------------------------------------- * * Key Code Constants * *----------------------------------------------------------------- * * Constant representing the useful keycodes * *///-------------------------------------------------------------- var BACKSPACE = 8; var DELETE = 46; var ESCAPE = 27; var carat = $.carat.get(this); var code = e.keyCode; var buffer = $(this).data("mask-buffer"); if(carat.isSelection() && (code == BACKSPACE || code == DELETE)){ buffer.reset(carat.start, carat.end); } switch(code){ case BACKSPACE: while(carat.start-- >= 0){ if(availableMasks[buffer.getMaskValue(carat.start)]){ buffer.reset(carat.start); if($.browser.opera){ /* Opera can't cancel backspace, prevent deletion */ $(this).val(buffer.get().substring(0, carat.start) + " " + buffer.get().substring(carat.start)); $.carat.set(this, new Carat(carat.start++)); }else{ $(this).val(buffer.get()); $.carat.set(this, new Carat(Math.max(buffer.getStartingMaskPosition(), carat.start))); } return false; } } break; case DELETE: buffer.reset(carat.start); $(this).val(buffer.get()); $.carat.set(this, new Carat(Math.max(buffer.getStartingMaskPosition(), carat.start))); return false; case ESCAPE: buffer.reset(); $(this).val(buffer.get()); $.carat.set(this, new Carat(buffer.getStartingMaskPosition())); return false; } } /*--------------------------------------------------------------------- * * Handle Focus * *--------------------------------------------------------------------- * * On focus we want to set the value to the current buffer state and * determine where we set the carat * *///------------------------------------------------------------------ function handleFocus(){ $(this).val($(this).data("mask-buffer").get()); if(o.selectOnFocus){ $.carat.set(this, new Carat(0, $(this).val().length)); }else{ $.carat.set(this, new Carat(Math.max(start, this.value.indexOf(o.placeholder)))); } } /*--------------------------------------------------------------------- * * Handle Paste * *--------------------------------------------------------------------- * * Custom event used to handle onpaste events. When we paste data * into a masked field we loop over the buffer and only apply the * valid parts of the paste. * * This method is not ideal but it does the job for now. * *///------------------------------------------------------------------ function handlePaste(){ var currentValue = $(this).val().split(''); var buffer = $(this).data("mask-buffer"); for(var i = 0; i < buffer.size(); i++){ if(availableMasks[buffer.getMaskValue(i)]){ var re = new RegExp(availableMasks[buffer.getMaskValue(i)]); if(re.test(currentValue[i])){ buffer.set(i, currentValue[i]) }else{ buffer.reset(i); } } } $(this).val(buffer.get()); $.carat.set(this, new Carat((this.value.indexOf(o.placeholder) == -1)?buffer.size():this.value.indexOf(o.placeholder))); handleBlur.call(this); } /*--------------------------------------------------------------------- * * Bind Events * *--------------------------------------------------------------------- * * Bind the mask events to the current field. Include the custom * paste event. * *///------------------------------------------------------------------ $(this).bind('blur', handleBlur); $(this).bind('keypress', handleKeyPress); $(this).bind('focus', handleFocus); $(this).bind('keydown', handleKeyDown); if ($.browser.msie){ this.onpaste = function(){ setTimeout(handlePaste,0); }; }else if ($.browser.mozilla){ this.addEventListener('input', handlePaste, false); } /*--------------------------------------------------------------------- * * Unmask Event * *--------------------------------------------------------------------- * * Add the unmask event to be fired only once. This will remove all * the mask associated data and event listeners from the field * *///------------------------------------------------------------------ $(this).one('unmask', function(){ /* Unbond Events */ $(this).unbind('blur', handleBlur); $(this).unbind('keypress', handleKeyPress); $(this).unbind('focus', handleFocus); $(this).unbind('keydown', handleKeyDown); /* Remove Buffer Data */ $(this).removeData("mask-buffer"); /* Remove Custom Paste Event */ if ($.browser.msie){ this.onpaste = null; }else if ($.browser.mozilla){ this.removeEventListener('input',handlePaste,false); } }); /*--------------------------------------------------------------------- * * Initialize Field * *--------------------------------------------------------------------- * * Call the handlePaste event to intialize the field. This will * handle any existing text that is there and set the inital value of * the field. * *///------------------------------------------------------------------ handlePaste.call(this); }); } /*----------------------------------------------------------------------------- * * $.fn.unmask * *----------------------------------------------------------------------------- * * Remove any masks from the passed in fields. * *///-------------------------------------------------------------------------- $.fn.unmask = function(){ return this.trigger('unmask'); } /*----------------------------------------------------------------------------- * * Default Options * *----------------------------------------------------------------------------- * * This map represents the global default options that should be used * when applying constraints. They can be overridden via custom maps * passed into the functions. * *///-------------------------------------------------------------------------- var defaultOptions = { invalidHandler : function(el){}, placeholder : "_", alwaysShowMask : true, permitIncomplete : false, selectOnFocus : true }; /*----------------------------------------------------------------------------- * * Carat Object * *----------------------------------------------------------------------------- * * The Carat object encapsulates carat functionality such as setting and * getting selections and postions. * * @constructor * *///-------------------------------------------------------------------------- var Carat = function(s,e){ this.start = s || 0; this.end = e || s || 0; } Carat.prototype = { /*------------------------------------------------------------------------- * * isSelection * *------------------------------------------------------------------------- * * Determines if the current carat position is a selection or not * *///---------------------------------------------------------------------- isSelection:function(){ return this.start < this.end; }, start : this.start, end : this.end } /*----------------------------------------------------------------------------- * * getCaratPosition * *----------------------------------------------------------------------------- * * Based on the passed in input field this function will return the current * carat position as an object with a start and end property. This allows * for both single carat positions and whole selection ranges * * @param el element to extract carat position from * *///-------------------------------------------------------------------------- Carat.getCaratPosition = function(el){ if (el.setSelectionRange){ return new Carat(el.selectionStart, el.selectionEnd); }else if (document.selection && document.selection.createRange){ var range = document.selection.createRange(); var start = 0 - range.duplicate().moveStart('character', -100000); return new Carat(start, start + range.text.length); } } /*----------------------------------------------------------------------------- * * Set Carat Position * *----------------------------------------------------------------------------- * * Sets the postion of the carat on the passed in element. Can be a single * postion of a selection. * * @param el element to set carat position omn * @param from start position of carat * @param to end position of carat (optional) * *///-------------------------------------------------------------------------- Carat.setCaratPosition = function(el, c){ if(el.setSelectionRange){ el.focus(); el.setSelectionRange(c.start,c.end); }else if (el.createTextRange){ var range = el.createTextRange(); range.collapse(true); range.moveEnd('character', c.end); range.moveStart('character', c.start); range.select(); } } /*----------------------------------------------------------------------------- * * Mask Buffer Class * *----------------------------------------------------------------------------- * * The Mask Buffer Class houses all the internal buffer mechanisms asnd * provides a cleaner interface for working with buffers. * *///-------------------------------------------------------------------------- var MaskBuffer = function(m, p){ /*------------------------------------------------------------------------- * * isFixedCharacter * *------------------------------------------------------------------------- * * Determines if the postion within the passed in mask is a fixed * character or if it is a mask character * * @param m - the mask * @param postion - position to check * *///---------------------------------------------------------------------- this.isFixedCharacter = function(m, position){ return !availableMasks[m.charAt(position)]; } /*------------------------------------------------------------------------- * * Generate RegExp * *------------------------------------------------------------------------- * * Build up a complete mask regualr expression to validate the enitre * input upon blur and paste events. * *///---------------------------------------------------------------------- var parsedMask = $.map(m.split(""), function(it){ return availableMasks[it] || (/[A-Za-z0-9]/.test(it)?"":"\\") + it; }); this.fullRegEx = new RegExp("^" + parsedMask.join("") + "$"); /*------------------------------------------------------------------------- * * Initialize Object * *------------------------------------------------------------------------- * * Build the initial buffer, find the first mask character position and * store the values internall in this object * *///---------------------------------------------------------------------- this.start = m.length; this.buffer = new Array(m.length); for(var i = m.length-1; i >= 0; i--){ if(!this.isFixedCharacter(m,i)){ this.start = i; this.buffer[i] = p; }else{ this.buffer[i] = m.charAt(i); } } this.initial = $.map(this.buffer, function(e){return e}), this.mask = m; } /* Extend the object */ MaskBuffer.prototype = { /*------------------------------------------------------------------------- * * getInitialMask * *------------------------------------------------------------------------- * * Returns a copy of the initial mask array that was used to create * this buffer instance. A copy is returned to to prevent any pass by * reference overwritting. * *///---------------------------------------------------------------------- getInitialMask: function(){ return $.map(this.initial, function(e){return e}); // clone }, /*------------------------------------------------------------------------- * * getStartingMaskPosition * *------------------------------------------------------------------------- * * Returns the index of the first mask (i.e. non fixed) character in the * mask * *///---------------------------------------------------------------------- getStartingMaskPosition: function(){ return this.start; }, /*------------------------------------------------------------------------- * * nextMaskPostion * *------------------------------------------------------------------------- * * from the passed in index returns the postion of the next non fixed * mask character. * *///---------------------------------------------------------------------- nextMaskPosition : function(i){ var target = i||0; while(++target < this.mask.length){ if(!this.isFixedCharacter(this.mask,target)){ return target; } } return this.mask.length; }, /*------------------------------------------------------------------------- * * get * *------------------------------------------------------------------------- * * Returns, depending on the arguments passed, either the string value of * the current buffer state of the character at the passed index of the * buffer * *///---------------------------------------------------------------------- get: function(){ return (arguments.length === 0)?this.buffer.join(''):this.buffer[arguments[0]]; }, /*------------------------------------------------------------------------- * * getMaskValue * *------------------------------------------------------------------------- * * Returns the character of the mask at the current position * *///---------------------------------------------------------------------- getMaskValue: function(idx){ return this.mask.charAt(idx); }, /*------------------------------------------------------------------------- * * size * *------------------------------------------------------------------------- * * Returns the length of the buffer/mask/initial mask etc * *///---------------------------------------------------------------------- size: function(){ return this.buffer.length; }, /*------------------------------------------------------------------------- * * set * *------------------------------------------------------------------------- * * Sets either the enitre buffer or a specific character of the buffer. * * @param i Array|Number. The array to set the buffer to or the postion * of the character to set the value of * @param v Boolean|Character. If boolean then it makes determines if a * clone of the array is to be used. If character it is the * character to put in the current position of the buffer; * *///---------------------------------------------------------------------- set: function(i,v){ if(i.constructor === Array){ this.buffer = (v)?$.map(i, function(e){return e}):i; }else{ this.buffer[i] = v; } }, /*------------------------------------------------------------------------- * * reset * *------------------------------------------------------------------------- * * Resets, returns back to the inital mask, all/some/one part of the * current buffer depending on inputs * *///---------------------------------------------------------------------- reset: function(){ var start, end; switch(arguments.length){ case 0 : start = 0; end = this.buffer.length; break; case 1 : start = end = arguments[0]; break; case 2 : start = arguments[0]; end = arguments[1]; break; } for(var i = start; i <= end; i++){ this.buffer[i] = this.initial[i]; } }, /*------------------------------------------------------------------------- * * test * *------------------------------------------------------------------------- * * Test current buffer state against the complete RegExp of the field to * determine if it is invliad/incomplete * *///---------------------------------------------------------------------- test: function(){ return this.fullRegEx.test(this.buffer.join('')); } } /*----------------------------------------------------------------------------- * * Public API * *----------------------------------------------------------------------------- * * Extend the core jQuery object to allow access to the public functions * for the plugin functionality * *///-------------------------------------------------------------------------- $.extend({ /* $.fn.mask * $.fn.unmask * $.applyMasks */ mask : { //-- CONFIGURATION ACCESSORS ------------------------------------------ availableMasks : availableMasks, options : defaultOptions, /*--------------------------------------------------------------------- * * Unistall plugin * *--------------------------------------------------------------------- * * Unistall the entire plugin by deregistering all events and data * caches in the document but also delete the objects from memory. * *///------------------------------------------------------------------ uninstall: function(){ /* UNMASK */ $.annotated("@Mask", document).unmask(); /* GARBAGE COLLECT */ delete(availableMasks); delete($.applyMasks); delete($.fn.mask); delete($.fn.unmask); delete(defaultOptions); delete(Carat); delete(MaskBuffer); delete($.carat); delete($.mask); } }, carat : { get : Carat.getCaratPosition, set : Carat.setCaratPosition } }); })(jQuery);
Revision: 9340
Updated Code
at October 31, 2008 08:56 by kouphax
Updated Code
/*--------------------------------------------------------------------------------- * * InputMask jQuery Plugin * *--------------------------------------------------------------------------------- * * Taking alot of inspiration and code from * http://digitalbush.com/projects/masked-input-plugin this is a masked input * solution that should handle most cases. It uses annotations to determine the * actual mask. Mask characters include, * * % - Any digit or numeric sign * # - Any digit * @ - Any letter * * - Any letter or digit * ~ - Any sign (+/-) * ? - Currencies ($,£,€ or ¥) * * @author James Hughes * * ------------------------------------------------------------------------------- * 29/10/2008 - Initial Version * ------------------------------------------------------------------------------- * *///------------------------------------------------------------------------------ (function($){ /*----------------------------------------------------------------------------- * * availableMasks * *----------------------------------------------------------------------------- * * Available Character Masks. This can be extended or modified via the * $.mask.availableMasks config. * *///-------------------------------------------------------------------------- var availableMasks = { '%' : '[-+0-9]', // Any digit or numeric sign '#' : '[0-9]', // Any digit '@' : '[A-Za-z]', // Any letter '*' : '[A-Za-z0-9]', // Any letter or digit '~' : '[+-]', // Any sign (+/-) '?' : '[\$£€¥]' // Typical World Currencies } /*----------------------------------------------------------------------------- * * $.applyMasks * *----------------------------------------------------------------------------- * * Deteremines, based @Mask on annotations, all elements below either the * specified root or the document element that should have masks applied * * @plugin * * @param opts - options * *///-------------------------------------------------------------------------- $.applyMasks = function(root, opts){ $.annotated("@Mask", root || document).mask(opts); } /*----------------------------------------------------------------------------- * * $.fn.mask * *----------------------------------------------------------------------------- * * Applies the annotated masks to the passed in elements. Applicable options * * invalidHandler custom event fired when field blurs invalid * placeholder placeholder for mask characters. defaults to _ * alwaysShowMask determine if we always show the input mask * permitIncomplete determine if we allow the field to be partially * filled on blur. * selectOnFocus : true * *///-------------------------------------------------------------------------- $.fn.mask = function(opts){ /*------------------------------------------------------------------------- * * Apply Mask * *------------------------------------------------------------------------- * * This section discovers the required mask on a per field basis and * applies the behaviour to the field * *///---------------------------------------------------------------------- return this.each(function(){ /*--------------------------------------------------------------------- * * No Mask Annotation Failover * *--------------------------------------------------------------------- * * Most of this API is open to the public therefore open to the * irresponsible, ignorant, clueless and just plain stupid. We need * to cater for as much worst case edge cases as we can without * making the good people suffer. Exit if no mask defined on the * element. * *///------------------------------------------------------------------ if(!$(this).annotations("@Mask")[0]){ return undefined }; /*--------------------------------------------------------------------- * * Apply Options * *--------------------------------------------------------------------- * * Merge the default and custom options resulting in a specific * options map for this function call. * *///------------------------------------------------------------------ var o = $.extend({}, defaultOptions, opts); /*--------------------------------------------------------------------- * * Assign Buffers * *--------------------------------------------------------------------- * * Iterate over the jQuery collection of fields passed in and add * the intial buffer data to each. * *///------------------------------------------------------------------ $(this).each(function(){ $(this).data("mask-buffer", new MaskBuffer( $(this).annotations("@Mask")[0].data, o.placeholder )); }); /*--------------------------------------------------------------------- * * Handle Blur * *--------------------------------------------------------------------- * * When a masked field blurs we need to handle overall validation and * based on the confguration options hide and show the mask. If the * field is invalid the event will fire the custom handler. * *///------------------------------------------------------------------ function handleBlur(){ var buffer = $(this).data("mask-buffer"); if(!o.permitIncomplete){ var v = $(this).val(); if(((!v || v === "") || !buffer.test())){ buffer.reset(); $(this).val( o.alwaysShowMask ? buffer.get() : "" ); o.invalidHandler(this, v); } } } /*--------------------------------------------------------------------- * * Handle Key Press * *--------------------------------------------------------------------- * * In the keypress event we are responsible for handling the standard * keys and managing the buffer * *///------------------------------------------------------------------ function handleKeyPress(e){ var code = ($.browser.msie) ? e.which : e.charCode; if(code != 0 && !(e.ctrlKey || e.altKey)){ var buffer = $(this).data("mask-buffer"); var carat = $.carat.get(this); var ncp = buffer.nextMaskPosition(carat.start-1); if(ncp < buffer.size()){ var characterTest = new RegExp(availableMasks[buffer.getMaskValue(ncp)]); var value = String.fromCharCode(code); if(characterTest.test(value)){ buffer.set(ncp, value); $(this).val(buffer.get()); $.carat.set(this, new Carat(buffer.nextMaskPosition(ncp))); } } /* handle ourselves */ return false; } } /*--------------------------------------------------------------------- * * Handle Key Down * *--------------------------------------------------------------------- * * In the key down we are responsible for handling special characters * such as delete, backspace and escape. As well as clearing any * selections * *///------------------------------------------------------------------ function handleKeyDown(e){ /*----------------------------------------------------------------- * * Key Code Constants * *----------------------------------------------------------------- * * Constant representing the useful keycodes * *///-------------------------------------------------------------- var BACKSPACE = 8; var DELETE = 46; var ESCAPE = 27; var carat = $.carat.get(this); var code = e.keyCode; var buffer = $(this).data("mask-buffer"); if(carat.isSelection() && (code == BACKSPACE || code == DELETE)){ buffer.reset(carat.start, carat.end); } switch(code){ case BACKSPACE: while(carat.start-- >= 0){ if(availableMasks[buffer.getMaskValue(carat.start)]){ buffer.reset(carat.start); if($.browser.opera){ /* Opera can't cancel backspace, prevent deletion */ $(this).val(buffer.get().substring(0, carat.start) + " " + buffer.get().substring(carat.start)); $.carat.set(this, new Carat(carat.start++)); }else{ $(this).val(buffer.get()); $.carat.set(this, new Carat(Math.max(buffer.getStartingMaskPosition(), carat.start))); } return false; } } break; case DELETE: buffer.reset(carat.start); $(this).val(buffer.get()); $.carat.set(this, new Carat(Math.max(buffer.getStartingMaskPosition(), carat.start))); return false; case ESCAPE: buffer.reset(); $(this).val(buffer.get()); $.carat.set(this, new Carat(buffer.getStartingMaskPosition())); return false; } } /*--------------------------------------------------------------------- * * Handle Focus * *--------------------------------------------------------------------- * * On focus we want to set the value to the current buffer state and * determine where we set the carat * *///------------------------------------------------------------------ function handleFocus(){ $(this).val($(this).data("mask-buffer").get()); if(o.selectOnFocus){ $.carat.set(this, new Carat(0, $(this).val().length)); }else{ $.carat.set(this, new Carat(Math.max(start, this.value.indexOf(o.placeholder)))); } } /*--------------------------------------------------------------------- * * Handle Paste * *--------------------------------------------------------------------- * * Custom event used to handle onpaste events. When we paste data * into a masked field we loop over the buffer and only apply the * valid parts of the paste. * * This method is not ideal but it does the job for now. * *///------------------------------------------------------------------ function handlePaste(){ var currentValue = $(this).val().split(''); var buffer = $(this).data("mask-buffer"); for(var i = 0; i < buffer.size(); i++){ if(availableMasks[buffer.getMaskValue(i)]){ var re = new RegExp(availableMasks[buffer.getMaskValue(i)]); if(re.test(currentValue[i])){ buffer.set(i, currentValue[i]) }else{ buffer.reset(i); } } } $(this).val(buffer.get()); $.carat.set(this, new Carat((this.value.indexOf(o.placeholder) == -1)?buffer.size():this.value.indexOf(o.placeholder))); handleBlur.call(this); } /*--------------------------------------------------------------------- * * Bind Events * *--------------------------------------------------------------------- * * Bind the mask events to the current field. Include the custom * paste event. * *///------------------------------------------------------------------ $(this).bind('blur', handleBlur); $(this).bind('keypress', handleKeyPress); $(this).bind('focus', handleFocus); $(this).bind('keydown', handleKeyDown); if ($.browser.msie){ this.onpaste = function(){ setTimeout(handlePaste,0); }; }else if ($.browser.mozilla){ this.addEventListener('input', handlePaste, false); } /*--------------------------------------------------------------------- * * Unmask Event * *--------------------------------------------------------------------- * * Add the unmask event to be fired only once. This will remove all * the mask associated data and event listeners from the field * *///------------------------------------------------------------------ $(this).one('unmask', function(){ /* Unbond Events */ $(this).unbind('blur', handleBlur); $(this).unbind('keypress', handleKeyPress); $(this).unbind('focus', handleFocus); $(this).unbind('keydown', handleKeyDown); /* Remove Buffer Data */ $(this).removeData("mask-buffer"); /* Remove Custom Paste Event */ if ($.browser.msie){ this.onpaste = null; }else if ($.browser.mozilla){ this.removeEventListener('input',handlePaste,false); } }); /*--------------------------------------------------------------------- * * Initialize Field * *--------------------------------------------------------------------- * * Call the handlePaste event to intialize the field. This will * handle any existing text that is there and set the inital value of * the field. * *///------------------------------------------------------------------ handlePaste.call(this); }); } /*----------------------------------------------------------------------------- * * $.fn.unmask * *----------------------------------------------------------------------------- * * Remove any masks from the passed in fields. * *///-------------------------------------------------------------------------- $.fn.unmask = function(){ return this.trigger('unmask'); } /*----------------------------------------------------------------------------- * * Default Options * *----------------------------------------------------------------------------- * * This map represents the global default options that should be used * when applying constraints. They can be overridden via custom maps * passed into the functions. * *///-------------------------------------------------------------------------- var defaultOptions = { invalidHandler : function(el){}, placeholder : "_", alwaysShowMask : true, permitIncomplete : false, selectOnFocus : true }; /*----------------------------------------------------------------------------- * * Carat Object * *----------------------------------------------------------------------------- * * The Carat object encapsulates carat functionality such as setting and * getting selections and postions. * * @constructor * *///-------------------------------------------------------------------------- var Carat = function(s,e){ this.start = s || 0; this.end = e || s || 0; } Carat.prototype = { /*------------------------------------------------------------------------- * * isSelection * *------------------------------------------------------------------------- * * Determines if the current carat position is a selection or not * *///---------------------------------------------------------------------- isSelection:function(){ return this.start < this.end; }, start : this.start, end : this.end } /*----------------------------------------------------------------------------- * * getCaratPosition * *----------------------------------------------------------------------------- * * Based on the passed in input field this function will return the current * carat position as an object with a start and end property. This allows * for both single carat positions and whole selection ranges * * @param el element to extract carat position from * *///-------------------------------------------------------------------------- Carat.getCaratPosition = function(el){ if (el.setSelectionRange){ return new Carat(el.selectionStart, el.selectionEnd); }else if (document.selection && document.selection.createRange){ var range = document.selection.createRange(); var start = 0 - range.duplicate().moveStart('character', -100000); return new Carat(start, start + range.text.length); } } /*----------------------------------------------------------------------------- * * Set Carat Position * *----------------------------------------------------------------------------- * * Sets the postion of the carat on the passed in element. Can be a single * postion of a selection. * * @param el element to set carat position omn * @param from start position of carat * @param to end position of carat (optional) * *///-------------------------------------------------------------------------- Carat.setCaratPosition = function(el, c){ if(el.setSelectionRange){ el.focus(); el.setSelectionRange(c.start,c.end); }else if (el.createTextRange){ var range = el.createTextRange(); range.collapse(true); range.moveEnd('character', c.end); range.moveStart('character', c.start); range.select(); } } /*----------------------------------------------------------------------------- * * Mask Buffer Class * *----------------------------------------------------------------------------- * * The Mask Buffer Class houses all the internal buffer mechanisms asnd * provides a cleaner interface for working with buffers. * *///-------------------------------------------------------------------------- var MaskBuffer = function(m, p){ /*------------------------------------------------------------------------- * * isFixedCharacter * *------------------------------------------------------------------------- * * Determines if the postion within the passed in mask is a fixed * character or if it is a mask character * * @param m - the mask * @param postion - position to check * *///---------------------------------------------------------------------- this.isFixedCharacter = function(m, position){ return !availableMasks[m.charAt(position)]; } /*------------------------------------------------------------------------- * * Generate RegExp * *------------------------------------------------------------------------- * * Build up a complete mask regualr expression to validate the enitre * input upon blur and paste events. * *///---------------------------------------------------------------------- var parsedMask = $.map(m.split(""), function(it){ return availableMasks[it] || (/[A-Za-z0-9]/.test(it)?"":"\\") + it; }); this.fullRegEx = new RegExp("^" + parsedMask.join("") + "$"); /*------------------------------------------------------------------------- * * Initialize Object * *------------------------------------------------------------------------- * * Build the initial buffer, find the first mask character position and * store the values internall in this object * *///---------------------------------------------------------------------- this.start = m.length; this.buffer = new Array(m.length); for(var i = m.length-1; i >= 0; i--){ if(!this.isFixedCharacter(m,i)){ this.start = i; this.buffer[i] = p; }else{ this.buffer[i] = m.charAt(i); } } this.initial = $.map(this.buffer, function(e){return e}), this.mask = m; } /* Extend the object */ MaskBuffer.prototype = { /*------------------------------------------------------------------------- * * getInitialMask * *------------------------------------------------------------------------- * * Returns a copy of the initial mask array that was used to create * this buffer instance. A copy is returned to to prevent any pass by * reference overwritting. * *///---------------------------------------------------------------------- getInitialMask: function(){ return $.map(this.initial, function(e){return e}); // clone }, /*------------------------------------------------------------------------- * * getStartingMaskPosition * *------------------------------------------------------------------------- * * Returns the index of the first mask (i.e. non fixed) character in the * mask * *///---------------------------------------------------------------------- getStartingMaskPosition: function(){ return this.start; }, /*------------------------------------------------------------------------- * * nextMaskPostion * *------------------------------------------------------------------------- * * from the passed in index returns the postion of the next non fixed * mask character. * *///---------------------------------------------------------------------- nextMaskPosition : function(i){ var target = i||0; while(++target < this.mask.length){ if(!this.isFixedCharacter(this.mask,target)){ return target; } } return this.mask.length; }, /*------------------------------------------------------------------------- * * get * *------------------------------------------------------------------------- * * Returns, depending on the arguments passed, either the string value of * the current buffer state of the character at the passed index of the * buffer * *///---------------------------------------------------------------------- get: function(){ return (arguments.length === 0)?this.buffer.join(''):this.buffer[arguments[0]]; }, /*------------------------------------------------------------------------- * * getMaskValue * *------------------------------------------------------------------------- * * Returns the character of the mask at the current position * *///---------------------------------------------------------------------- getMaskValue: function(idx){ return this.mask.charAt(idx); }, /*------------------------------------------------------------------------- * * size * *------------------------------------------------------------------------- * * Returns the length of the buffer/mask/initial mask etc * *///---------------------------------------------------------------------- size: function(){ return this.buffer.length; }, /*------------------------------------------------------------------------- * * set * *------------------------------------------------------------------------- * * Sets either the enitre buffer or a specific character of the buffer. * * @param i Array|Number. The array to set the buffer to or the postion * of the character to set the value of * @param v Boolean|Character. If boolean then it makes determines if a * clone of the array is to be used. If character it is the * character to put in the current position of the buffer; * *///---------------------------------------------------------------------- set: function(i,v){ if(i.constructor === Array){ this.buffer = (v)?$.map(i, function(e){return e}):i; }else{ this.buffer[i] = v; } }, /*------------------------------------------------------------------------- * * reset * *------------------------------------------------------------------------- * * Resets, returns back to the inital mask, all/some/one part of the * current buffer depending on inputs * *///---------------------------------------------------------------------- reset: function(){ var start, end; switch(arguments.length){ case 0 : start = 0; end = this.buffer.length; break; case 1 : start = end = arguments[0]; break; case 2 : start = arguments[0]; end = arguments[1]; break; } for(var i = start; i <= end; i++){ this.buffer[i] = this.initial[i]; } }, /*------------------------------------------------------------------------- * * test * *------------------------------------------------------------------------- * * Test current buffer state against the complete RegExp of the field to * determine if it is invliad/incomplete * *///---------------------------------------------------------------------- test: function(){ return this.fullRegEx.test(this.buffer.join('')); } } /*----------------------------------------------------------------------------- * * Public API * *----------------------------------------------------------------------------- * * Extend the core jQuery object to allow access to the public functions * for the plugin functionality * *///-------------------------------------------------------------------------- $.extend({ /* $.fn.mask * $.fn.unmask * $.applyMasks */ mask : { //-- CONFIGURATION ACCESSORS ------------------------------------------ availableMasks : availableMasks, options : defaultOptions, /*--------------------------------------------------------------------- * * Unistall plugin * *--------------------------------------------------------------------- * * Unistall the entire plugin by deregistering all events and data * caches in the document but also delete the objects from memory. * *///------------------------------------------------------------------ uninstall: function(){ /* UNMASK */ $.annotated("@Mask", document).unmask(); /* GARBAGE COLLECT */ delete(availableMasks); delete($.applyMasks); delete($.fn.mask); delete($.fn.unmask); delete(defaultOptions); delete(Carat); delete(MaskBuffer); delete($.carat); delete($.mask); } }, carat : { get : Carat.getCaratPosition, set : Carat.setCaratPosition } }); })(jQuery);
Revision: 9339
Initial Code
Initial URL
Initial Description
Initial Title
Initial Tags
Initial Language
at October 31, 2008 08:52 by kouphax
Initial Code
/*--------------------------------------------------------------------------------- * * InputMask jQuery Plugin * *--------------------------------------------------------------------------------- * * Taking alot of inspiration and code from * http://digitalbush.com/projects/masked-input-plugin this is a masked input * solution that should handle most cases. It uses annotations to determine the * actual mask. Mask characters include, * * % - Any digit or numeric sign * # - Any digit * @ - Any letter * * - Any letter or digit * ~ - Any sign (+/-) * ? - Currencies ($,£,€ or ¥) * * @author James Hughes * * ------------------------------------------------------------------------------- * 29/10/2008 - Initial Version * ------------------------------------------------------------------------------- * *///------------------------------------------------------------------------------ (function($){ /*----------------------------------------------------------------------------- * * availableMasks * *----------------------------------------------------------------------------- * * Available Character Masks. This can be extended or modified via the * $.mask.availableMasks config. * *///-------------------------------------------------------------------------- var availableMasks = { '%' : '[-+0-9]', // Any digit or numeric sign '#' : '[0-9]', // Any digit '@' : '[A-Za-z]', // Any letter '*' : '[A-Za-z0-9]', // Any letter or digit '~' : '[+-]', // Any sign (+/-) '?' : '[\$£€¥]' // Typical World Currencies } /*----------------------------------------------------------------------------- * * $.applyMasks * *----------------------------------------------------------------------------- * * Deteremines, based @Mask on annotations, all elements below either the * specified root or the document element that should have masks applied * * @plugin * * @param opts - options * *///-------------------------------------------------------------------------- $.applyMasks = function(root, opts){ $.annotated("@Mask", root || document).mask(opts); } /*----------------------------------------------------------------------------- * * $.fn.mask * *----------------------------------------------------------------------------- * * Applies the annotated masks to the passed in elements. Applicable options * * invalidHandler custom event fired when field blurs invalid * placeholder placeholder for mask characters. defaults to _ * alwaysShowMask determine if we always show the input mask * permitIncomplete determine if we allow the field to be partially * filled on blur. * selectOnFocus : true * *///-------------------------------------------------------------------------- $.fn.mask = function(opts){ /*------------------------------------------------------------------------- * * Apply Mask * *------------------------------------------------------------------------- * * This section discovers the required mask on a per field basis and * applies the behaviour to the field * *///---------------------------------------------------------------------- return this.each(function(){ /*--------------------------------------------------------------------- * * No Mask Annotation Failover * *--------------------------------------------------------------------- * * Most of this API is open to the public therefore open to the * irresponsible, ignorant, clueless and just plain stupid. We need * to cater for as much worst case edge cases as we can without * making the good people suffer. Exit if no mask defined on the * element. * *///------------------------------------------------------------------ if(!$(this).annotations("@Mask")[0]){ return undefined }; /*--------------------------------------------------------------------- * * Apply Options * *--------------------------------------------------------------------- * * Merge the default and custom options resulting in a specific * options map for this function call. * *///------------------------------------------------------------------ var o = $.extend({}, defaultOptions, opts); /*--------------------------------------------------------------------- * * Assign Buffers * *--------------------------------------------------------------------- * * Iterate over the jQuery collection of fields passed in and add * the intial buffer data to each. * *///------------------------------------------------------------------ $(this).each(function(){ $(this).data("mask-buffer", new MaskBuffer( $(this).annotations("@Mask")[0].data, o.placeholder )); }); /*--------------------------------------------------------------------- * * Handle Blur * *--------------------------------------------------------------------- * * When a masked field blurs we need to handle overall validation and * based on the confguration options hide and show the mask. If the * field is invalid the event will fire the custom handler. * *///------------------------------------------------------------------ function handleBlur(){ var buffer = $(this).data("mask-buffer"); if(!o.permitIncomplete){ var v = $(this).val(); if(((!v || v === "") || !buffer.test())){ buffer.reset(); $(this).val( o.alwaysShowMask ? buffer.get() : "" ); o.invalidHandler(this, v); } } } /*--------------------------------------------------------------------- * * Handle Key Press * *--------------------------------------------------------------------- * * In the keypress event we are responsible for handling the standard * keys and managing the buffer * *///------------------------------------------------------------------ function handleKeyPress(e){ var code = ($.browser.msie) ? e.which : e.charCode; if(code != 0 && !(e.ctrlKey || e.altKey)){ var buffer = $(this).data("mask-buffer"); var carat = $.carat.get(this); var ncp = buffer.nextMaskPosition(carat.start-1); if(ncp < buffer.size()){ var characterTest = new RegExp(availableMasks[buffer.getMaskValue(ncp)]); var value = String.fromCharCode(code); if(characterTest.test(value)){ buffer.set(ncp, value); $(this).val(buffer.get()); $.carat.set(this, new Carat(buffer.nextMaskPosition(ncp))); } } /* handle ourselves */ return false; } } /*--------------------------------------------------------------------- * * Handle Key Down * *--------------------------------------------------------------------- * * In the key down we are responsible for handling special characters * such as delete, backspace and escape. As well as clearing any * selections * *///------------------------------------------------------------------ function handleKeyDown(e){ /*----------------------------------------------------------------- * * Key Code Constants * *----------------------------------------------------------------- * * Constant representing the useful keycodes * *///-------------------------------------------------------------- var BACKSPACE = 8; var DELETE = 46; var ESCAPE = 27; var carat = $.carat.get(this); var code = e.keyCode; var buffer = $(this).data("mask-buffer"); if(carat.isSelection() && (code == BACKSPACE || code == DELETE)){ buffer.reset(carat.start, carat.end); } switch(code){ case BACKSPACE: while(carat.start-- >= 0){ if(availableMasks[buffer.getMaskValue(carat.start)]){ buffer.reset(carat.start); if($.browser.opera){ /* Opera can't cancel backspace, prevent deletion */ $(this).val(buffer.get().substring(0, carat.start) + " " + buffer.get().substring(carat.start)); $.carat.set(this, new Carat(carat.start++)); }else{ $(this).val(buffer.get()); $.carat.set(this, new Carat(Math.max(buffer.getStartingMaskPosition(), carat.start))); } return false; } } break; case DELETE: buffer.reset(carat.start); $(this).val(buffer.get()); $.carat.set(this, new Carat(Math.max(buffer.getStartingMaskPosition(), carat.start))); return false; case ESCAPE: buffer.reset(); $(this).val(buffer.get()); $.carat.set(this, new Carat(buffer.getStartingMaskPosition())); return false; } } /*--------------------------------------------------------------------- * * Handle Focus * *--------------------------------------------------------------------- * * On focus we want to set the value to the current buffer state and * determine where we set the carat * *///------------------------------------------------------------------ function handleFocus(){ $(this).val($(this).data("mask-buffer").get()); if(o.selectOnFocus){ $.carat.set(this, new Carat(0, $(this).val().length)); }else{ $.carat.set(this, new Carat(Math.max(start, this.value.indexOf(o.placeholder)))); } } /*--------------------------------------------------------------------- * * Handle Paste * *--------------------------------------------------------------------- * * Custom event used to handle onpaste events. When we paste data * into a masked field we loop over the buffer and only apply the * valid parts of the paste. * * This method is not ideal but it does the job for now. * *///------------------------------------------------------------------ function handlePaste(){ var currentValue = $(this).val().split(''); var buffer = $(this).data("mask-buffer"); for(var i = 0; i < buffer.size(); i++){ if(availableMasks[buffer.getMaskValue(i)]){ var re = new RegExp(availableMasks[buffer.getMaskValue(i)]); if(re.test(currentValue[i])){ buffer.set(i, currentValue[i]) }else{ buffer.reset(i); } } } $(this).val(buffer.get()); $.carat.set(this, new Carat((this.value.indexOf(o.placeholder) == -1)?buffer.size():this.value.indexOf(o.placeholder))); handleBlur.call(this); } /*--------------------------------------------------------------------- * * Bind Events * *--------------------------------------------------------------------- * * Bind the mask events to the current field. Include the custom * paste event. * *///------------------------------------------------------------------ $(this).bind('blur', handleBlur); $(this).bind('keypress', handleKeyPress); $(this).bind('focus', handleFocus); $(this).bind('keydown', handleKeyDown); if ($.browser.msie){ this.onpaste = function(){ setTimeout(handlePaste,0); }; }else if ($.browser.mozilla){ this.addEventListener('input', handlePaste, false); } /*--------------------------------------------------------------------- * * Unmask Event * *--------------------------------------------------------------------- * * Add the unmask event to be fired only once. This will remove all * the mask associated data and event listeners from the field * *///------------------------------------------------------------------ $(this).one('unmask', function(){ /* Unbond Events */ $(this).unbind('blur', handleBlur); $(this).unbind('keypress', handleKeyPress); $(this).unbind('focus', handleFocus); $(this).unbind('keydown', handleKeyDown); /* Remove Buffer Data */ $(this).removeData("mask-buffer"); /* Remove Custom Paste Event */ if ($.browser.msie){ this.onpaste = null; }else if ($.browser.mozilla){ this.removeEventListener('input',handlePaste,false); } }); /*--------------------------------------------------------------------- * * Initialize Field * *--------------------------------------------------------------------- * * Call the handlePaste event to intialize the field. This will * handle any existing text that is there and set the inital value of * the field. * *///------------------------------------------------------------------ handlePaste.call(this); }); } /*----------------------------------------------------------------------------- * * $.fn.unmask * *----------------------------------------------------------------------------- * * Remove any masks from the passed in fields. * *///-------------------------------------------------------------------------- $.fn.unmask = function(){ return this.trigger('unmask'); } /*----------------------------------------------------------------------------- * * Default Options * *----------------------------------------------------------------------------- * * This map represents the global default options that should be used * when applying constraints. They can be overridden via custom maps * passed into the functions. * *///-------------------------------------------------------------------------- var defaultOptions = { invalidHandler : function(el){}, placeholder : "_", alwaysShowMask : true, permitIncomplete : false, selectOnFocus : true }; /*----------------------------------------------------------------------------- * * Carat Object * *----------------------------------------------------------------------------- * * The Carat object encapsulates carat functionality such as setting and * getting selections and postions. * * @constructor * *///-------------------------------------------------------------------------- var Carat = function(s,e){ this.start = s || 0; this.end = e || s || 0; } Carat.prototype = { /*------------------------------------------------------------------------- * * isSelection * *------------------------------------------------------------------------- * * Determines if the current carat position is a selection or not * *///---------------------------------------------------------------------- isSelection:function(){ return this.start < this.end; }, start : this.start, end : this.end } /*----------------------------------------------------------------------------- * * getCaratPosition * *----------------------------------------------------------------------------- * * Based on the passed in input field this function will return the current * carat position as an object with a start and end property. This allows * for both single carat positions and whole selection ranges * * @param el element to extract carat position from * *///-------------------------------------------------------------------------- Carat.getCaratPosition = function(el){ if (el.setSelectionRange){ return new Carat(el.selectionStart, el.selectionEnd); }else if (document.selection && document.selection.createRange){ var range = document.selection.createRange(); var start = 0 - range.duplicate().moveStart('character', -100000); return new Carat(start, start + range.text.length); } } /*----------------------------------------------------------------------------- * * Set Carat Position * *----------------------------------------------------------------------------- * * Sets the postion of the carat on the passed in element. Can be a single * postion of a selection. * * @param el element to set carat position omn * @param from start position of carat * @param to end position of carat (optional) * *///-------------------------------------------------------------------------- Carat.setCaratPosition = function(el, c){ if(el.setSelectionRange){ el.focus(); el.setSelectionRange(c.start,c.end); }else if (el.createTextRange){ var range = el.createTextRange(); range.collapse(true); range.moveEnd('character', c.end); range.moveStart('character', c.start); range.select(); } } /*----------------------------------------------------------------------------- * * Mask Buffer Class * *----------------------------------------------------------------------------- * * The Mask Buffer Class houses all the internal buffer mechanisms asnd * provides a cleaner interface for working with buffers. * *///-------------------------------------------------------------------------- var MaskBuffer = function(m, p){ /*------------------------------------------------------------------------- * * isFixedCharacter * *------------------------------------------------------------------------- * * Determines if the postion within the passed in mask is a fixed * character or if it is a mask character * * @param m - the mask * @param postion - position to check * *///---------------------------------------------------------------------- this.isFixedCharacter = function(m, position){ return !availableMasks[m.charAt(position)]; } /*------------------------------------------------------------------------- * * Generate RegExp * *------------------------------------------------------------------------- * * Build up a complete mask regualr expression to validate the enitre * input upon blur and paste events. * *///---------------------------------------------------------------------- var parsedMask = $.map(m.split(""), function(it){ return availableMasks[it] || (/[A-Za-z0-9]/.test(it)?"":"\\") + it; }); this.fullRegEx = new RegExp("^" + parsedMask.join("") + "$"); /*------------------------------------------------------------------------- * * Initialize Object * *------------------------------------------------------------------------- * * Build the initial buffer, find the first mask character position and * store the values internall in this object * *///---------------------------------------------------------------------- this.start = m.length; this.buffer = new Array(m.length); for(var i = m.length-1; i >= 0; i--){ if(!this.isFixedCharacter(m,i)){ this.start = i; this.buffer[i] = p; }else{ this.buffer[i] = m.charAt(i); } } this.initial = $.map(this.buffer, function(e){return e}), this.mask = m; } /* Extend the object */ MaskBuffer.prototype = { /*------------------------------------------------------------------------- * * getInitialMask * *------------------------------------------------------------------------- * * Returns a copy of the initial mask array that was used to create * this buffer instance. A copy is returned to to prevent any pass by * reference overwritting. * *///---------------------------------------------------------------------- getInitialMask: function(){ return $.map(this.initial, function(e){return e}); // clone }, /*------------------------------------------------------------------------- * * getStartingMaskPosition * *------------------------------------------------------------------------- * * Returns the index of the first mask (i.e. non fixed) character in the * mask * *///---------------------------------------------------------------------- getStartingMaskPosition: function(){ return this.start; }, /*------------------------------------------------------------------------- * * nextMaskPostion * *------------------------------------------------------------------------- * * from the passed in index returns the postion of the next non fixed * mask character. * *///---------------------------------------------------------------------- nextMaskPosition : function(i){ var target = i||0; while(++target < this.mask.length){ if(!this.isFixedCharacter(this.mask,target)){ return target; } } return this.mask.length; }, /*------------------------------------------------------------------------- * * get * *------------------------------------------------------------------------- * * Returns, depending on the arguments passed, either the string value of * the current buffer state of the character at the passed index of the * buffer * *///---------------------------------------------------------------------- get: function(){ return (arguments.length === 0)?this.buffer.join(''):this.buffer[arguments[0]]; }, /*------------------------------------------------------------------------- * * getMaskValue * *------------------------------------------------------------------------- * * Returns the character of the mask at the current position * *///---------------------------------------------------------------------- getMaskValue: function(idx){ return this.mask.charAt(idx); }, /*------------------------------------------------------------------------- * * size * *------------------------------------------------------------------------- * * Returns the length of the buffer/mask/initial mask etc * *///---------------------------------------------------------------------- size: function(){ return this.buffer.length; }, /*------------------------------------------------------------------------- * * set * *------------------------------------------------------------------------- * * Sets either the enitre buffer or a specific character of the buffer. * * @param i Array|Number. The array to set the buffer to or the postion * of the character to set the value of * @param v Boolean|Character. If boolean then it makes determines if a * clone of the array is to be used. If character it is the * character to put in the current position of the buffer; * *///---------------------------------------------------------------------- set: function(i,v){ if(i.constructor === Array){ this.buffer = (v)?$.map(i, function(e){return e}):i; }else{ this.buffer[i] = v; } }, /*------------------------------------------------------------------------- * * reset * *------------------------------------------------------------------------- * * Resets, returns back to the inital mask, all/some/one part of the * current buffer depending on inputs * *///---------------------------------------------------------------------- reset: function(){ var start, end; switch(arguments.length){ case 0 : start = 0; end = this.buffer.length; break; case 1 : start = end = arguments[0]; break; case 2 : start = arguments[0]; end = arguments[1]; break; } for(var i = start; i <= end; i++){ this.buffer[i] = this.initial[i]; } }, /*------------------------------------------------------------------------- * * test * *------------------------------------------------------------------------- * * Test current buffer state against the complete RegExp of the field to * determine if it is invliad/incomplete * *///---------------------------------------------------------------------- test: function(){ return this.fullRegEx.test(this.buffer.join('')); } } /*----------------------------------------------------------------------------- * * Public API * *----------------------------------------------------------------------------- * * Extend the core jQuery object to allow access to the public functions * for the plugin functionality * *///-------------------------------------------------------------------------- $.extend({ /* $.fn.mask * $.fn.unmask * $.applyMasks */ mask : { //-- CONFIGURATION ACCESSORS ------------------------------------------ availableMasks : availableMasks, options : defaultOptions, /*--------------------------------------------------------------------- * * Unistall plugin * *--------------------------------------------------------------------- * * Unistall the entire plugin by deregistering all events and data * caches in the document but also delete the objects from memory. * *///------------------------------------------------------------------ uninstall: function(){ } }, carat : { get : Carat.getCaratPosition, set : Carat.setCaratPosition } }); })(jQuery);
Initial URL
Initial Description
Based on my Annotations plugin this plugin offers the ability to use Mask annotations to apply input masks over input elements on a page. Very much BETA. See comments for use. <!--@Mask("##/##/####")-->
Initial Title
InputMask jQuery Plugin
Initial Tags
javascript, plugin, jquery
Initial Language
JavaScript