Showing posts with label google web toolkit. Show all posts
Showing posts with label google web toolkit. Show all posts

Friday, September 19, 2014

GWT Image Cropper

In a previous project where the client was GWT based (in combination with GWTP and Errai) we needed an image cropping component. One of the standard widgets you need in nearly every second project and which is not provided by GWT as a pre-baked UI component. The ones I found on open source repositories I didn't like too much for the lack of documentation, huge parts of spaghetti code, lots of functionalities we didn't need or the combination of all those reasons. Wrapping an existing JS component was not an option, we didn't want to depend on another library. So I decided to implement a GWT only based cropper which might come handy for someone else too.

The inspiration on how it should behave was coming from Jira, the issue tracker we just migrated to back then. I had a look at the responsible DOM elements and how the adapt upon user interaction and tried to implement the cropping behaviour the same way. The idea is to create an outer div showing the image as background and three child divs: one for the glass (area to crop), one for the selection (area to clip) and one for adding the four resize handles to the corner of the selection.

The ImageCropper class takes care of registering the needed handlers, loading the source image and adding the selection component. A LoadHandler is used to size the div showing the background image correctly. To make this work on IE the image needs to be visible otherwise we get 0 for the height and width.


The abstract ResizeHandle contains the base logic for styling the handles and capturing mouse events. The calculation of how the selection should be adapted (getScaledYDelta() and getScaledXDelta()) is delegated to the concrete implementations of the handlers which are instantiated in the SelectionChooser class.


Besides creating the handlers the SelectionChooser class is also responsible of creating the glass and the clipping area as well as adapting the sizes and positions of them on user input.


I didn't test the implementation on all different browsers and versions since the application was used only on Chrome and Firefox (lucky us...). It should though work on IE10 as well when I remember correctly.

On the back end side of things we used Thumbnailator to do the actual cropping. With its fluent API the process of manipulating images becomes straightforward compared to using Java's 2D API along with other techniques.



Friday, July 18, 2014

Parse URL with GWT

We’ve implemented a simple rich text editor that supports basic text formatting like bold, italic, lists and so on. We also wanted to provide the functionality to enter URLs that automatically are truncated and afterwards transformed into anchor elements. For example the URL http://www.youtube.com/watch?v=T0X9BcBd-I0 should be transformed into <a href="http://www.youtube.com/watch?v=T0X9BcBd-I0">www.youtube.com</a>. This improves the reading as well as the look of an user post or comment.

Since we can’t use any of the classes of java.util.regex the solution below only relies on the regex methods of String. In our editor the parsing is done continuously while the user types. Meaning it is possible that toMarkup already contains an anchor element that was replaced previously. Therefore, we must not replace that URL again or more general, we must not parse any URL already contained in an anchor tag. The last part of PATTERN accounts for that situation.

/**
 * Converts URLs into short form of url elements.
 * 
 * The shorter form of the URL will contain the subdomain, domain and top domain. Protocol (http(s) or
 * ftp) and path will be removed.
 * 
 * @author zubi
 * 
 */
public class LinkParser implements RichTextHTMLParser {
 
    private static final String BEFORE_DOMAIN = "\\b((https?|ftp)://)";
    private static final String PATH = "(:\\d+)?(/[-a-z0-9A-Z_:@&?=+,.!/~*'%#$]*)*";
    private static final String PATTERN =
            BEFORE_DOMAIN
                    + "?([a-z0-9](?:[-a-z0-9A-Z]*[a-z0-9])?\\.)+(com\\b|edu\\b|biz\\b|gov\\b|in(?:t|fo)\\b|mil\\b|net\\b|org\\b|[a-z][a-z]\\b)"
                    + PATH + "(?!((?!(?:))";
 
    @Override
    public String getHTML(String toMarkup) {
        String[] splits = toMarkup.split(PATTERN, 2);
        String result = toMarkup;
        while (splits.length > 1) {
            result = replaceUrl(result, splits);
            splits = result.split(PATTERN, 2);
        }
        if (result.matches(".+?" + PATTERN)) {
            int start = splits[0].length();
            result = result.replaceAll(PATTERN, getLink(result.substring(start)));
        }
        return result;
    }
 
    private String replaceUrl(String text, String[] parts) {
        int start = parts[0].length();
        int end = parts[0].length() + text.substring(parts[0].length()).lastIndexOf(parts[1]);
        String url = text.substring(start, end);
        return text.replaceFirst(PATTERN, getLink(url));
    }
 
    private String getLink(String url) {
        if (!url.matches(BEFORE_DOMAIN + ".*")) {
            url = "http://" + url;
        }
        return "" + getDomain(url) + "";
    }
 
    private String getDomain(String url) {
        String domain = url.replaceAll(BEFORE_DOMAIN, "").replaceAll(PATH, "");
        return domain;
    }
}

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.