Thursday, July 17, 2014

Autogrow TextArea with GWT

This post is about a GWT only implementation of an autogrow textarea. That is, a textarea that automatically grows and shrinks vertically according to it’s content (best known of Facebook when writing comments or messages).

In our solution we calculate the needed height of the textarea upon every KeyUpEvent and KeyDownEvent. The KeyDownEvent is needed if a user keeps pressing a button. In this case only one KeyUpEvent is fired at the time when the key is released. Though the textarea won’t adjust its size during the pressing.

To calculate the size to fit the current content we use the following shrink() and grow() methods:

private void grow() {
    while (getElement().getScrollHeight() > getElement().getClientHeight()) {
        setVisibleLines(getVisibleLines() + fGrowLines);
    }
}

private void shrink() {
    int rows = getVisibleLines();
    while (rows > fInitialLines) {
        setVisibleLines(--rows);
    }
}

Where fInitialLines is the minimum size (in lines) of the textarea and fGrowLines are the number of lines the textarea will grow. Whenever one of the mentioned key events occurs we run both methods to adjust the size.

One last thing we took account of was cutting and pasting when triggered via right mouse click or the menu. We therefore sink two more events: ONPASTE and ONCUT. Whereas ONPASTE is already supported by GWT we have to use JSNI to register for ONCUT events (there’s an open issue for it).

private native void registerOnCut(Element element) /*-{
    var that = this.@com.ambitz.everest.gwt.client.widget.autogrowtextarea.AutoGrowTextArea::fSizeHandler;
    element.oncut = $entry(function() {
    that.@com.ambitz.everest.gwt.client.widget.autogrowtextarea.AutoGrowTextArea.SizeHandler::shrink()();
    return false;
    });
}-*/;

Here’s the complete class:

import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.TextArea;
 
/**
 * A {@link TextArea} that automatically grows and shrinks depending on its content.
 * 
 * @author zubi
 * 
 */
public class AutoGrowTextArea extends TextArea {
 
    private class SizeHandler implements KeyUpHandler, KeyPressHandler, BlurHandler {
 
        @Override
        public void onKeyUp(KeyUpEvent event) {
            size();
        }
 
        public void size() {
            shrink();
            grow();
        }
 
        private void grow() {
            while (getElement().getScrollHeight() > getElement().getClientHeight()) {
                setVisibleLines(getVisibleLines() + fGrowLines);
            }
        }
 
        private void shrink() {
            int rows = getVisibleLines();
            while (rows > fInitialLines) {
                setVisibleLines(--rows);
            }
        }
 
        @Override
        public void onKeyPress(KeyPressEvent event) {
            size();
        }
 
        @Override
        public void onBlur(BlurEvent event) {
            size();
        }
    }
 
    private int fInitialLines;
    private int fGrowLines;
    private SizeHandler fSizeHandler;
 
    /**
     * Creates new text area. Initial number of lines is set to 2, grow lines is set to 1 (see
     * {@link #AutoGrowTextArea(int, int)}.
     */
    public AutoGrowTextArea() {
        this(2, 1);
    }
 
    /**
     * Creates new text area.
     * 
     * @param initialLines
     *            how high in terms of visible lines the initial text box is (the height will never go below this
     *            number)
     * @param growLines
     *            how many lines the text box grows when content reaches the current last line
     */
    public AutoGrowTextArea(int initialLines, int growLines) {
        super();
        registerHandlers();
        adjustStyle();
        setVisibleLines(initialLines);
        fInitialLines = initialLines;
        fGrowLines = growLines;
    }
 
    private void adjustStyle() {
        getElement().getStyle().setOverflow(Overflow.HIDDEN);
        getElement().getStyle().setProperty("resize", "none");
    }
 
    private void registerHandlers() {
        fSizeHandler = new SizeHandler();
        addKeyUpHandler(fSizeHandler);
        addKeyPressHandler(fSizeHandler);
        addBlurHandler(fSizeHandler);
        sinkEvents(Event.ONPASTE);
        registerOnCut(getElement());
    }
 
    private native void registerOnCut(Element element) /*-{
        var that = this.@com.ambitz.everest.gwt.client.widget.autogrowtextarea.AutoGrowTextArea::fSizeHandler;
        element.oncut = $entry(function() {
        that.@com.ambitz.everest.gwt.client.widget.autogrowtextarea.AutoGrowTextArea.SizeHandler::shrink()();
        return false;
        });
    }-*/;
 
    @Override
    public void onBrowserEvent(Event event) {
        super.onBrowserEvent(event);
        switch (DOM.eventGetType(event)) {
            case Event.ONPASTE:
                Scheduler.get().scheduleDeferred(new ScheduledCommand() {
 
                    @Override
                    public void execute() {
                        fSizeHandler.size();
                    }
 
                });
                break;
        }
    }
}
Edit:
As mentioned by Mike (thanks for that!) you shouldn’t set a height on the textarea. Otherwise setVisibleLines() won’t work.

No comments:

Post a Comment