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&lt;User&gt; contacts = new LinkedList&lt;&gt;();
079 * <p>
080 *     public void addContacts(List&lt;User&gt; 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&lt;UserSuggestion&gt; list = new ArrayList&lt;&gt;();
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&lt;User&gt; getSelectedUsers(){
143 *     List&lt;? extends Suggestion&gt; values = userAutoComplete.getValue();
144 *     List&lt;User&gt; users = new ArrayList&lt;&gt;(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}