Return to Snippet

Revision: 9342
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
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
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
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