001    package org.gwtbootstrap3.extras.typeahead.client.ui;
002    
003    /*
004     * #%L
005     * GwtBootstrap3
006     * %%
007     * Copyright (C) 2013 - 2014 GwtBootstrap3
008     * %%
009     * Licensed under the Apache License, Version 2.0 (the "License");
010     * you may not use this file except in compliance with the License.
011     * You may obtain a copy of the License at
012     * 
013     *      http://www.apache.org/licenses/LICENSE-2.0
014     * 
015     * Unless required by applicable law or agreed to in writing, software
016     * distributed under the License is distributed on an "AS IS" BASIS,
017     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018     * See the License for the specific language governing permissions and
019     * limitations under the License.
020     * #L%
021     */
022    
023    import com.google.gwt.core.client.JavaScriptObject;
024    import com.google.gwt.core.client.JsArray;
025    import com.google.gwt.dom.client.Element;
026    import com.google.gwt.event.shared.HandlerRegistration;
027    import com.google.gwt.user.client.Event;
028    import org.gwtbootstrap3.client.ui.TextBox;
029    import org.gwtbootstrap3.extras.typeahead.client.base.Dataset;
030    import org.gwtbootstrap3.extras.typeahead.client.base.Suggestion;
031    import org.gwtbootstrap3.extras.typeahead.client.events.*;
032    
033    import java.util.Arrays;
034    import java.util.Collection;
035    
036    /**
037     * Twitter typeahead.js
038     *
039     * https://github.com/twitter/typeahead.js
040     *
041     * @author Florian Kremser <florian.kremser@sage.com>
042     */
043    public class Typeahead<T> extends TextBox {
044        private Collection<? extends Dataset<T>> datasets;
045        private boolean highlight = false;
046        private boolean hint = true;
047        private int minLength = 1;
048    
049        public Typeahead() {
050        }
051    
052        /**
053         * A typeahead is composed of one or more datasets. When an end-user
054         * modifies the value of a typeahead, each dataset will attempt to
055         * render suggestions for the new value.
056         *
057         * @param dataset a dataset for providing suggestions
058         */
059        public Typeahead(final Dataset<T> dataset) {
060            setDatasets(dataset);
061        }
062    
063        public Typeahead(final Collection<? extends Dataset<T>> datasets) {
064            setDatasets(datasets);
065        }
066    
067        public void setDatasets(final Dataset<T> dataset) {
068            this.datasets = Arrays.asList(dataset);
069        }
070    
071        public void setDatasets(final Collection<? extends Dataset<T>> datasets) {
072            this.datasets = datasets;
073        }
074    
075        @Override
076        public void setValue(final String value, final boolean fireEvents) {
077            setValueNative(getElement(), value);
078            super.setValue(value, fireEvents);
079        }
080    
081        /**
082         * If {@code true}, when suggestions are rendered, pattern matches for the
083         * current query in text nodes will be wrapped in a {@code strong} element
084         * with the {@code tt-highlight} class. Defaults to {@code false}.
085         *
086         * @param highlight {@code true} to highlight pattern matches in suggestions
087         */
088        public void setHighlight(final boolean highlight) {
089            this.highlight = highlight;
090        }
091    
092        /**
093         * If {@code false}, the typeahead will not show a hint. Defaults to {@code true}.
094         *
095         * @param hint {@code true} to show a hint
096         */
097        public void setHint(final boolean hint) {
098            this.hint = hint;
099        }
100    
101        /**
102         * The minimum character length needed before suggestions start getting
103         * rendered. Defaults to 1.
104         *
105         * @param minLength minimum required input length for matching
106         */
107        public void setMinLength(final int minLength) {
108            this.minLength = minLength;
109        }
110    
111        public HandlerRegistration addTypeaheadOpenedHandler(final TypeaheadOpenedHandler<T> handler) {
112            return addHandler(handler, TypeaheadOpenedEvent.getType());
113        }
114    
115        public HandlerRegistration addTypeaheadClosedHandler(final TypeaheadClosedHandler<T> handler) {
116            return addHandler(handler, TypeaheadClosedEvent.getType());
117        }
118    
119        public HandlerRegistration addTypeaheadCursorChangededHandler(final TypeaheadCursorChangedHandler<T> handler) {
120            return addHandler(handler, TypeaheadCursorChangedEvent.getType());
121        }
122    
123        public HandlerRegistration addTypeaheadAutoCompletedHandler(final TypeaheadAutoCompletedHandler<T> handler) {
124            return addHandler(handler, TypeaheadAutoCompletedEvent.getType());
125        }
126    
127        public HandlerRegistration addTypeaheadSelectedHandler(final TypeaheadSelectedHandler<T> handler) {
128            return addHandler(handler, TypeaheadSelectedEvent.getType());
129        }
130    
131        /**
132         * Triggered when the dropdown menu of the typeahead is opened.
133         *
134         * @param event the event
135         */
136        private void onOpened(final Event event) {
137            TypeaheadOpenedEvent.fire(this, event);
138        }
139    
140        /**
141         * Triggered when the dropdown menu of the typeahead is closed.
142         *
143         * @param event the event
144         */
145        private void onClosed(final Event event) {
146            TypeaheadClosedEvent.fire(this, event);
147        }
148    
149        /**
150         * Triggered when the dropdown menu cursor is moved to a different suggestion.
151         *
152         * @param event the event
153         * @param suggestion the suggestion object
154         */
155        private void onCursorChanged(final Event event, final Suggestion<T> suggestion) {
156            TypeaheadCursorChangedEvent.fire(this, suggestion, event);
157        }
158    
159        /**
160         * Triggered when the query is autocompleted. Autocompleted means the query was changed to the hint.
161         *
162         * @param event the event
163         * @param suggestion the suggestion object
164         */
165        private void onAutoCompleted(final Event event, final Suggestion<T> suggestion) {
166            TypeaheadAutoCompletedEvent.fire(this, suggestion, event);
167        }
168    
169        /**
170         * Triggered when a suggestion from the dropdown menu is selected.
171         *
172         * @param event the event
173         * @param suggestion the suggestion object
174         */
175        private void onSelected(final Event event, final Suggestion<T> suggestion) {
176            TypeaheadSelectedEvent.fire(this, suggestion, event);
177        }
178    
179        public void reconfigure() {
180            remove(getElement());
181            configure();
182        }
183    
184        @Override
185        protected void onLoad() {
186            super.onLoad();
187            configure();
188        }
189    
190        @Override
191        protected void onUnload() {
192            super.onUnload();
193            remove(getElement());
194        }
195    
196        protected void configure() {
197            JsArray<JavaScriptObject> datasetJSOs = JsArray.createArray().cast();
198            for (Dataset<T> dataset : datasets) {
199                JavaScriptObject jso = toJSO(dataset);
200                datasetJSOs.push(jso);
201            }
202            configure(getElement(), highlight, hint, minLength, datasetJSOs);
203        }
204    
205        private native JavaScriptObject toJSO(Dataset<T> dataset) /*-{
206            var emptyTemplate = dataset.@org.gwtbootstrap3.extras.typeahead.client.base.Dataset::getEmptyTemplate()();
207            var headerTemplate = dataset.@org.gwtbootstrap3.extras.typeahead.client.base.Dataset::getHeaderTemplate()();
208            var footerTemplate = dataset.@org.gwtbootstrap3.extras.typeahead.client.base.Dataset::getFooterTemplate()();
209            var suggestionTemplate = dataset.@org.gwtbootstrap3.extras.typeahead.client.base.Dataset::getSuggestionTemplate()();
210    
211            var findMatches = function () {
212                return function (query, cb) {
213                    var scb = @org.gwtbootstrap3.extras.typeahead.client.base.SuggestionCallback::new(Lcom/google/gwt/core/client/JavaScriptObject;)(cb);
214                    return dataset.@org.gwtbootstrap3.extras.typeahead.client.base.Dataset::findMatches(Ljava/lang/String;Lorg/gwtbootstrap3/extras/typeahead/client/base/SuggestionCallback;)(query, scb);
215                };
216            };
217    
218            return {
219                name: dataset.@org.gwtbootstrap3.extras.typeahead.client.base.Dataset::name,
220                source: findMatches(),
221                templates: {
222                    empty: (emptyTemplate != null ? function (query) {
223                        return emptyTemplate.@org.gwtbootstrap3.extras.typeahead.client.base.Template::render()();
224                    } : undefined),
225                    header: (headerTemplate != null ? function (query) {
226                        return headerTemplate.@org.gwtbootstrap3.extras.typeahead.client.base.Template::render()();
227                    } : undefined),
228                    footer: (footerTemplate != null ? function (query) {
229                        return footerTemplate.@org.gwtbootstrap3.extras.typeahead.client.base.Template::render()();
230                    } : undefined),
231                    suggestion: (suggestionTemplate != null ? function (suggestion) {
232                        return suggestionTemplate.@org.gwtbootstrap3.extras.typeahead.client.base.SuggestionTemplate::render(Lorg/gwtbootstrap3/extras/typeahead/client/base/Suggestion;)(suggestion);
233                    } : undefined)
234                }
235    
236            };
237        }-*/;
238    
239        private native void configure(
240                Element e, boolean highlight, boolean hint, int minLength, JsArray<JavaScriptObject> datasets) /*-{
241            var that = this;
242            $wnd.jQuery(e).typeahead({
243                    highlight: highlight,
244                    hint: hint,
245                    minLength: minLength
246                },
247                datasets)
248                .on('typeahead:opened', function (e) {
249                    that.@org.gwtbootstrap3.extras.typeahead.client.ui.Typeahead::onOpened(Lcom/google/gwt/user/client/Event;)(e);
250                })
251                .on('typeahead:closed', function (e) {
252                    that.@org.gwtbootstrap3.extras.typeahead.client.ui.Typeahead::onClosed(Lcom/google/gwt/user/client/Event;)(e);
253                })
254                .on('typeahead:cursorchanged', function (e, value, datasetName) {
255                    that.@org.gwtbootstrap3.extras.typeahead.client.ui.Typeahead::onCursorChanged(Lcom/google/gwt/user/client/Event;Lorg/gwtbootstrap3/extras/typeahead/client/base/Suggestion;)(e, value);
256                })
257                .on('typeahead:autocompleted', function (e, value, datasetName) {
258                    that.@org.gwtbootstrap3.extras.typeahead.client.ui.Typeahead::onAutoCompleted(Lcom/google/gwt/user/client/Event;Lorg/gwtbootstrap3/extras/typeahead/client/base/Suggestion;)(e, value);
259                })
260                .on('typeahead:selected', function (e, value, datasetName) {
261                    that.@org.gwtbootstrap3.extras.typeahead.client.ui.Typeahead::onSelected(Lcom/google/gwt/user/client/Event;Lorg/gwtbootstrap3/extras/typeahead/client/base/Suggestion;)(e, value);
262                });
263        }-*/;
264    
265        private native void remove(Element e) /*-{
266            $wnd.jQuery(e).typeahead('destroy');
267        }-*/;
268    
269        private native void setValueNative(Element e, String value) /*-{
270            $wnd.jQuery(e).typeahead('val', value);
271        }-*/;
272    }