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}