Saturday, 16 April 2016

An interesting feature to have for your Input Text Box on the JSPX - "The Character Counter"

Yes, The Character Counter - sounded like a really cool name to me and that's why I chose it.
First let me give u a bit of a background for the need to create this component.

So, I had this weird use case where I was asked to restrict the user to 2000 characters. So, I created an ADF input text box and set the maximum length to 2000 as is obvious.
I was surprise to see a defect logged by one of the testers that it is allowing more than 2000 characters to be entered and during commit, is throwing a JBO exception saying the actual length:2048 is greater than the column length:2000.
I tested it with random strings of length more than 2000 characters and it was working fine. It used to truncate the string entered till 2000 characters and then save the rest into the database. Then, I noticed something strange with the string used by the tester. It had bullets, new lines and other special characters. I immediately checked the table structure and found out that the column was defined as VARCHAR2(2000), but it was in bytes. Yes my ADF folks, the problem was because the size was 2000 bytes and not 2000 characters. Now, I didn't have the luxury to just go and alter the table. I played the whole thing in my mind and came up with this idea that I would restrict the user to 2000 bytes and not 2000 characters. So, here is what I did.

There is no way we can make the maximumLength attribute of the af:inputText understand the string in bytes. It only understands one language, which is characters. So, I created a custom validator in my managed bean inside which I was first converting the string entered into a byte array and then checking its length and throwing the appropriate message to the user if it exceeded. Here is the code snippet:

<af:inputText label="Add Notes" autoSubmit="true" maximumLength="#{bindings.Notes.hints.precision}" rows="4" columns="100" value="#{pageFlowScope.AddNotesMB.notes}" id="it1"
            validator="#{pageFlowScope.AddNotesMB.notesValidator}">
</af:inputText>

    public void notesValidator(javax.faces.context.FacesContext facesContext,
                               UIComponent uIComponent, Object object) {
        // Add event code here...
        FacesContext fc = facesContext;
        if(object!=null){
            String notes = (String)object;
            byte[] b = notes.getBytes(Charset.forName("UTF-8"));
                if(b.length>2000){
                    FacesMessage fm = new FacesMessage(FacesMessage.SEVERITY_ERROR,null,"Entered text is greater than the allowed limit, Please enter a smaller text");
                    fc.addMessage(null, fm);
                    ((RichInputText)uIComponent).setValid(false);
                }
        }
    }

Now, this worked fine and I pushed the code into the QA environment. The tester again fails the defect fix (we all know how mean the Tester Community is!!!) saying that it is stopping the user before even he has entered 2000 characters, that's because some characters use up more than one byte. And what really messed up the situation for me was every editor was returning a different character count for the same text viz. Word and Notepad++.
So, I thought may be if I myself tell the user that how many more characters he is allowed to enter, would make things easy for the user (but not for me, the developer). So, I thought of putting up a counter over the input text which would change as the user types, or copy pastes, or right-click & copy. I surrounded the input text with a panel box and inserted an output text in the toolbar facet of the panel box. And then called a javascript using the clientListener attribute which would count the number of bytes (not characters) remaining. I had to create a separate javascript method to convert the char sequence into a byte array. Here is the snippet after implementing that:

<af:panelBox id="pgl0" clientComponent="true">
        <af:inputText label="Add Notes" autoSubmit="true" clientComponent="true" maximumLength="#{bindings.Notes.hints.precision}" rows="4" columns="100" value="#{pageFlowScope.AddNotesMB.notes}" id="it1"
            validator="#{pageFlowScope.AddNotesMB.notesValidator}">
            <af:clientListener method="charChecker" type="keyDown"/>
        </af:inputText>
    </af:panelBox>

The javascript, Your Highness, goes like this:
    <af:resource type="javascript">
        function charChecker(evt){
             textfield = evt.getCurrentTarget();
             textfield_current_content = textfield.getSubmittedValue();
             textfield_current_content_length =getByteLen(textfield_current_content);
             vGREEN = textfield.getMaximumLength();
             vORANGE = vGREEN/2;
             vRED = vORANGE/4;
             vBACKGROUND_COLOR ='white';
             vCOLOR ='black';
             counter = AdfPage.PAGE.findComponentByAbsoluteId("pt1:r1:0:r2:0:out1");  
             counter_value = counter.getValue();
             if (vGREEN - textfield_current_content_length &lt;= vGREEN &amp;&amp; vGREEN - textfield_current_content_length &gt;= 0){
                 if(vGREEN - textfield_current_content_length &lt;= vRED){
                    vBACKGROUND_COLOR ='red';
                    vCOLOR='white';
                 } else{
                    if(vGREEN - textfield_current_content_length &lt;= vORANGE){
                        vBACKGROUND_COLOR ='orange';
                        vCOLOR='black';
                    }else{
                        vBACKGROUND_COLOR ='green';
                        vCOLOR='white';
                    }
                 }
             }
             counter.setInlineStyleProperty("background-color",vBACKGROUND_COLOR);counter.setInlineStyleProperty("color",vCOLOR);
             counter_value = vGREEN - textfield_current_content_length;
             counter.setValue(counter_value);
        }
        function getByteLen(str) {
            var utf8 = [];
            for (var i=0; i &lt; str.length; i++) {
                var charcode = str.charCodeAt(i);
                if (charcode &lt; 0x80) utf8.push(charcode);
                else if (charcode &lt; 0x800) {
                    utf8.push(0xc0 | (charcode >> 6), 
                              0x80 | (charcode &amp; 0x3f));
                }
                else if (charcode &lt; 0xd800 || charcode &gt;= 0xe000) {
                    utf8.push(0xe0 | (charcode >> 12), 
                              0x80 | ((charcode>>6) &amp; 0x3f), 
                              0x80 | (charcode &amp; 0x3f));
                }
                else {
                    // let's keep things simple and only handle chars up to U+FFFF...
                    utf8.push(0xef, 0xbf, 0xbd); // U+FFFE "replacement character"
                }
            }
            var length=utf8.length;
            return length;
        }
    </af:resource>

Notice, the id used to find the element "out1" and stored into the variable counter is because my input text was on a taskflow which was being invoked from another taskflow which was present on a certain JSPX (Too much encapsulation I guess :P :P :P). So, I couldn't get it to work with just "out1". I had to use "pt1:r1:0:r2:0:out1" instead which is the Id of the component in the HTML version. The various colors are used just to give user an idea of how many characters (ergo bytes) are left. So, the counter value comes with a green background till 1000 characters are left. 1000 to 250 is Orange and after that its all Red.

Like I always say, its insignificant but it matters. Hope you guys liked it.

No comments:

Post a Comment