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.addins.client.autocomplete; 021 022import com.google.gwt.dom.client.Document; 023import com.google.gwt.event.dom.client.*; 024import com.google.gwt.event.logical.shared.*; 025import com.google.gwt.event.shared.HandlerRegistration; 026import com.google.gwt.user.client.DOM; 027import com.google.gwt.user.client.ui.*; 028import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; 029import gwt.material.design.addins.client.MaterialAddins; 030import gwt.material.design.addins.client.autocomplete.constants.AutocompleteType; 031import gwt.material.design.addins.client.base.constants.AddinsCssName; 032import gwt.material.design.client.MaterialDesignBase; 033import gwt.material.design.client.base.*; 034import gwt.material.design.client.base.mixin.*; 035import gwt.material.design.client.constants.CssName; 036import gwt.material.design.client.constants.IconType; 037import gwt.material.design.client.constants.ProgressType; 038import gwt.material.design.client.ui.MaterialChip; 039import gwt.material.design.client.ui.MaterialLabel; 040import gwt.material.design.client.ui.MaterialProgress; 041import gwt.material.design.client.ui.html.Label; 042import gwt.material.design.client.ui.html.ListItem; 043import gwt.material.design.client.ui.html.UnorderedList; 044 045import java.util.*; 046import java.util.Map.Entry; 047 048//@formatter:off 049 050/** 051 * <p> 052 * Use GWT Autocomplete to search for matches from local or remote data sources. 053 * We used MultiWordSuggestOracle to populate the list to be added on the 054 * autocomplete values. 055 * </p> 056 * <p> 057 * <h3>XML Namespace Declaration</h3> 058 * <pre> 059 * {@code 060 * xmlns:ma='urn:import:gwt.material.design.addins.client' 061 * } 062 * </pre> 063 * <p> 064 * <h3>UiBinder Usage:</h3> 065 * <pre> 066 * {@code 067 * <ma:autocomplete.MaterialAutoComplete ui:field="autocomplete" placeholder="States" />} 068 * </pre> 069 * <p> 070 * <h3>Java Usage:</h3> 071 * <p> 072 * <p> 073 * To use your domain object inside the MaterialAutoComplete, for example, an object 074 * "User", you can subclass the {@link gwt.material.design.addins.client.autocomplete.base.MaterialSuggestionOracle} and {@link Suggestion}, like this: 075 * </p> 076 * <p><pre> 077 * public class UserOracle extends MaterialSuggestionOracle { 078 * private List<User> contacts = new LinkedList<>(); 079 * <p> 080 * public void addContacts(List<User> users){ 081 * contacts.addAll(users); 082 * } 083 * <p> 084 * {@literal @}Override 085 * public void requestSuggestions(final Request request, final Callback callback) { 086 * Response resp = new Response(); 087 * if (contacts.isEmpty()){ 088 * callback.onSuggestionsReady(request, resp); 089 * return; 090 * } 091 * String text = request.getQuery(); 092 * text = text.toLowerCase(); 093 * <p> 094 * List<UserSuggestion> list = new ArrayList<>(); 095 * <p> 096 * /{@literal *} 097 * {@literal *} Finds the contacts that meets the criteria. Note that since the 098 * {@literal *} requestSuggestions method is asynchronous, you can fetch the 099 * {@literal *} results from the server instead of using a local contacts List. 100 * {@literal *}/ 101 * for (User contact : contacts){ 102 * if (contact.getName().toLowerCase().contains(text)){ 103 * list.add(new UserSuggestion(contact)); 104 * } 105 * } 106 * resp.setSuggestions(list); 107 * callback.onSuggestionsReady(request, resp); 108 * } 109 * } 110 * <p> 111 * public class UserSuggestion implements SuggestOracle.Suggestion { 112 * <p> 113 * private User user; 114 * <p> 115 * public UserSuggestion(User user){ 116 * this.user = user; 117 * } 118 * <p> 119 * {@literal @}Override 120 * public String getDisplayString() { 121 * return getReplacementString(); 122 * } 123 * <p> 124 * {@literal @}Override 125 * public String getReplacementString() { 126 * return user.getName(); 127 * } 128 * <p> 129 * public User getUser() { 130 * return user; 131 * } 132 * } 133 * </pre></p> 134 * <p> 135 * And then use the UserOracle like this: 136 * </p> 137 * <p><pre> 138 * //Constructor 139 * MaterialAutoComplete userAutoComplete = new MaterialAutoComplete(new UserOracle()); 140 * <p> 141 * //How to get the selected User objects 142 * public List<User> getSelectedUsers(){ 143 * List<? extends Suggestion> values = userAutoComplete.getValue(); 144 * List<User> users = new ArrayList<>(values.size()); 145 * for (Suggestion value : values) { 146 * if (value instanceof UserSuggestion){ 147 * UserSuggestion us = (UserSuggestion) value; 148 * User user = us.getUser(); 149 * users.add(user); 150 * } 151 * } 152 * return users; 153 * } 154 * </pre></p> 155 * 156 * @author kevzlou7979 157 * @author gilberto-torrezan 158 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#!autocomplete">Material AutoComplete</a> 159 */ 160// @formatter:on 161public class MaterialAutoComplete extends AbstractValueWidget<List<? extends Suggestion>> implements HasPlaceholder, 162 HasProgress, HasType<AutocompleteType>, HasSelectionHandlers<Suggestion>, HasReadOnly { 163 164 static { 165 if (MaterialAddins.isDebug()) { 166 MaterialDesignBase.injectCss(MaterialAutocompleteDebugClientBundle.INSTANCE.autocompleteCssDebug()); 167 } else { 168 MaterialDesignBase.injectCss(MaterialAutocompleteClientBundle.INSTANCE.autocompleteCss()); 169 } 170 } 171 172 private int limit = 0; 173 private boolean directInputAllowed = true; 174 private String selectedChipStyle = "blue white-text"; 175 private Map<Suggestion, Widget> suggestionMap = new LinkedHashMap<>(); 176 private Label label = new Label(); 177 private List<ListItem> itemsHighlighted = new ArrayList<>(); 178 private FlowPanel panel = new FlowPanel(); 179 private UnorderedList list = new UnorderedList(); 180 private SuggestOracle suggestions; 181 private TextBox itemBox = new TextBox(); 182 private SuggestBox suggestBox = new SuggestBox(); 183 private MaterialLabel errorLabel = new MaterialLabel(); 184 private MaterialChipProvider chipProvider = new DefaultMaterialChipProvider(); 185 186 private ErrorMixin<AbstractValueWidget, MaterialLabel> errorMixin; 187 private ProgressMixin<MaterialAutoComplete> progressMixin; 188 private FocusableMixin<MaterialWidget> focusableMixin; 189 private ReadOnlyMixin<MaterialAutoComplete, TextBox> readOnlyMixin; 190 private CssTypeMixin<AutocompleteType, MaterialAutoComplete> typeMixin; 191 192 /** 193 * Use MaterialAutocomplete to search for matches from local or remote data 194 * sources. 195 */ 196 public MaterialAutoComplete() { 197 super(Document.get().createDivElement(), AddinsCssName.AUTOCOMPLETE, CssName.INPUT_FIELD); 198 add(panel); 199 } 200 201 public MaterialAutoComplete(AutocompleteType type) { 202 this(); 203 setType(type); 204 } 205 206 public MaterialAutoComplete(String placeholder) { 207 this(); 208 setPlaceholder(placeholder); 209 } 210 211 /** 212 * Use MaterialAutocomplete to search for matches from local or remote data 213 * sources. 214 * 215 * @see #setSuggestions(SuggestOracle) 216 */ 217 public MaterialAutoComplete(SuggestOracle suggestions) { 218 this(); 219 setup(suggestions); 220 } 221 222 private HandlerRegistration listHandler, itemBoxKeyDownHandler, itemBoxBlurHandler, itemBoxClickHandler; 223 224 @Override 225 protected void onLoad() { 226 super.onLoad(); 227 228 loadHandlers(); 229 } 230 231 protected void loadHandlers() { 232 listHandler = list.addDomHandler(event -> suggestBox.showSuggestionList(), ClickEvent.getType()); 233 234 itemBoxBlurHandler = itemBox.addBlurHandler(blurEvent -> { 235 if (getValue().size() > 0) { 236 label.addStyleName(CssName.ACTIVE); 237 } 238 }); 239 240 itemBoxKeyDownHandler = itemBox.addKeyDownHandler(event -> { 241 boolean changed = false; 242 243 switch (event.getNativeKeyCode()) { 244 case KeyCodes.KEY_ENTER: 245 if (directInputAllowed) { 246 String value = itemBox.getValue(); 247 if (value != null && !(value = value.trim()).isEmpty()) { 248 gwt.material.design.client.base.Suggestion directInput = new gwt.material.design.client.base.Suggestion(); 249 directInput.setDisplay(value); 250 directInput.setSuggestion(value); 251 changed = addItem(directInput); 252 if (getType() == AutocompleteType.TEXT) { 253 itemBox.setText(value); 254 } else { 255 itemBox.setValue(""); 256 } 257 itemBox.setFocus(true); 258 } 259 } 260 break; 261 case KeyCodes.KEY_BACKSPACE: 262 if (itemBox.getValue().trim().isEmpty()) { 263 if (itemsHighlighted.isEmpty()) { 264 if (suggestionMap.size() > 0) { 265 ListItem li = (ListItem) list.getWidget(list.getWidgetCount() - 2); 266 267 if (tryRemoveSuggestion(li.getWidget(0))) { 268 li.removeFromParent(); 269 changed = true; 270 } 271 } 272 } 273 } 274 case KeyCodes.KEY_DELETE: 275 if (itemBox.getValue().trim().isEmpty()) { 276 for (ListItem li : itemsHighlighted) { 277 if (tryRemoveSuggestion(li.getWidget(0))) { 278 li.removeFromParent(); 279 changed = true; 280 } 281 } 282 itemsHighlighted.clear(); 283 } 284 itemBox.setFocus(true); 285 break; 286 } 287 }); 288 289 itemBoxClickHandler = itemBox.addClickHandler(event -> suggestBox.showSuggestionList()); 290 } 291 292 @Override 293 protected void onUnload() { 294 super.onUnload(); 295 296 unloadHandlers(); 297 } 298 299 protected void unloadHandlers() { 300 removeHandler(listHandler); 301 removeHandler(itemBoxBlurHandler); 302 removeHandler(itemBoxKeyDownHandler); 303 removeHandler(itemBoxClickHandler); 304 } 305 306 /** 307 * Generate and build the List Items to be set on Auto Complete box. 308 */ 309 protected void setup(SuggestOracle suggestions) { 310 311 if (itemBoxKeyDownHandler != null) { 312 itemBoxKeyDownHandler.removeHandler(); 313 } 314 315 list.setStyleName(AddinsCssName.MULTIVALUESUGGESTBOX_LIST); 316 this.suggestions = suggestions; 317 final ListItem item = new ListItem(); 318 319 item.setStyleName(AddinsCssName.MULTIVALUESUGGESTBOX_INPUT_TOKEN); 320 321 suggestBox = new SuggestBox(suggestions, itemBox); 322 suggestBox.addSelectionHandler(selectionEvent -> { 323 Suggestion selectedItem = selectionEvent.getSelectedItem(); 324 itemBox.setValue(""); 325 if (addItem(selectedItem)) { 326 ValueChangeEvent.fire(MaterialAutoComplete.this, getValue()); 327 } 328 itemBox.setFocus(true); 329 }); 330 331 loadHandlers(); 332 333 setLimit(this.limit); 334 String autocompleteId = DOM.createUniqueId(); 335 itemBox.getElement().setId(autocompleteId); 336 337 item.add(suggestBox); 338 item.add(label); 339 list.add(item); 340 341 panel.add(list); 342 panel.getElement().setAttribute("onclick", 343 "document.getElementById('" + autocompleteId + "').focus()"); 344 panel.add(errorLabel); 345 suggestBox.setFocus(true); 346 } 347 348 protected boolean tryRemoveSuggestion(Widget widget) { 349 Set<Entry<Suggestion, Widget>> entrySet = suggestionMap.entrySet(); 350 for (Entry<Suggestion, Widget> entry : entrySet) { 351 if (widget.equals(entry.getValue())) { 352 if (chipProvider.isChipRemovable(entry.getKey())) { 353 suggestionMap.remove(entry.getKey()); 354 return true; 355 } 356 return false; 357 } 358 } 359 return false; 360 } 361 362 /** 363 * Adding the item value using Material Chips added on auto complete box 364 */ 365 protected boolean addItem(final Suggestion suggestion) { 366 SelectionEvent.fire(MaterialAutoComplete.this, suggestion); 367 if (getLimit() > 0) { 368 if (suggestionMap.size() >= getLimit()) { 369 return false; 370 } 371 } 372 373 if (suggestionMap.containsKey(suggestion)) { 374 return false; 375 } 376 377 final ListItem displayItem = new ListItem(); 378 displayItem.setStyleName(AddinsCssName.MULTIVALUESUGGESTBOX_TOKEN); 379 380 if (getType() == AutocompleteType.TEXT) { 381 suggestionMap.clear(); 382 itemBox.setText(suggestion.getReplacementString()); 383 } else { 384 final MaterialChip chip = chipProvider.getChip(suggestion); 385 if (chip == null) { 386 return false; 387 } 388 389 registerHandler(chip.addClickHandler(event -> { 390 if (chipProvider.isChipSelectable(suggestion)) { 391 if (itemsHighlighted.contains(displayItem)) { 392 chip.removeStyleName(selectedChipStyle); 393 itemsHighlighted.remove(displayItem); 394 } else { 395 chip.addStyleName(selectedChipStyle); 396 itemsHighlighted.add(displayItem); 397 } 398 } 399 })); 400 401 if (chip.getIcon() != null) { 402 registerHandler(chip.getIcon().addClickHandler(event -> { 403 if (chipProvider.isChipRemovable(suggestion)) { 404 suggestionMap.remove(suggestion); 405 list.remove(displayItem); 406 itemsHighlighted.remove(displayItem); 407 ValueChangeEvent.fire(MaterialAutoComplete.this, getValue()); 408 suggestBox.showSuggestionList(); 409 } 410 })); 411 } 412 413 suggestionMap.put(suggestion, chip); 414 displayItem.add(chip); 415 list.insert(displayItem, list.getWidgetCount() - 1); 416 } 417 return true; 418 } 419 420 /** 421 * Clear the chip items on the autocomplete box 422 */ 423 public void clear() { 424 itemBox.setValue(""); 425 label.removeStyleName(CssName.ACTIVE); 426 427 Collection<Widget> values = suggestionMap.values(); 428 for (Widget widget : values) { 429 Widget parent = widget.getParent(); 430 if (parent instanceof ListItem) { 431 parent.removeFromParent(); 432 } 433 } 434 suggestionMap.clear(); 435 436 clearErrorOrSuccess(); 437 } 438 439 @Override 440 public void showProgress(ProgressType type) { 441 getProgressMixin().showProgress(ProgressType.INDETERMINATE); 442 } 443 444 @Override 445 public void setPercent(double percent) { 446 getProgressMixin().setPercent(percent); 447 } 448 449 @Override 450 public void hideProgress() { 451 getProgressMixin().hideProgress(); 452 } 453 454 @Override 455 public MaterialProgress getProgress() { 456 return getProgressMixin().getProgress(); 457 } 458 459 /** 460 * @return the item values on autocomplete 461 * @see #getValue() 462 */ 463 public List<String> getItemValues() { 464 Set<Suggestion> keySet = suggestionMap.keySet(); 465 List<String> values = new ArrayList<>(keySet.size()); 466 for (Suggestion suggestion : keySet) { 467 values.add(suggestion.getReplacementString()); 468 } 469 return values; 470 } 471 472 /** 473 * @param itemValues the itemsSelected to set 474 * @see #setValue(Object) 475 */ 476 public void setItemValues(List<String> itemValues) { 477 setItemValues(itemValues, false); 478 } 479 480 /** 481 * @param itemValues the itemsSelected to set 482 * @param fireEvents will fire value change event if true 483 * @see #setValue(Object) 484 */ 485 public void setItemValues(List<String> itemValues, boolean fireEvents) { 486 if (itemValues == null) { 487 clear(); 488 return; 489 } 490 List<Suggestion> list = new ArrayList<>(itemValues.size()); 491 for (String value : itemValues) { 492 Suggestion suggestion = new gwt.material.design.client.base.Suggestion(value, value); 493 list.add(suggestion); 494 } 495 setValue(list, fireEvents); 496 if (itemValues.size() > 0) { 497 label.addStyleName(CssName.ACTIVE); 498 } 499 } 500 501 /** 502 * @return the itemsHighlighted 503 */ 504 public List<ListItem> getItemsHighlighted() { 505 return itemsHighlighted; 506 } 507 508 /** 509 * @param itemsHighlighted the itemsHighlighted to set 510 */ 511 public void setItemsHighlighted(List<ListItem> itemsHighlighted) { 512 this.itemsHighlighted = itemsHighlighted; 513 } 514 515 /** 516 * @return the suggestion oracle 517 */ 518 public SuggestOracle getSuggestions() { 519 return suggestions; 520 } 521 522 /** 523 * Sets the SuggestOracle to be used to provide suggestions. Also setups the 524 * component with the needed event handlers and UI elements. 525 * 526 * @param suggestions the suggestion oracle to set 527 */ 528 public void setSuggestions(SuggestOracle suggestions) { 529 this.suggestions = suggestions; 530 setup(suggestions); 531 } 532 533 public void setSuggestions(SuggestOracle suggestions, AutocompleteType type) { 534 setType(type); 535 setSuggestions(suggestions); 536 } 537 538 public int getLimit() { 539 return limit; 540 } 541 542 public void setLimit(int limit) { 543 this.limit = limit; 544 if (this.suggestBox != null) { 545 this.suggestBox.setLimit(limit); 546 } 547 } 548 549 /** 550 * Set the number of suggestions to be displayed to the user. This differs from 551 * setLimit() which set both the suggestions displayed AND the limit of values 552 * allowed within the autocomplete. 553 * 554 * @param limit 555 */ 556 public void setAutoSuggestLimit(int limit) { 557 if (this.suggestBox != null) { 558 this.suggestBox.setLimit(limit); 559 } 560 } 561 562 @Override 563 public String getPlaceholder() { 564 return itemBox.getElement().getAttribute("placeholder"); 565 } 566 567 @Override 568 public void setPlaceholder(String placeholder) { 569 itemBox.getElement().setAttribute("placeholder", placeholder); 570 } 571 572 /** 573 * @param label 574 * @see gwt.material.design.client.ui.MaterialValueBox#setLabel(String) 575 */ 576 public void setLabel(String label) { 577 this.label.setText(label); 578 if (!getPlaceholder().isEmpty()) { 579 this.label.setStyleName(CssName.ACTIVE); 580 } 581 } 582 583 /** 584 * Gets the current {@link MaterialChipProvider}. By default, the class uses 585 * an instance of {@link DefaultMaterialChipProvider}. 586 */ 587 public MaterialChipProvider getChipProvider() { 588 return chipProvider; 589 } 590 591 /** 592 * Sets a {@link MaterialChipProvider} that can customize how the 593 * {@link MaterialChip} is created for each selected {@link Suggestion}. 594 */ 595 public void setChipProvider(MaterialChipProvider chipProvider) { 596 this.chipProvider = chipProvider; 597 } 598 599 /** 600 * When set to <code>false</code>, only {@link Suggestion}s from the 601 * SuggestionOracle are accepted. Direct input create by the user is 602 * ignored. By default, direct input is allowed. 603 */ 604 public void setDirectInputAllowed(boolean directInputAllowed) { 605 this.directInputAllowed = directInputAllowed; 606 } 607 608 /** 609 * @return if {@link Suggestion}s created by direct input from the user 610 * should be allowed. By default directInputAllowed is 611 * <code>true</code>. 612 */ 613 public boolean isDirectInputAllowed() { 614 return directInputAllowed; 615 } 616 617 /** 618 * Sets the style class applied to chips when they are selected. 619 * <p> 620 * Defaults to "blue white-text". 621 * </p> 622 * 623 * @param selectedChipStyle The class or classes to be applied to selected chips 624 */ 625 public void setSelectedChipStyle(String selectedChipStyle) { 626 this.selectedChipStyle = selectedChipStyle; 627 } 628 629 /** 630 * Returns the style class applied to chips when they are selected. 631 * <p> 632 * Defaults to "blue white-text". 633 * </p> 634 */ 635 public String getSelectedChipStyle() { 636 return selectedChipStyle; 637 } 638 639 @Override 640 public void setType(AutocompleteType type) { 641 getTypeMixin().setType(type); 642 } 643 644 @Override 645 public AutocompleteType getType() { 646 return getTypeMixin().getType(); 647 } 648 649 @Override 650 public void setReadOnly(boolean value) { 651 getReadOnlyMixin().setReadOnly(value); 652 if (value) { 653 setEnabled(false); 654 } 655 } 656 657 @Override 658 public boolean isReadOnly() { 659 return getReadOnlyMixin().isReadOnly(); 660 } 661 662 @Override 663 public void setToggleReadOnly(boolean toggle) { 664 getReadOnlyMixin().setToggleReadOnly(toggle); 665 } 666 667 @Override 668 public boolean isToggleReadOnly() { 669 return getReadOnlyMixin().isToggleReadOnly(); 670 } 671 672 /** 673 * Interface that defines how a {@link MaterialChip} is created, given a 674 * {@link Suggestion}. 675 * 676 * @see MaterialAutoComplete#setChipProvider(MaterialChipProvider) 677 */ 678 public interface MaterialChipProvider { 679 680 /** 681 * Creates and returns a {@link MaterialChip} based on the selected 682 * {@link Suggestion}. 683 * 684 * @param suggestion the selected {@link Suggestion} 685 * @return the created MaterialChip, or <code>null</code> if the 686 * suggestion should be ignored. 687 */ 688 MaterialChip getChip(Suggestion suggestion); 689 690 /** 691 * Returns whether the chip defined by the suggestion should be selected when the user clicks on it. 692 * <p> 693 * <p> 694 * Selecion of chips is used to batch remove suggestions, for example. 695 * </p> 696 * 697 * @param suggestion the selected {@link Suggestion} 698 * @see MaterialAutoComplete#setSelectedChipStyle(String) 699 */ 700 boolean isChipSelectable(Suggestion suggestion); 701 702 /** 703 * Returns whether the chip defined by the suggestion should be removed from the autocomplete when clicked on its icon. 704 * <p> 705 * <p> 706 * Override this method returning <code>false</code> to implement your own logic when the user clicks on the chip icon. 707 * </p> 708 * 709 * @param suggestion the selected {@link Suggestion} 710 */ 711 boolean isChipRemovable(Suggestion suggestion); 712 } 713 714 /** 715 * Default implementation of the {@link MaterialChipProvider} interface, 716 * used by the {@link MaterialAutoComplete}. 717 * <p> 718 * <p> 719 * By default all chips are selectable and removable. The default {@link IconType} used by the chips provided is the {@link IconType#CLOSE}. 720 * </p> 721 * 722 * @see MaterialAutoComplete#setChipProvider(MaterialChipProvider) 723 */ 724 public static class DefaultMaterialChipProvider implements MaterialChipProvider { 725 726 @Override 727 public MaterialChip getChip(Suggestion suggestion) { 728 final MaterialChip chip = new MaterialChip(); 729 730 String imageChip = suggestion.getDisplayString(); 731 String textChip = imageChip; 732 733 String s = "<img src=\""; 734 if (imageChip.contains(s)) { 735 int ix = imageChip.indexOf(s) + s.length(); 736 imageChip = imageChip.substring(ix, imageChip.indexOf("\"", ix + 1)); 737 chip.setUrl(imageChip); 738 textChip = textChip.replaceAll("[<](/)?img[^>]*[>]", ""); 739 } 740 chip.setText(textChip); 741 chip.setIconType(IconType.CLOSE); 742 743 return chip; 744 } 745 746 @Override 747 public boolean isChipRemovable(Suggestion suggestion) { 748 return true; 749 } 750 751 @Override 752 public boolean isChipSelectable(Suggestion suggestion) { 753 return true; 754 } 755 } 756 757 /** 758 * Returns the selected {@link Suggestion}s. Modifications to the list are 759 * not propagated to the component. 760 * 761 * @return the list of selected {@link Suggestion}s, or empty if none was 762 * selected (never <code>null</code>). 763 */ 764 @Override 765 public List<? extends Suggestion> getValue() { 766 return new ArrayList<>(suggestionMap.keySet()); 767 } 768 769 @Override 770 public void setValue(List<? extends Suggestion> value, boolean fireEvents) { 771 clear(); 772 if (value != null) { 773 label.addStyleName(CssName.ACTIVE); 774 for (Suggestion suggestion : value) { 775 addItem(suggestion); 776 } 777 } 778 super.setValue(value, fireEvents); 779 } 780 781 @Override 782 public void setEnabled(boolean enabled) { 783 super.setEnabled(enabled); 784 itemBox.setEnabled(enabled); 785 } 786 787 public Label getLabel() { 788 return label; 789 } 790 791 public TextBox getItemBox() { 792 return itemBox; 793 } 794 795 public MaterialLabel getErrorLabel() { 796 return errorLabel; 797 } 798 799 public SuggestBox getSuggestBox() { 800 return suggestBox; 801 } 802 803 @Override 804 public HandlerRegistration addKeyUpHandler(final KeyUpHandler handler) { 805 return itemBox.addKeyUpHandler(event -> { 806 if (isEnabled()) { 807 handler.onKeyUp(event); 808 } 809 }); 810 } 811 812 @Override 813 public HandlerRegistration addSelectionHandler(final SelectionHandler<Suggestion> handler) { 814 return addHandler(new SelectionHandler<Suggestion>() { 815 @Override 816 public void onSelection(SelectionEvent<Suggestion> event) { 817 if (isEnabled()) { 818 handler.onSelection(event); 819 } 820 } 821 }, SelectionEvent.getType()); 822 } 823 824 @Override 825 public HandlerRegistration addValueChangeHandler(final ValueChangeHandler<List<? extends Suggestion>> handler) { 826 return addHandler(new ValueChangeHandler<List<? extends Suggestion>>() { 827 @Override 828 public void onValueChange(ValueChangeEvent<List<? extends Suggestion>> event) { 829 if (isEnabled()) { 830 handler.onValueChange(event); 831 } 832 } 833 }, ValueChangeEvent.getType()); 834 } 835 836 @Override 837 public HandlerRegistration addBlurHandler(BlurHandler handler) { 838 return itemBox.addHandler(blurEvent -> { 839 if (isEnabled()) { 840 handler.onBlur(blurEvent); 841 } 842 }, BlurEvent.getType()); 843 } 844 845 @Override 846 public HandlerRegistration addFocusHandler(FocusHandler handler) { 847 return itemBox.addHandler(focusEvent -> { 848 if (isEnabled()) { 849 handler.onFocus(focusEvent); 850 } 851 }, FocusEvent.getType()); 852 } 853 854 protected ProgressMixin<MaterialAutoComplete> getProgressMixin() { 855 if (progressMixin == null) { 856 progressMixin = new ProgressMixin<>(this); 857 } 858 return progressMixin; 859 } 860 861 protected CssTypeMixin<AutocompleteType, MaterialAutoComplete> getTypeMixin() { 862 if (typeMixin == null) { 863 typeMixin = new CssTypeMixin<>(this, this); 864 } 865 return typeMixin; 866 } 867 868 @Override 869 public ErrorMixin<AbstractValueWidget, MaterialLabel> getErrorMixin() { 870 if (errorMixin == null) { 871 errorMixin = new ErrorMixin<>(this, errorLabel, list, label); 872 } 873 return errorMixin; 874 } 875 876 protected ReadOnlyMixin<MaterialAutoComplete, TextBox> getReadOnlyMixin() { 877 if (readOnlyMixin == null) { 878 readOnlyMixin = new ReadOnlyMixin<>(this, itemBox); 879 } 880 return readOnlyMixin; 881 } 882 883 @Override 884 protected FocusableMixin<MaterialWidget> getFocusableMixin() { 885 if (focusableMixin == null) { 886 focusableMixin = new FocusableMixin<>(new MaterialWidget(itemBox.getElement())); 887 } 888 return focusableMixin; 889 } 890}