Thanks for the input Adam!

I updated the code snippet below with some comments and updates that 
dynamically attach the keydown/blur events. Could you elaborate on how to 
implement the inline hook and where to look in the code to do this? I would 
like to convert the component into a validator/converter and it looks like 
adding a hook would be better suited to do so.

-----Original Message-----
From: Adam Winer [mailto:[EMAIL PROTECTED]
Sent: Thursday, June 07, 2007 11:32 AM
To: MyFaces Discussion
Subject: Re: [Trinidad] Input Text Format That Uses A Mask


On 6/7/07, William Hoover <[EMAIL PROTECTED]> wrote:
> Question: If it was to use a converter/validator at what point would it 
> append onfocus (and any other events needed)? With a component it is appended 
> when it is rendered. If it were a validator it would have to add events to 
> the component its validating. Is this a good practice?

I was thinking we'd add a hook to the general client-side validation
framework that, during onload (and after PPR replacement, if new
fields are added or rewritten), would let  the validators have a pass
at the DOM.

There's a few big advantages to doing this in JS, instead of in rendering:
- There's no I/O cost of sending across the JS
- You're writing JS in JS, instead of writing Java that outputs JS,
which is always awkward IMO
- The Renderer doesn't need to change

> I'm in the process of implementing the previously suggested JS event 
> attachments so the only event that has to be explicitly attached is the 
> focus. Correct me if I'm wrong, but once the field gains focus it should 
> dynamically attach the onkeydown/blur events to the passed input text 
> element? Currently it attaches a "MaskType" object to the input text to keep 
> track of input info such as the last cursor position, the raw mask, the 
> parsed mask, the viewing mask, and the containing mask validator function. 
> This makes it easier to validate the input content because the validating 
> method is attached to the input element itself. It also prevents reparsing of 
> code that isn't necessary. Here is the client script (does not yet have the 
> dynamic event attachment mentioned above) Suggestions are welcomed!:

Dynamically attaching the onkeydown/blur when you receive the focus definitely
seems like a good idea.

I guess my first suggestion for the JS is that some comments would be nice!

-- Adam


// TODO : bug in Opera (doesnt allow backspace cancel). 
// TODO : needs tested on other OS platforms.
// TODO : need to add feature that will allow a mask that can have an infinate 
(or predifined) amount of validated values/characters at the end of the mask
/**
 * CoreInputTextForamt component script used for mask/regexp operations.
 * Mask Individual Character Usage:
 * 9 - designates only numeric values
 * L - designates only uppercase letter values
 * l - designates only lowercase letter values
 * A - designates only alphanumeric values
 * X - denotes that a custom client script regular expression is specified</li>
 * All other characters are assumed to be "special" characters used to mask
 * the input component
 * Example 1:
 * (999)999-9999 only numeric values can be entered where the the character
 * position value is 9. Parenthesis and dash are non-editable/mask characters.
 * Example 2:
 * 99L-ll-X[^A-C]X only numeric values for the first two characters,
 * uppercase values for the third character, lowercase letters for the
 * fifth/sixth characters, and the last character X[^A-C]X together counts 
 * as the eighth character regular expression that would allow all characters 
 * but "A", "B", and "C". Dashes outside the regular expression are 
 * non-editable/mask characters.
 */
var CoreInputTextFormat = {
        processMaskFocus: function(input, mask, clearWhenInvalid){
                // create an input mask and register it on the specified input 
(if it hasnt already been added by a previous call
                CoreInputTextFormat.createInputMask(input, mask, 
clearWhenInvalid);
                if(input.value.length == 0){
                        // when the input value is empty populate it with the 
viewing mask and move the cursor to the
                        // beginning of the input field
                        var cursorPos = 
CoreInputTextFormat.getCursorPosition(input, input.value);
                        input.value = input.mask.viewMask;
                        CoreInputTextFormat.moveCursorToPosition(input, null, 
cursorPos);
                }
        },
        getEvent: function(e) {
                // get the event either from the window or from the passed event
                return (typeof event != 'undefined')? event: e;
        },
        handleEventBubble: function(keyEvent, keyCode){
                // this method ensures that the key enterned by the user is not 
propagated unless it is a tab or arrow key
                try {
                        if(keyCode && (keyCode.isTab || 
keyCode.isLeftOrRightArrow)){
                                // allow all tab/arrow keys by returning true- 
no further action required
                                return true;
                        }
                        keyEvent.cancelBubble = true;
                        if(keyEvent.stopPropagation){
                                // prevent other event triggers
                                keyEvent.stopPropagation();
                        }
                        if(keyEvent.preventDefault){
                                // prevent the default event from firing. in 
this case it is propagation of the keyed input
                                keyEvent.preventDefault();
                        }
                        return false;
                } catch(e) {
                        alert(e.message);
                }
        },
        createInputMask: function(input, mask, clearWhenInvalid) {
                // if this input hasnt already registered its mask go ahead and 
do so now. This only needs to be performed the
                // first time the input is encountered when it gains focus. It 
will attach the MaskType object to the input object
                // add add all of the appropriate event listeners to ensure 
that the mask is applied
                if(!input.mask || input.mask.rawMask != mask){
                        input.mask = new CoreInputTextFormat.MaskType(input, 
mask, clearWhenInvalid);
                        // add the event listeners that will ensure that when 
the input contains an incomplete mask it will be remove.
                        // Also, make sure that the keydown event is fired from 
this point forward thus invoking the mask format.
                        if(input.addEventListener){
                                // most doms
                                input.addEventListener('blur', 
function(){input.mask.removeValueWhenInvalid();}, false);
                                input.addEventListener('keydown', 
function(e){return input.mask.processMaskFormatting(e);}, false);
                                if(window.opera){
                                        // in opera- need to ensure that the 
keypress event isnt interfering with this input mask
                                        input.addEventListener('keypress', 
function(e){return 
CoreInputTextFormat.handleEventBubble(CoreInputTextFormat.getEvent(e), null);}, 
false);
                                }
                        } else if(input.attachEvent) {
                                // ie
                                input.attachEvent('onblur', 
function(){input.mask.removeValueWhenInvalid();});
                                input.attachEvent('onkeydown', 
function(e){return input.mask.processMaskFormatting(e);});
                        } else {
                                // other browsers that do not support dynamic 
event propagations
                                input.onBlur = 
function(){input.mask.removeValueWhenInvalid();};
                                input.onKeyDown = 
function(e){input.mask.processMaskFormatting(e)};
                        }
                }
        },
        getCursorPosition: function(input, previousValue) {
                // gets the current cursor position (s=start, e=end) and 
creates/returns a new CursorPosition instance
                var s, e, r;
                if(input.createTextRange){
                        // ie- need to capture the start/end cursor positions
                        r = document.selection.createRange().duplicate();
                        r.moveEnd('character', previousValue.length);
                        if(r.text === ''){
                                s = previousValue.length;
                        } else {
                                s = previousValue.lastIndexOf(r.text);
                        }
                        r = document.selection.createRange().duplicate();
                        r.moveStart('character', -previousValue.length);
                        e = r.text.length;
                } else {
                        // other browsers
                        s = input.selectionStart;
                        e = input.selectionEnd;
                }
                return new CoreInputTextFormat.CursorPosition(s, e, r, 
previousValue);
        },
        moveCursorToPosition: function(input, keyCode, cursorPosition) {
                // moves a cursor position for the passed input element to the 
specified cursor position- because the 
                // range cursor position is 1 indexed we add an additional 
space (unless the pressed key is a backspace)
                var p = (!keyCode || (keyCode && keyCode.isBackspace))? 
cursorPosition.start: cursorPosition.start + 1;
                if(input.createTextRange){
                        // ie move- cursor to the index p
                        cursorPosition.range.move('character', p);
                        cursorPosition.range.select();
                } else {
                        // other browser- move cursor to the index p
                        input.selectionStart = p;
                        input.selectionEnd = p;
                }
        },
        injectValue: function(input, keyCode, cursorPosition) {
                // inject the validated key into the input mask at the 
specified cursor position and return true on success
                var key = (keyCode.isBackspace)? '_': 
input.mask.getValidatedKey(keyCode, cursorPosition);
                if(key){
                        input.value = cursorPosition.previousValue.substring(0, 
cursorPosition.start) + key + 
cursorPosition.previousValue.substring(cursorPosition.start + 1, 
cursorPosition.previousValue.length);
                        return true;
                }
                // invalid key
                return false;
        },
        MaskType: function(inputTextElement, mask, clearWhenInvalid) {
                // this object instance is holds relative mask properties for a 
specified input element
                this.inputTextElement = inputTextElement;
                // designates whether or not the input value is cleared when 
its mask is incomplete and a blur event is triggered
                this.clearWhenInvalid = clearWhenInvalid;
                // holds the last validated key code
                this.lastValidatedKeyCode = null;
                // the mask value used to validate/mask valid input
                this.rawMask = mask;
                // the mask displayed in the input
                this.viewMask = '';
                // the string array of all the raw mask values (some indexes 
contain more than one char so we need to track this) 
                this.maskArray = new Array();
                var mai = 0;
                var regexp = '';
                // cycle through the raw mask and perform view mask conversions
                for(var i=0; i<mask.length; i++){
                        if(regexp){
                                if(regexp == 'X'){
                                        // end of current regexp slot
                                        regexp = '';
                                }
                                if(mask.charAt(i) == 'X'){
                                        // current mask array index contains 
the complete regexp so we need to store it in the array
                                        this.maskArray[mai] = regexp;
                                        mai++;
                                        regexp = null;
                                } else {
                                        // still in the middle of the regexp 
keep adding the current character to the regexp
                                        regexp += mask.charAt(i);
                                }
                        } else if(mask.charAt(i) == 'X'){
                                // current slot is a regexp
                                regexp += 'X';
                                this.viewMask += '_';
                        } else if(mask.charAt(i) == '9' || mask.charAt(i) == 
'L' || mask.charAt(i) == 'l' || mask.charAt(i) == 'A') {
                                // the current mask character is one of the 
predefined/reserved characters
                                this.viewMask += '_';
                                this.maskArray[mai] = mask.charAt(i);
                                mai++;
                        } else {
                                // just a regular char
                                this.viewMask += mask.charAt(i);
                                this.maskArray[mai] = mask.charAt(i);
                                mai++;
                        }
                }
                // the predefined/reserved characters need to be replaced with 
the viewing mask char that desigantes an editable value (underscore)
                this.specialChars = this.viewMask.replace(/(L|l|9|A|_|X)/g,'');
                this.getValidatedKey = function(keyCode, cursorPosition) {
                        // validates if the passed key code is valid for the 
specified cursor position and returns the value if it is. otherwise, return 
false
                        var maskKey = this.maskArray[cursorPosition.start];
                        if(maskKey == '9'){
                                // only allow numbers at the specified slot
                                return keyCode.pressedKey.match(/[0-9]/);
                        } else if(maskKey == 'L'){
                                // only allow uppercase letters at specified 
slot (convert if necessary)
                                return (keyCode.pressedKey.match(/[A-Za-z]/))? 
keyCode.pressedKey.toUpperCase(): null;
                        } else if(maskKey == 'l'){
                                // only allow lowercase letters at specified 
slot (convert if necessary)
                                return (keyCode.pressedKey.match(/[A-Za-z]/))? 
keyCode.pressedKey.toLowerCase(): null;
                        } else {
                                if(maskKey == 'A'){
                                        // only allow alpha-numeric values at 
the specified slot
                                        return 
keyCode.pressedKey.match(/[A-Za-z0-9]/);
                                } else {
                                        // only allow values that are verified 
by the specified regexp at the specified slot
                                        return 
(this.maskArray[cursorPosition.start].length > 1)? keyCode.pressedKey.match(new 
RegExp(maskKey)): null;
                                }
                        }
                };
                this.removeValueWhenInvalid = function(){
                        // removes value from the input element when the mask 
is incomplete
                        if(this.inputTextElement.value.indexOf('_') > -1){
                                this.inputTextElement.value = '';
                        }
                };
                this.processMaskFormatting = function(e) {
                        // capture event (should be the keydown event)
                        var onKeyDownEvent = CoreInputTextFormat.getEvent(e);
                        // create the key code from the event. 
                        var keyCode = new 
CoreInputTextFormat.KeyCode(onKeyDownEvent);
                        
if(CoreInputTextFormat.handleEventBubble(onKeyDownEvent, keyCode)){
                                // the pressed key is allowed to propagate- no 
mask injection required
                                return true;
                        }
                        var v = this.inputTextElement.value;
                        if(v.length === 0){
                                // when the input value is empty populate it 
with the viewing mask
                                this.inputTextElement.value = this.viewMask;
                        }
                        var cursorPos = 
CoreInputTextFormat.getCursorPosition(this.inputTextElement, v);
                        if(cursorPos.end == cursorPos.previousValue.length && 
!keyCode.isBackspace){
                                // input cursor position is at the end of the 
mask- do not allow any more characters to be keyed
                                return false;
                        }
                        // move the cursor position to the next slot that does 
not contain a mask character
                        
while(this.inputTextElement.mask.specialChars.match(RegExp.escape(cursorPos.previousValue.charAt(((keyCode.isBackspace)?
 cursorPos.start-1: cursorPos.start))))){
                                if(keyCode.isBackspace) {
                                        // backspace needs to move the cursor 
backwards
                                        cursorPos.decStart();
                                } else {
                                        // still moving cursor one space to the 
right
                                        cursorPos.incStart();
                                }
                                if(cursorPos.start >= 
cursorPos.previousValue.length || cursorPos.start < 0){
                                        // end of the mask- no more keys should 
be keyed
                                        return false;
                                }
                        }
                        if(keyCode.isBackspace){
                                // need to go back one space to the left
                                cursorPos.decStart();
                        }
                        // inject the key that was pressed into the input value 
mask
                        
if(CoreInputTextFormat.injectValue(this.inputTextElement, keyCode, cursorPos)){
                                // when the injection is sucessful move the 
cursor to the next slot to the right of the injected key
                                
CoreInputTextFormat.moveCursorToPosition(this.inputTextElement, keyCode, 
cursorPos);
                        }
                        // because the pressed key is being injected we always 
need to return false to prevent duplicate
                        // key injection
                        return false;
                };
        },
        KeyCode: function(onKeyDownEvent) {
                this.onKeyDownEvent = onKeyDownEvent;
                // get the unicode value from the key event
                this.unicode = onKeyDownEvent.which? onKeyDownEvent.which: 
(onKeyDownEvent.keyCode? onKeyDownEvent.keyCode: (onKeyDownEvent.charCode? 
onKeyDownEvent.charCode: 0));
                this.isShiftPressed = onKeyDownEvent.shiftKey == false || 
onKeyDownEvent.shiftKey == true? onKeyDownEvent.shiftKey: 
(onKeyDownEvent.modifiers && (onKeyDownEvent.modifiers & 4)); //bitWise AND
                // TODO : need to get cap lock capture for onkeydown event
                //this.isCapLock = ((!this.isShiftPressed && this.unicode >= 65 
&& this.unicode <= 90) || (this.unicode >= 97 && this.unicode <= 122 && 
this.isShiftPressed));
                if(this.unicode >= 96 && this.unicode <= 105) {
                        this.unicode -= 48; // handle number keypad
                }
                if(this.unicode >= 65 && this.unicode <= 90 && 
!this.isShiftPressed){
                        this.unicode += 32; // handle uppercase
                }
                this.isTab = (this.unicode == 9)? true: false;
                this.isBackspace = (this.unicode == 8)? true: false;
                this.isLeftOrRightArrow = (this.unicode == 37 || this.unicode 
== 39)? true: false;
                // capture the actual key for the passed key code
                this.pressedKey = String.fromCharCode(this.unicode);
        },
        CursorPosition: function(start, end, range, previousValue) {
                // holds the cursor position values
                this.start = isNaN(start)? 0: start;
                this.end = isNaN(end)? 0: end;
                this.range = range;
                this.previousValue = previousValue;
                this.incStart = function(){
                        this.start++;
                };
                this.decStart = function(){
                        this.start--;
                };
        }
};
// Add escape prototype feature to RegExp object
if(!RegExp.escape) {
        RegExp.escape = function(text){
                var sp;
                if(!arguments.callee.sRE){
                        
sp=['/','.','*','+','?','|','(',')','[',']','{','}','\\'];
                        arguments.callee.sRE = new RegExp('(\\' + 
sp.join('|\\') + ')','g');
                }
                return text.replace(arguments.callee.sRE, '\\$1');
        };
}
>
> -----Original Message-----
> From: Adam Winer [mailto:[EMAIL PROTECTED]
> Sent: Wednesday, June 06, 2007 8:26 PM
> To: MyFaces Discussion
> Subject: Re: [Trinidad] Input Text Format That Uses A Mask
>
>
> Trinidad already has a validateRegExp validator, FWIW,
> which attaches both client-side and server-side validation, but
> has no mask functionality.  The Tomahawk validator is just a Java
> JSF Validator, no client-side functionality, right?
>
> What this masking thing adds is:
> -  a simpler syntax for expressing masking.  For example,
>  to do a phone number in regexp, you'd have to write something
> like:
>   \(\d\d\d\)-\d\d\d-\d\d\d\d
> instead of:
>   (999)-999-9999
> - keydown/blur/focus handling so that the parts of the
>   mask that are "fixed" can automatically be inserted
>   and skipped over.
>
> onblur, I imagine, is responsible for reporting errors.
> That part of this should definitely be hooked into the
> existing Trinidad client-side validation - which isn't
> just alerts anymore thanks to Dan Robinson, but does
> need a final tweak to be onblur instead of onsubmit,
> at least for INLINE style.
>
> That leaves keydown/focus.  One way this might be
> implemented is having an optional method on the
> JS validator instances that, if present, will get called
> with the relevant DOM form element.  At that point,
> the validator could attach any keydown/blur/focus
> handling that it wants to.  The framework would handle
> blur in general, the mask validator would just need to
> attach keydown/focus.
>
> -- Adam
>
>
>
>
> On 6/6/07, Mike Kienenberger <[EMAIL PROTECTED]> wrote:
> > How does this compare to validateRegExpr in Tomahawk, particularly if
> > it becomes a validator instead of a component?
> >
> > http://myfaces.apache.org/tomahawk/validateRegExpr.html
> >
> > On 6/6/07, William Hoover <[EMAIL PROTECTED]> wrote:
> > > Point well taken! The component should extend UIXInput instead and 
> > > renamed CoreInputMask. Are you are proposing to change this into a 
> > > validator or converter instead of a component extension? If it was to use 
> > > a converter/validator at what point would it append the JS event calls 
> > > (onkeydown, onblur, onfocus)? As of yet it does not address server-side 
> > > validation, but it would be fairly easy to implement. Currently, using 
> > > the example of a (999)999-9999 mask, and an input of (415)555-1212 the 
> > > bean would see exactly what the user input i.e. (415)555-1212 If a 
> > > converter/validator was to be used it would be simple enough to strip the 
> > > mask characters. I would assume that it would be best to have this as an 
> > > option because it may be desirable to maintain the mask?
> > >
> > >
> > >
> > > -----Original Message-----
> > > From: Adam Winer [mailto:[EMAIL PROTECTED]
> > > Sent: Wednesday, June 06, 2007 12:03 PM
> > > To: MyFaces Discussion
> > > Subject: Re: [Trinidad] Input Text Format That Uses A Mask
> > >
> > >
> > > A few and questions:
> > > - Generally speaking, we don't extend CoreInputText, we just
> > >   re-extend UIXInput.  The metadata system supports "includes"
> > >   for generation, so you don't have to cut-and-paste that much.
> > >   One good reason, in this case, is that I assume that this
> > >   component doesn't support <TEXTAREA>, just <INPUT> -
> > >   so you don't want "rows" or "wrap".
> > > - I'd love to see this as a converter or validator tag that can be
> > >   added to an ordinary af:inputText.  We'd need a bit of beefing
> > >   up of our client-side code JS framework for validators, but
> > >   that'd be worthwhile.
> > > - What's the server-side model look like?  E.g., when you
> > >    have (999)999-9999, does your bean see strings like
> > >    (415)555-1212, or do you get 4155551212?  Is there server-side
> > >    validation to double-check the mask was applied?
> > > - If this is a component, I think CoreInputTextMasked might
> > >   be clearer, if the property is named "mask".
> > >
> > > -- Adam
> > >
> > >
> > >
> > > On 6/6/07, William Hoover <[EMAIL PROTECTED]> wrote:
> > > > Thanks for the info Adam!
> > > >
> > > > The component (CoreInputTextFormat) logic is fairly simple and could be 
> > > > directly integrated into the CoreInputText, if desired. It extends 
> > > > CoreInputText and adds two extra PropertyKeys: "mask" and 
> > > > "clearWhenInvalid". The "mask" attribute designates the pattern for 
> > > > which will be used to prevent invalid characters at the specified 
> > > > slots. For Example, (999)999-9999 would be displayed as (___)___-____ 
> > > > allowing only numeric values to be entered where underscores are 
> > > > present (see examples below for a more detailed overview). The 
> > > > "clearWhenInvalid" is an option to clear the contents (onblur) of the 
> > > > input field when it does not meet the mask pattern requirements- 
> > > > default is currently true. The only other logic contained in the 
> > > > component is used to make the JS calls: onblur, onfocus, and onkeydown. 
> > > > The client script is contained in a namespace called 
> > > > "CoreInputTextFormat" so none of the functions will interfere with 
> > > > other Trinidad scripts (as you suggested in TRINIDAD-37 it would be 
> > > > nice if we had a Trinidad namespace that could register component level 
> > > > namespaces!). It does however add a prototype extension to RegExp to 
> > > > allow RegExp.escape(someText) preventing recompilation of the escape 
> > > > expression. That is it! I don't think there is a significant amount of 
> > > > code to warrant a CLA (client script under 200 lines, component logic 
> > > > is trivial). Let me know what your thoughts on all of this!
> > > >
> > > > Mask Individual Character Usage and Reserved Characters:
> > > > 9 - designates only numeric values
> > > > L - designates only uppercase letter values
> > > > l - designates only lowercase letter values
> > > > A - designates only alphanumeric values
> > > > X - denotes that a custom client script regular expression is specified
> > > > All other characters are assumed to be "special" characters used to 
> > > > mask the input component
> > > >
> > > > Examples:
> > > > (999)999-9999
> > > >         only numeric values can be entered where the character position 
> > > > value is 9. Parenthesis and dash are non-editable/mask characters.
> > > > 99L-ll-X[^A-C]X
> > > >         only numeric values for the first two characters, uppercase 
> > > > values for the third character, lowercase letters for the fifth/sixth 
> > > > characters, and the last character X[^A-C]X together counts as the 
> > > > eighth character regular expression that would allow all characters but 
> > > > "A", "B", and "C". Dashes outside the regular expression are 
> > > > non-editable/mask characters.
> > > >
> > > > -----Original Message-----
> > > > From: Adam Winer [mailto:[EMAIL PROTECTED]
> > > > Sent: Tuesday, June 05, 2007 7:09 PM
> > > > To: MyFaces Discussion
> > > > Subject: Re: [Trinidad] Input Text Format That Uses A Mask
> > > >
> > > >
> > > > Roughly speaking, you:
> > > > - Create an issue on JIRA
> > > > - Attach a patch
> > > > - If it's a significant quantity of code, file a CLA
> > > >   http://www.apache.org/licenses/icla.txt
> > > >
> > > > It's also generally a good thing to talk over the
> > > > design first.  I'd thing it'd be great if this were part of
> > > > the client-side validation code, instead of just its
> > > > own code.  I think getting this issue fixed:
> > > > http://issues.apache.org/jira/browse/TRINIDAD-37
> > > > ... would be important for that.
> > > >
> > > > I'd love to see this functionality!
> > > >
> > > > -- Adam
> > > >
> > > >
> > > > On 6/5/07, William Hoover <[EMAIL PROTECTED]> wrote:
> > > > >
> > > > >
> > > > >
> > > > > Hello all,
> > > > > I have created a Trinidad component that allows input text boxes to 
> > > > > have a
> > > > > user defined mask for entries on the client (similar to Atlas MaskEdit
> > > > > <http://www.fci.com.br/maskedit/MaskEdit/MaskEdit.aspx>). I
> > > > > would like to know what the process/procedure is to commit this 
> > > > > component to
> > > > > the sandbox?
> > > >
> > > >
> > >
> > >
> >
>
>

Reply via email to