Tuesday 10 November 2009

Capturing Tab key in GWT TextArea

A common thing developers have to deal with when using text areas in their web applications is the TAB key. Different from most of the other common web UI components, the text area component may require to capture the TAB key and "display" it as part of the text instead of transferring the focus to the next UI component in line.

This article will discuss how to capture the TAB key and prevent the transfer of focus within the text area. Technically speaking this code should happen within JavaScript, but this article makes use of the Google Web Toolkit (GWT). Here we're not going to discuss the pros and cons of GWT, but one should consider such API when developing JavaScript websites. Please refer to http://code.google.com/webtoolkit/ for more information about GWT.

For those of you who would only like to see the solution, please click here or scroll down to the bottom. For the others who would like to read about it; we'll start with a simple example and will be incrementing on it as we go along.

The Solution

We need to do the following things in order to capture the TAB key within the text area:

  • Listen to key (Key Down) events
  • Filter the TAB key (check if the TAB key is pressed)
  • Prevent the default action and stop event propagation
  • Insert the tab character at the current cursor position
  • Register the listener with the text area

Each of the above points are discussed, first individually and then merged together as one solution.

Key Down Event Handler

There are three types of key events (and handlers which handle the respective event):

  • Key Down
  • Key Up
  • Key Pressed

The first question that comes to mind is, why three? Very simple; there are situations, like ours, where you want to handle the events before they trigger other events (transfer focuse), while in other cases we just want to act after these happened, such as the key pressed event (spellchecker for example). In our case we want to disable the default behaviour from propagating any further and prevent the text area from losing its focus.

GWT provides the com.google.gwt.event.dom.client.KeyDownHandler interface that can be registered with a text area and is invoked by the text area whenever a key is pressed. To be precise, this handler is invoked when the key is down (the first from the three handlers mentioned above). The KeyDownHandler interface has only one method called onKeyDown() that expects one parameter of type: com.google.gwt.event.dom.client.KeyDownEvent. The following code fragment shows how to create a key down handler (Java - GWT).

KeyDownHandler handler = new KeyDownHandler() {
  @Override
  public void onKeyDown(KeyDownEvent event) {
    // Handle key down event here
  }
}
How come you're creating an instance of an interface? As you can see form the above example, Java allows to create new classes that extends or implements other classes and interfaces on-the-fly. These are called, anonymous inner classes. Anonymous, because the new class does not have a name. This is very convenient as you don't have to create a new class, but just implement in within the class you're using it.

We can now register the above handler with the text area as illustrated in the following code fragment

TextArea textArea = new TextArea();
textArea.addKeyDownHandler(handler); 

The numbered list shown above, lists this step as last. It doesn't make a difference when you register the handler, as long as the handler is initialised and not null.

Filter the TAB key

The onKeyDown() method is invoked by the text area every time a key is pressed while it's in focus. The text area will only invoke this method when you're typing in it. With that said, we only need to act when the TAB key is pressed. All the other keys are ignored. To do that, we have to filter the TAB by using the its ASCII decimal value: 9, as highlighted in the following code fragment.

KeyDownHandler handler = new KeyDownHandler() {
  @Override
  public void onKeyDown(KeyDownEvent event) {
    if (event.getNativeKeyCode() == 9) {
      // Handle TAB key down event here
    }
  }
}

The if statement will only yield true when the TAB key is pressed, otherwise false

Prevent the default action and stop event propagation

By default, when the TAB key is presses, the focus is transferred from the UI component that has it to the next com.google.gwt.user.client.ui.Focusable component. We have to prevent the default behaviour by invoking the methods preventDefault() and stopPropagation() on the KeyDownEvent instance provided as a parameter to the onKeyDown() method.

KeyDownHandler handler = new KeyDownHandler() {
  @Override
  public void onKeyDown(KeyDownEvent event) {
    if (event.getNativeKeyCode() == 9) {
      event.preventDefault();
      event.stopPropagation();
    }
  }
}

The above code will capture the TAB key events and will prevent the text area from losing its focus, but unfortunately will not print the TAB key in the text area. We have to programmatically add this key to the text area text as shown in the following section

Insert the tab character at the current cursor position

This should be the least of our problems but one must pay attention. Remember that the user may be editing in the middle of the document so we cannot just append the TAB at the end. We have to insert the TAB just right where the cursor is as highlighted in the following code fragment.

KeyDownHandler handler = new KeyDownHandler() {
  @Override
  public final void onKeyDown(KeyDownEvent event) {
    if (event.getNativeKeyCode() == 9) {
      event.preventDefault();
      event.stopPropagation();
      if(event.getSource() instanceof TextArea) {
        TextArea ta = (TextArea) event.getSource();
        int index = ta.getCursorPos();
        String text = ta.getText();
        ta.setText(text.substring(0, index) 
                   + "\t" + text.substring(index));
        ta.setCursorPos(index + 1);
      }
    }
  }
};

This can be easier said than done for some. So let's start line by line. We're first checking that the source is of type TextArea. We don't have to do this, but it will prevent an exception should someone else use this handler with another UI component that text area. Next, we're casting the source which triggered the event into a TextArea. Then we retrieved the cursor position (as an integer) and all its text in the line following that. Now, we split the text retrieved in two parts: the part before the cursor and the part following the cursor. We create a new string made from, the both parts delimited with the TAB character with is attaching both parts shown below.

ta.setText(text.substring(0, index) 
           + "\t" + text.substring(index));

Finally, we set the cursor location just after the TAB key, that is, one up in respect to the cursor before the key was pressed.

The Example

Complete example

public static final KeyDownHandler DEFAULT_TEXTAREA_TAB_HANDLER = new KeyDownHandler() {
    @Override
    public final void onKeyDown(KeyDownEvent event) {
      if (event.getNativeKeyCode() == 9) {
        event.preventDefault();
        event.stopPropagation();
        final TextArea ta = (TextArea) event.getSource();
        final int index = ta.getCursorPos();
        final String text = ta.getText();
        ta.setText(text.substring(0, index) 
                   + "\t" + text.substring(index));
        ta.setCursorPos(index + 1);
      }
    }
  };

Use

final TextArea textArea = new TextArea();
textArea.addKeyDownHandler(DEFAULT_TEXTAREA_TAB_HANDLER);