001package com.github.gwtbootstrap.client.ui;
002
003import com.github.gwtbootstrap.client.ui.base.MarkupWidget;
004import com.github.gwtbootstrap.client.ui.base.TextBoxBase;
005import com.google.gwt.core.client.JavaScriptObject;
006import com.google.gwt.core.client.JsArrayString;
007import com.google.gwt.core.client.Scheduler;
008import com.google.gwt.core.client.Scheduler.ScheduledCommand;
009import com.google.gwt.user.client.Element;
010import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
011import com.google.gwt.user.client.ui.SuggestOracle;
012import com.google.gwt.user.client.ui.SuggestOracle.Callback;
013import com.google.gwt.user.client.ui.SuggestOracle.Request;
014import com.google.gwt.user.client.ui.SuggestOracle.Response;
015import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
016import com.google.gwt.user.client.ui.Widget;
017import java.util.Collection;
018
019public class Typeahead extends MarkupWidget {
020
021    public interface UpdaterCallback {
022        String onSelection(Suggestion selectedSuggestion);
023    }
024
025    public interface HighlighterCallback {
026        String highlight(String item);
027    }
028
029    public interface MatcherCallback {
030        boolean compareQueryToItem(String query, String item);
031    }
032
033    private int displayItems = 8;
034
035    private int minLength = 1;
036
037    private final SuggestOracle oracle;
038    private Collection<? extends Suggestion> suggestions;
039
040    private UpdaterCallback updaterCallback;
041    private HighlighterCallback highlighterCallback;
042    private MatcherCallback matcherCallback;
043
044    /**
045     * Constructor for {@link Typeahead}. Creates a {@link MultiWordSuggestOracle} to use with this
046     */
047    public Typeahead() {
048        this(new MultiWordSuggestOracle());
049    }
050
051    /**
052     * Constructor for {@link Typeahead}.
053     *
054     * @param oracle the oracle for this <code>Typeahead</code>
055     */
056    public Typeahead(SuggestOracle oracle) {
057        this.oracle = oracle;
058        this.updaterCallback = createDefaultUpdaterCallback();
059        this.highlighterCallback = createDefaultHighlighterCallback();
060        this.matcherCallback = createDefaultMatcherCallback();
061    }
062
063    private UpdaterCallback createDefaultUpdaterCallback() {
064        return new UpdaterCallback() {
065            @Override
066            public String onSelection(Suggestion selectedSuggestion) {
067                return selectedSuggestion.getReplacementString();
068            }
069        };
070    }
071
072    private HighlighterCallback createDefaultHighlighterCallback() {
073        return new HighlighterCallback() {
074            @Override
075            public String highlight(String item) {
076                return item;
077            }
078        };
079    }
080
081    private MatcherCallback createDefaultMatcherCallback() {
082        return new MatcherCallback() {
083            @Override
084            public boolean compareQueryToItem(String query, String item) {
085                return item.toLowerCase().contains(query.toLowerCase());
086            }
087        };
088    }
089
090    /**
091     * {@inheritDoc}
092     */
093    @Override
094    public void setWidget(Widget w) {
095
096        if (!(w instanceof TextBoxBase || w instanceof com.google.gwt.user.client.ui.TextBoxBase)) {
097            throw new IllegalArgumentException("Typeahead should be set TextBoxBase childs");
098        }
099
100        super.setWidget(w);
101    }
102
103    @Override
104    public Widget asWidget() {
105
106        if (widget != null) {
107            Scheduler.get().scheduleDeferred(new ScheduledCommand() {
108                @Override
109                public void execute() {
110                    reconfigure();
111                }
112            });
113
114        }
115
116        return super.asWidget();
117    }
118
119    /**
120     * reconfigure setting.
121     */
122    public void reconfigure() {
123
124        if (widget == null) {
125            return;
126        }
127
128        removeDataIfExists(widget.getElement());
129
130        configure(widget.getElement(), displayItems, minLength);
131    }
132
133    /**
134     * Get the max number of items to display in the dropdown.
135     *
136     * @return the max number of items to display in the dropdown
137     */
138    public int getDisplayItemCount() {
139        return displayItems;
140    }
141
142    /**
143     * Set max number of items to display in the dropdown.
144     *
145     * @param displayItems the max number of items to display in the dropdown
146     */
147    public void setDisplayItemCount(int displayItems) {
148        this.displayItems = displayItems;
149    }
150
151    /**
152     * Get the minimum character length needed before triggering autocomplete suggestions.
153     *
154     * @return the minimum character length needed before triggering autocomplete suggestions
155     */
156    public int getMinLength() {
157        return minLength;
158    }
159
160    /**
161     * Set the minimum character length needed before triggering autocomplete suggestions.
162     *
163     * @param minLength The minimum character length needed before triggering autocomplete suggestions.
164     */
165    public void setMinLength(int minLength) {
166        this.minLength = minLength;
167    }
168
169    public void setUpdaterCallback(UpdaterCallback updaterCallback) {
170        this.updaterCallback = updaterCallback;
171    }
172
173    public void setHighlighterCallback(
174            HighlighterCallback highlighterCallback) {
175        this.highlighterCallback = highlighterCallback;
176    }
177
178    public void setMatcherCallback(MatcherCallback matcherCallback) {
179        this.matcherCallback = matcherCallback;
180    }
181
182    /**
183     * Get suggest oracle
184     *
185     * @return oracle
186     */
187    public SuggestOracle getSuggestOracle() {
188        return oracle;
189    }
190
191    private void query(String query, final JavaScriptObject process) {
192        Callback callback = new Callback() {
193            @Override
194            public void onSuggestionsReady(Request request, Response response) {
195                callback(process, response);
196            }
197        };
198
199        if (query != null && !query.isEmpty()) {
200            oracle.requestSuggestions(new Request(query, displayItems), callback);
201        } else {
202            oracle.requestDefaultSuggestions(new Request(), callback);
203        }
204    }
205
206    private void callback(final JavaScriptObject process, Response response) {
207        suggestions = response.getSuggestions();
208
209        JsArrayString jsArrayString = JavaScriptObject.createArray().cast();
210
211        for (Suggestion suggestion : suggestions) {
212            jsArrayString.push(suggestion.getDisplayString());
213        }
214        callProcess(jsArrayString, process);
215    }
216
217    private String updater(String item) {
218        for (Suggestion suggestion : suggestions) {
219            if (suggestion.getDisplayString().equals(item)) {
220                return this.updaterCallback.onSelection(suggestion);
221            }
222        }
223
224        return item;
225    }
226
227    private String highlighter(String item) {
228        return this.highlighterCallback.highlight(item);
229    }
230
231    private boolean selectionMatcher(String query, String item) {
232        return this.matcherCallback.compareQueryToItem(query, item);
233    }
234    
235    //@formatter:off
236    private native void callProcess(JsArrayString items ,JavaScriptObject process) /*-{
237        process(items);
238    }-*/;
239
240    private native void removeDataIfExists(Element element) /*-{
241        var $this = $wnd.jQuery(element);
242        $this.data("typeahead") && $this.removeData("typeahead");
243    }-*/;
244    
245    public native void configure(Element element,int items, int minLength) /*-{
246        var that = this;
247        $wnd.jQuery(element).typeahead({
248            "source" : function(query , process) {
249                that.@com.github.gwtbootstrap.client.ui.Typeahead::query(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(query , process);
250            },
251            "items" : items,
252            "minLength" : minLength,
253            "updater" : function(item) {
254                return that.@com.github.gwtbootstrap.client.ui.Typeahead::updater(Ljava/lang/String;)(item);
255            },
256            "highlighter" : function(item) {
257                return that.@com.github.gwtbootstrap.client.ui.Typeahead::highlighter(Ljava/lang/String;)(item);
258            },
259            "matcher" : function(item) {
260                query = this.query;
261                return that.@com.github.gwtbootstrap.client.ui.Typeahead::selectionMatcher(Ljava/lang/String;Ljava/lang/String;)(query, item);
262            }
263        });
264    }-*/;
265    //@formatter:on
266}