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? > > > > > > > > > > > > > > > > > >