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