001/*
002 * #%L
003 * GwtMaterial
004 * %%
005 * Copyright (C) 2015 - 2017 GwtMaterialDesign
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 * 
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 * 
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package gwt.material.design.client.ui;
021
022import com.google.gwt.core.client.Scheduler;
023import com.google.gwt.event.dom.client.KeyCodes;
024import com.google.gwt.event.dom.client.KeyUpEvent;
025import com.google.gwt.event.dom.client.KeyUpHandler;
026import com.google.gwt.event.logical.shared.*;
027import com.google.gwt.event.shared.HandlerRegistration;
028import com.google.gwt.user.client.ui.TextBox;
029import gwt.material.design.client.base.HasActive;
030import gwt.material.design.client.base.HasSearchHandlers;
031import gwt.material.design.client.base.SearchObject;
032import gwt.material.design.client.constants.Color;
033import gwt.material.design.client.constants.CssName;
034import gwt.material.design.client.constants.IconType;
035import gwt.material.design.client.constants.InputType;
036import gwt.material.design.client.events.SearchFinishEvent;
037import gwt.material.design.client.events.SearchNoResultEvent;
038import gwt.material.design.client.ui.html.Label;
039
040import java.util.ArrayList;
041import java.util.List;
042
043import static gwt.material.design.jquery.client.api.JQuery.$;
044
045//@formatter:off
046
047/**
048 * Material Search is a value box component that returns a result based on your search
049 * <p>
050 * <p>
051 * <h3>UiBinder Usage:</h3>
052 * <pre>
053 * {@code
054 * <m:MaterialSearch placeholder="Sample"/>
055 * }
056 * </pre>
057 * <p>
058 * <h3>Populating the search result objects</h3>
059 * {@code
060 * <p>
061 * List<SearchObject> objects = new ArrayList<>();
062 * <p>
063 * private void onInitSearch() {
064 * objects.add(new SearchObject(IconType.POLYMER, "Pushpin", "#!pushpin"));
065 * objects.add(new SearchObject(IconType.POLYMER, "SideNavs", "#!sidenavs"));
066 * objects.add(new SearchObject(IconType.POLYMER, "Scrollspy", "#!scrollspy"));
067 * objects.add(new SearchObject(IconType.POLYMER, "Tabs", "#!tabs"));
068 * txtSearch.setListSearches(objects);
069 * }
070 * <p>
071 * }
072 * </p>
073 *
074 * @author kevzlou7979
075 * @author Ben Dol
076 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#navbar">Material Search</a>
077 * @see <a href="https://material.io/guidelines/patterns/search.html#">Material Design Specification</a>
078 */
079//@formatter:on
080public class MaterialSearch extends MaterialValueBox<String> implements HasOpenHandlers<String>, HasCloseHandlers<String>,
081        HasActive, HasSearchHandlers {
082
083    private Label label = new Label();
084    private MaterialIcon iconSearch = new MaterialIcon(IconType.SEARCH);
085    private MaterialIcon iconClose = new MaterialIcon(IconType.CLOSE);
086
087    /**
088     * The list of search objects added to MaterialSearchResult panel to
089     * display the lists of result items
090     */
091    private List<SearchObject> listSearches = new ArrayList<>();
092    /**
093     * Used to determine the selected searches while matching the keyword to result
094     */
095    private List<SearchObject> tempSearches = new ArrayList<>();
096    /**
097     * Panel to display the result items
098     */
099    private MaterialSearchResult searchResultPanel = new MaterialSearchResult();
100    /**
101     * Link selected to determine easily during the selection event (up / down key events)
102     */
103    private MaterialLink selectedLink;
104    /**
105     * Gets the selected object after Search Finish event
106     */
107    private SearchObject selectedObject;
108    /**
109     * -1 means that the selected index is not yet selected.
110     * It will increment or decrement once trigger by key up / down events
111     */
112    private int curSel = -1;
113    private boolean active;
114
115    public MaterialSearch() {
116        super(new TextBox());
117    }
118
119    public MaterialSearch(String placeholder) {
120        this();
121        setPlaceholder(placeholder);
122    }
123
124    public MaterialSearch(String placeholder, Color backgroundColor, Color iconColor, boolean active, int shadow) {
125        this(placeholder);
126        setBackgroundColor(backgroundColor);
127        setIconColor(iconColor);
128        setActive(active);
129        setShadow(shadow);
130    }
131
132    @Override
133    protected void onLoad() {
134        super.onLoad();
135
136        setType(InputType.SEARCH);
137        label.add(iconSearch);
138        label.getElement().setAttribute("for", "search");
139        add(label);
140        add(iconClose);
141
142        registerHandler(iconClose.addMouseDownHandler(mouseDownEvent -> CloseEvent.fire(MaterialSearch.this, getText())));
143
144        add(searchResultPanel);
145
146        // Add Key Up event to filter the searches
147        registerHandler(addKeyUpHandler(new KeyUpHandler() {
148            @Override
149            public void onKeyUp(KeyUpEvent event) {
150                String keyword = getText().toLowerCase();
151                // Clear the panel and temp objects
152                searchResultPanel.clear();
153                tempSearches.clear();
154
155                // Populate the search result items
156                for (final SearchObject obj : getListSearches()) {
157                    MaterialLink link = new MaterialLink();
158                    link.setIconColor(Color.GREY);
159                    link.setTextColor(Color.BLACK);
160                    // Generate an icon
161                    if (obj.getIcon() != null) {
162                        link.setIconType(obj.getIcon());
163                    }
164
165                    // Generate an image
166                    MaterialImage image = new MaterialImage();
167                    if (obj.getResource() != null) {
168                        image.setResource(obj.getResource());
169                        link.insert(image, 0);
170                    }
171
172                    if (obj.getImageUrl() != null) {
173                        image.setUrl(obj.getImageUrl());
174                        link.insert(image, 0);
175                    }
176
177                    if (!obj.getLink().isEmpty()) {
178                        link.setHref(obj.getLink());
179                    }
180                    link.setText(obj.getKeyword());
181                    link.addClickHandler(event1 -> {
182                        setSelectedObject(obj);
183                        reset(obj.getKeyword());
184                    });
185                    // If matches add to search result container and object to temp searches
186                    if (obj.getKeyword().toLowerCase().contains(keyword)) {
187                        searchResultPanel.add(link);
188                        tempSearches.add(obj);
189                    }
190                }
191
192                // Apply selected search
193                if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER && !tempSearches.isEmpty()) {
194                    if (getCurSel() == -1) {
195                        setSelectedObject(tempSearches.get(0));
196                        setSelectedLink((MaterialLink) searchResultPanel.getWidget(0));
197                    } else {
198                        setSelectedObject(tempSearches.get(curSel));
199                    }
200
201                    MaterialLink selLink = getSelectedLink();
202                    if (!selLink.getHref().isEmpty()) {
203                        locateSearch(selLink.getHref());
204                    }
205                    reset(selLink.getText());
206                }
207
208                // Fire an event if there's no search result
209                if (searchResultPanel.getWidgetCount() == 0) {
210                    SearchNoResultEvent.fire(MaterialSearch.this);
211                }
212
213                // Selection logic using key down event to navigate the search results
214                int totalItems = searchResultPanel.getWidgetCount();
215                if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_DOWN) {
216                    if (curSel >= totalItems) {
217                        setCurSel(getCurSel());
218                        applyHighlightedItem((MaterialLink) searchResultPanel.getWidget(curSel - 1));
219                    } else {
220                        setCurSel(getCurSel() + 1);
221                        applyHighlightedItem((MaterialLink) searchResultPanel.getWidget(curSel));
222                    }
223                }
224
225                // Selection logic using key up event to navigate the search results
226                if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_UP) {
227                    if (curSel <= -1) {
228                        setCurSel(-1);
229                        applyHighlightedItem((MaterialLink) searchResultPanel.getWidget(curSel));
230                    } else {
231                        setCurSel(getCurSel() - 1);
232                        applyHighlightedItem((MaterialLink) searchResultPanel.getWidget(curSel));
233                    }
234                }
235            }
236
237            // Resets the search result panel
238            private void reset(String keyword) {
239                SearchFinishEvent.fire(MaterialSearch.this);
240                curSel = -1;
241                setText(keyword);
242                $(valueBoxBase.getElement()).focus();
243                searchResultPanel.clear();
244            }
245        }));
246    }
247
248    @Override
249    protected void onUnload() {
250        super.onUnload();
251
252        clear();
253        setCurSel(-1);
254    }
255
256    /**
257     * Programmatically open the search input field component
258     */
259    public void open() {
260        setActive(true);
261        Scheduler.get().scheduleDeferred(() -> $(valueBoxBase.getElement()).focus());
262        OpenEvent.fire(MaterialSearch.this, getText());
263    }
264
265    public void close() {
266        setActive(false);
267        Scheduler.get().scheduleDeferred(() -> $(valueBoxBase.getElement()).blur());
268        CloseEvent.fire(MaterialSearch.this, getText());
269    }
270
271    protected void applyHighlightedItem(MaterialLink link) {
272        link.addStyleName(CssName.HIGLIGHTED);
273        setSelectedLink(link);
274    }
275
276    protected native void locateSearch(String location)/*-{
277        $wnd.window.location.hash = location;
278    }-*/;
279
280    @Override
281    public void setActive(boolean active) {
282        this.active = active;
283        if (active) {
284            setTextColor(Color.BLACK);
285            iconClose.setIconColor(Color.BLACK);
286            iconSearch.setIconColor(Color.BLACK);
287        } else {
288            iconClose.setIconColor(Color.WHITE);
289            iconSearch.setIconColor(Color.WHITE);
290        }
291    }
292
293    @Override
294    public boolean isActive() {
295        return active;
296    }
297
298    public MaterialLink getSelectedLink() {
299        return selectedLink;
300    }
301
302    public void setSelectedLink(MaterialLink selectedLink) {
303        this.selectedLink = selectedLink;
304    }
305
306    public List<SearchObject> getListSearches() {
307        return listSearches;
308    }
309
310    public void setListSearches(List<SearchObject> listSearches) {
311        this.listSearches = listSearches;
312    }
313
314    public int getCurSel() {
315        return curSel;
316    }
317
318    public void setCurSel(int curSel) {
319        this.curSel = curSel;
320    }
321
322    public SearchObject getSelectedObject() {
323        return selectedObject;
324    }
325
326    public void setSelectedObject(SearchObject selectedObject) {
327        this.selectedObject = selectedObject;
328    }
329
330    /**
331     * Gets the temporary search objects.
332     */
333    public List<SearchObject> getTempSearches() {
334        return tempSearches;
335    }
336
337    public MaterialIcon getIconClose() {
338        return iconClose;
339    }
340
341    public MaterialSearchResult getSearchResultPanel() {
342        return searchResultPanel;
343    }
344
345    @Override
346    public Label getLabel() {
347        return label;
348    }
349
350    public MaterialIcon getIconSearch() {
351        return iconSearch;
352    }
353
354    @Override
355    public HandlerRegistration addCloseHandler(final CloseHandler<String> handler) {
356        return addHandler((CloseHandler<String>) handler::onClose, CloseEvent.getType());
357    }
358
359    @Override
360    public HandlerRegistration addOpenHandler(OpenHandler<String> handler) {
361        return addHandler((OpenHandler<String>) handler::onOpen, OpenEvent.getType());
362    }
363
364    /**
365     * This handler will be triggered when search is finish
366     */
367    @Override
368    public HandlerRegistration addSearchFinishHandler(final SearchFinishEvent.SearchFinishHandler handler) {
369        return addHandler(handler, SearchFinishEvent.TYPE);
370    }
371
372    /**
373     * This handler will be triggered when there's no search result
374     */
375    @Override
376    public HandlerRegistration addSearchNoResultHandler(final SearchNoResultEvent.SearchNoResultHandler handler) {
377        return addHandler(handler, SearchNoResultEvent.TYPE);
378    }
379}