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.GWT;
023import com.google.gwt.dom.client.Document;
024import com.google.gwt.dom.client.OptionElement;
025import com.google.gwt.dom.client.SelectElement;
026import com.google.gwt.event.dom.client.DomEvent;
027import com.google.gwt.event.logical.shared.ValueChangeEvent;
028import com.google.gwt.i18n.client.HasDirection.Direction;
029import com.google.gwt.user.client.ui.FormPanel;
030import com.google.gwt.user.client.ui.HasConstrainedValue;
031import com.google.gwt.user.client.ui.ListBox;
032import gwt.material.design.client.base.*;
033import gwt.material.design.client.base.mixin.ErrorMixin;
034import gwt.material.design.client.base.mixin.ReadOnlyMixin;
035import gwt.material.design.client.base.mixin.ToggleStyleMixin;
036import gwt.material.design.client.constants.CssName;
037import gwt.material.design.client.js.JsMaterialElement;
038import gwt.material.design.client.ui.html.Label;
039import gwt.material.design.jquery.client.api.JQueryElement;
040
041import java.util.ArrayList;
042import java.util.Collection;
043import java.util.LinkedList;
044import java.util.List;
045
046import static gwt.material.design.client.js.JsMaterialElement.$;
047//@formatter:off
048
049/**
050 * <p>Material ListBox is another dropdown component that will set / get the value depends on the selected index
051 * <h3>UiBinder Usage:</h3>
052 * <p>
053 * <pre>
054 * {@code
055 *    <m:MaterialListBox ui:field="lstBox" />
056 * }
057 * </pre>
058 * <h3>Java Usage:</h3>
059 * <p>
060 * <pre>
061 * {@code
062 *     // functions
063 *    lstBox.setSelectedIndex(2);
064 *    lstBox.getSelectedIndex();
065 *    lstBox.addValueChangeHandler(handler);
066 * }
067 * </pre>
068 * </p>
069 *
070 * @author kevzlou7979
071 * @author Ben Dol
072 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#forms">Material ListBox</a>
073 * @see <a href="https://material.io/guidelines/components/menus.html">Material Design Specification</a>
074 */
075//@formatter:on
076public class MaterialListValueBox<T> extends AbstractValueWidget<T> implements JsLoader, HasPlaceholder,
077        HasConstrainedValue<T>, HasReadOnly {
078
079    private final ListBox listBox = new ListBox();
080    private final Label label = new Label();
081    protected final List<T> values = new ArrayList<>();
082    private KeyFactory<T, String> keyFactory = Object::toString;
083    private MaterialLabel errorLabel = new MaterialLabel();
084
085    private ToggleStyleMixin<ListBox> toggleOldMixin;
086    private ReadOnlyMixin<MaterialListValueBox<T>, ListBox> readOnlyMixin;
087    private ErrorMixin<AbstractValueWidget, MaterialLabel> errorMixin;
088
089    private String emptyPlaceHolder = null;
090
091    public MaterialListValueBox() {
092        super(Document.get().createDivElement(), CssName.INPUT_FIELD, CssName.LISTBOX_WRAPPER);
093    }
094
095    @Override
096    protected void onLoad() {
097        super.onLoad();
098
099        add(listBox);
100        add(label);
101        add(errorLabel);
102
103        registerHandler(addValueChangeHandler(valueChangeEvent -> {
104            if (isToggleReadOnly()) {
105                setReadOnly(true);
106            }
107        }));
108
109        load();
110    }
111
112    @Override
113    public void load() {
114        JQueryElement listBoxElement = $(listBox.getElement());
115        JsMaterialElement.$(listBox.getElement()).material_select(
116                () -> $("input.select-dropdown").trigger("close", true));
117        listBoxElement.change((e, param) -> {
118            try {
119                ValueChangeEvent.fire(this, getValue());
120            } catch (IndexOutOfBoundsException ex) {
121                GWT.log("ListBox value change handler threw an exception.", ex);
122            }
123            return true;
124        });
125
126        JQueryElement selectDropdown = listBoxElement.siblings("input.select-dropdown");
127        selectDropdown.mousedown((event, o) -> {
128            $("input[data-activates!='" + listBoxElement.attr("data-activates") + "'].select-dropdown").trigger("close", true);
129            return true;
130        });
131
132        selectDropdown.blur((e, param1) -> {
133            DomEvent.fireNativeEvent(Document.get().createBlurEvent(), this);
134            return true;
135        });
136
137        selectDropdown.focus((e, param1) -> {
138            DomEvent.fireNativeEvent(Document.get().createFocusEvent(), this);
139            return true;
140        });
141    }
142
143    @Override
144    protected void onUnload() {
145        super.onUnload();
146
147        unload();
148    }
149
150    @Override
151    public void unload() {
152        if (listBox != null && listBox.isAttached()) {
153            $(listBox.getElement()).siblings("input.select-dropdown").off("mousedown");
154            $(listBox.getElement()).off("change");
155            $(listBox.getElement()).material_select("destroy");
156        }
157    }
158
159    @Override
160    public void reload() {
161        if (isAttached()) {
162            unload();
163            load();
164        }
165    }
166
167    public void add(T value) {
168        addItem(value);
169    }
170
171    /**
172     * Adds an item to the list box, specifying its direction. This method has
173     * the same effect as
174     * <pre>addItem(value, dir, item)</pre>
175     *
176     * @param value the item's value, to be submitted if it is part of a
177     *              {@link FormPanel}; cannot be <code>null</code>
178     * @param dir   the item's direction
179     */
180    public void addItem(T value, Direction dir) {
181        addItem(value, dir, true);
182    }
183
184    /**
185     * Adds an item to the list box, specifying its direction. This method has
186     * the same effect as
187     * <pre>addItem(value, dir, item)</pre>
188     *
189     * @param value  the item's value, to be submitted if it is part of a
190     *              {@link FormPanel}; cannot be <code>null</code>
191     * @param dir    the item's direction
192     * @param reload perform a 'material select' reload to update the DOM.
193     */
194    public void addItem(T value, Direction dir, boolean reload) {
195        values.add(value);
196        listBox.addItem(keyFactory.generateKey(value), dir);
197
198        if (reload) {
199            reload();
200        }
201    }
202
203    /**
204     * Adds an item to the list box. This method has the same effect as
205     * <pre>addItem(value, item)</pre>
206     *
207     * @param value the item's value, to be submitted if it is part of a
208     *              {@link FormPanel}; cannot be <code>null</code>
209     */
210    public void addItem(T value) {
211        addItem(value, true);
212    }
213
214    /**
215     * Adds an item to the list box. This method has the same effect as
216     * <pre>addItem(value, item)</pre>
217     *
218     * @param value  the item's value, to be submitted if it is part of a
219     *              {@link FormPanel}; cannot be <code>null</code>
220     * @param reload perform a 'material select' reload to update the DOM.
221     */
222    public void addItem(T value, boolean reload) {
223        values.add(value);
224        listBox.addItem(keyFactory.generateKey(value));
225
226        if (reload) {
227            reload();
228        }
229    }
230
231    /**
232     * Adds an item to the list box, specifying an initial value for the item.
233     *
234     * @param value the item's value, to be submitted if it is part of a
235     *              {@link FormPanel}; cannot be <code>null</code>
236     * @param text  the text of the item to be added
237     */
238    public void addItem(T value, String text) {
239        addItem(value, text, true);
240    }
241
242    /**
243     * Adds an item to the list box, specifying an initial value for the item.
244     *
245     * @param value  the item's value, to be submitted if it is part of a
246     *               {@link FormPanel}; cannot be <code>null</code>
247     * @param text   the text of the item to be added
248     * @param reload perform a 'material select' reload to update the DOM.
249     */
250    public void addItem(T value, String text, boolean reload) {
251        values.add(value);
252        listBox.addItem(text, keyFactory.generateKey(value));
253
254        if (reload) {
255            reload();
256        }
257    }
258
259    /**
260     * Adds an item to the list box, specifying its direction and an initial
261     * value for the item.
262     *
263     * @param value the item's value, to be submitted if it is part of a
264     *              {@link FormPanel}; cannot be <code>null</code>
265     * @param dir   the item's direction
266     * @param text  the text of the item to be added
267     */
268    public void addItem(T value, Direction dir, String text) {
269        addItem(value, dir, text, true);
270    }
271
272    /**
273     * Adds an item to the list box, specifying its direction and an initial
274     * value for the item.
275     *
276     * @param value  the item's value, to be submitted if it is part of a
277     *               {@link FormPanel}; cannot be <code>null</code>
278     * @param dir    the item's direction
279     * @param text   the text of the item to be added
280     * @param reload perform a 'material select' reload to update the DOM.
281     */
282    public void addItem(T value, Direction dir, String text, boolean reload) {
283        values.add(value);
284        listBox.addItem(text, dir, keyFactory.generateKey(value));
285
286        if (reload) {
287            reload();
288        }
289    }
290
291    /**
292     * Inserts an item into the list box. Has the same effect as
293     * <pre>insertItem(value, item, index)</pre>
294     *
295     * @param value the item's value, to be submitted if it is part of a
296     *              {@link FormPanel}.
297     * @param index the index at which to insert it
298     */
299    public void insertItem(T value, int index) {
300        insertItemInternal(value, index, true);
301    }
302
303    /**
304     * Inserts an item into the list box. Has the same effect as
305     * <pre>insertItem(value, item, index)</pre>
306     *
307     * @param value  the item's value, to be submitted if it is part of a
308     *              {@link FormPanel}.
309     * @param index  the index at which to insert it
310     * @param reload perform a 'material select' reload to update the DOM.
311     */
312    public void insertItem(T value, int index, boolean reload) {
313        index += getIndexOffset();
314        insertItemInternal(value, index, reload);
315    }
316    protected void insertItemInternal(T value, int index, boolean reload) {
317        values.add(index, value);
318        listBox.insertItem(keyFactory.generateKey(value), index);
319
320        if (reload) {
321            reload();
322        }
323    }
324
325    /**
326     * Inserts an item into the list box, specifying its direction. Has the same
327     * effect as
328     * <pre>insertItem(value, dir, item, index)</pre>
329     *
330     * @param value the item's value, to be submitted if it is part of a
331     *              {@link FormPanel}.
332     * @param dir   the item's direction
333     * @param index the index at which to insert it
334     */
335    public void insertItem(T value, Direction dir, int index) {
336        insertItemInternal(value, dir, index, true);
337    }
338
339    /**
340     * Inserts an item into the list box, specifying its direction. Has the same
341     * effect as
342     * <pre>insertItem(value, dir, item, index)</pre>
343     *
344     * @param value  the item's value, to be submitted if it is part of a
345     *              {@link FormPanel}.
346     * @param dir    the item's direction
347     * @param index  the index at which to insert it
348     * @param reload perform a 'material select' reload to update the DOM.
349     */
350    public void insertItem(T value, Direction dir, int index, boolean reload) {
351        index += getIndexOffset();
352        insertItemInternal(value, dir, index, reload);
353    }
354    protected void insertItemInternal(T value, Direction dir, int index, boolean reload) {
355        values.add(index, value);
356        listBox.insertItem(keyFactory.generateKey(value), dir, index);
357
358        if (reload) {
359            reload();
360        }
361    }
362
363    /**
364     * Inserts an item into the list box, specifying an initial value for the
365     * item. Has the same effect as
366     * <pre>insertItem(value, null, item, index)</pre>
367     *
368     * @param value the item's value, to be submitted if it is part of a
369     *              {@link FormPanel}.
370     * @param text  the text of the item to be inserted
371     * @param index the index at which to insert it
372     */
373    public void insertItem(T value, String text, int index) {
374        insertItemInternal(value, text, index, true);
375    }
376
377    /**
378     * Inserts an item into the list box, specifying an initial value for the
379     * item. Has the same effect as
380     * <pre>insertItem(value, null, item, index)</pre>
381     *
382     * @param value  the item's value, to be submitted if it is part of a
383     *               {@link FormPanel}.
384     * @param text   the text of the item to be inserted
385     * @param index  the index at which to insert it
386     * @param reload perform a 'material select' reload to update the DOM.
387     */
388    public void insertItem(T value, String text, int index, boolean reload) {
389        index += getIndexOffset();
390        insertItemInternal(value, text, index, reload);
391    }
392    protected void insertItemInternal(T value, String text, int index, boolean reload) {
393        values.add(index, value);
394        listBox.insertItem(text, keyFactory.generateKey(value), index);
395
396        if (reload) {
397            reload();
398        }
399    }
400
401    /**
402     * Inserts an item into the list box, specifying its direction and an
403     * initial value for the item. If the index is less than zero, or greater
404     * than or equal to the length of the list, then the item will be appended
405     * to the end of the list.
406     *
407     * @param value the item's value, to be submitted if it is part of a
408     *              {@link FormPanel}.
409     * @param dir   the item's direction. If {@code null}, the item is displayed
410     *              in the widget's overall direction, or, if a direction
411     *              estimator has been set, in the item's estimated direction.
412     * @param text  the text of the item to be inserted
413     * @param index the index at which to insert it
414     */
415    public void insertItem(T value, Direction dir, String text, int index) {
416        insertItemInternal(value, dir, text, index, true);
417    }
418
419    /**
420     * Inserts an item into the list box, specifying its direction and an
421     * initial value for the item. If the index is less than zero, or greater
422     * than or equal to the length of the list, then the item will be appended
423     * to the end of the list.
424     *
425     * @param value  the item's value, to be submitted if it is part of a
426     *               {@link FormPanel}.
427     * @param dir    the item's direction. If {@code null}, the item is displayed
428     *               in the widget's overall direction, or, if a direction
429     *               estimator has been set, in the item's estimated direction.
430     * @param text   the text of the item to be inserted
431     * @param index  the index at which to insert it
432     * @param reload perform a 'material select' reload to update the DOM.
433     */
434    public void insertItem(T value, Direction dir, String text, int index, boolean reload) {
435        index += getIndexOffset();
436        insertItemInternal(value, dir, text, index, reload);
437    }
438    protected void insertItemInternal(T value, Direction dir, String text, int index, boolean reload) {
439        values.add(index, value);
440        listBox.insertItem(keyFactory.generateKey(value), dir, text, index);
441
442        if (reload) {
443            reload();
444        }
445    }
446
447    /**
448     * Removes the item at the specified index.
449     *
450     * @param index the index of the item to be removed
451     * @throws IndexOutOfBoundsException if the index is out of range
452     */
453    public void removeItem(int index) {
454        removeItemInternal(index, true);
455    }
456
457    /**
458     * Removes the item at the specified index.
459     *
460     * @param index  the index of the item to be removed
461     * @param reload perform a 'material select' reload to update the DOM.
462     * @throws IndexOutOfBoundsException if the index is out of range
463     */
464    public void removeItem(int index, boolean reload) {
465        index += getIndexOffset();
466        removeItemInternal(index, reload);
467    }
468    protected void removeItemInternal(int index, boolean reload) {
469        values.remove(index);
470        listBox.removeItem(index);
471
472        if (reload) {
473            reload();
474        }
475    }
476
477    /**
478     * Removes a value from the list box. Nothing is done if the value isn't on
479     * the list box.
480     *
481     * @param value the value to be removed from the list
482     */
483    public void removeValue(T value) {
484        removeValue(value, true);
485    }
486
487    /**
488     * Removes a value from the list box. Nothing is done if the value isn't on
489     * the list box.
490     *
491     * @param value  the value to be removed from the list
492     * @param reload perform a 'material select' reload to update the DOM.
493     */
494    public void removeValue(T value, boolean reload) {
495        int idx = getIndex(value);
496        if (idx >= 0) {
497            removeItemInternal(idx, reload);
498        }
499    }
500
501    @Override
502    public void reset() {
503        super.reset();
504        clear();
505    }
506
507    /**
508     * Removes all items from the list box.
509     */
510    @Override
511    public void clear() {
512        values.clear();
513        listBox.clear();
514        if(emptyPlaceHolder != null) {
515            insertEmptyPlaceHolder(emptyPlaceHolder);
516        }
517        reload();
518    }
519
520    @Override
521    public void setPlaceholder(String placeholder) {
522        label.setText(placeholder);
523    }
524
525    @Override
526    public String getPlaceholder() {
527        return label.getText();
528    }
529
530    public OptionElement getOptionElement(int index) {
531        return getSelectElement().getOptions().getItem(index);
532    }
533
534    protected SelectElement getSelectElement() {
535        return listBox.getElement().cast();
536    }
537
538    /**
539     * Sets whether this list allows multiple selections.
540     *
541     * @param multipleSelect <code>true</code> to allow multiple selections
542     */
543    public void setMultipleSelect(boolean multipleSelect) {
544        listBox.setMultipleSelect(multipleSelect);
545    }
546
547    /**
548     * Gets whether this list allows multiple selection.
549     *
550     * @return <code>true</code> if multiple selection is allowed
551     */
552    public boolean isMultipleSelect() {
553        return listBox.isMultipleSelect();
554    }
555
556    public void setEmptyPlaceHolder(String value) {
557        if(value == null) {
558            // about to un-set emptyPlaceHolder
559            if(emptyPlaceHolder != null) {
560                // emptyPlaceHolder is about to change from null to non-null
561                if(isEmptyPlaceHolderListed()) {
562                    // indeed first item is actually emptyPlaceHolder
563                    removeEmptyPlaceHolder();
564                } else {
565                    GWT.log("WARNING: emptyPlaceHolder is set but not listed.", new IllegalStateException());
566                }
567            }   // else no change
568        } else {
569            if(!value.equals(emptyPlaceHolder)) {
570                // adding emptyPlaceHolder
571                insertEmptyPlaceHolder(value);
572            }   // else no change
573        }
574
575        emptyPlaceHolder = value;
576    }
577
578    public String getEmptyPlaceHolder() {
579        return emptyPlaceHolder;
580    }
581
582    @Override
583    public void setAcceptableValues(Collection<T> values) {
584        clear();
585        values.forEach(this::addItem);
586    }
587
588    @Override
589    public T getValue() {
590        int selectedIndex = listBox.getSelectedIndex();
591        if (selectedIndex >= 0) {
592            return values.get(selectedIndex);
593        }
594        return null;
595    }
596
597    @Override
598    public void setValue(T value) {
599        setValue(value, false);
600    }
601
602    @Override
603    public void setValue(T value, boolean fireEvents) {
604        int index = values.indexOf(value);
605        if (index >= 0) {
606            T before = getValue();
607            setSelectedIndexInternal(index);
608
609            if (fireEvents) {
610                ValueChangeEvent.fireIfNotEqual(this, before, value);
611            }
612        }
613    }
614
615    public boolean isOld() {
616        return getToggleOldMixin().isOn();
617    }
618
619    public void setOld(boolean old) {
620        getToggleOldMixin().setOn(old);
621    }
622
623    /**
624     * Sets the value associated with the item at a given index. This value can
625     * be used for any purpose, but is also what is passed to the server when
626     * the list box is submitted as part of a {@link FormPanel}.
627     *
628     * @param index the index of the item to be set
629     * @param value the item's new value; cannot be <code>null</code>
630     * @throws IndexOutOfBoundsException if the index is out of range
631     */
632    public void setValue(int index, String value) {
633        index += getIndexOffset();
634        listBox.setValue(index, value);
635        reload();
636    }
637
638    @Override
639    public void setTitle(String title) {
640        listBox.setTitle(title);
641    }
642
643    /**
644     * Sets whether an individual list item is selected.
645     *
646     * @param index    the index of the item to be selected or unselected
647     * @param selected <code>true</code> to select the item
648     * @throws IndexOutOfBoundsException if the index is out of range
649     */
650    public void setItemSelected(int index, boolean selected) {
651        index += getIndexOffset();
652        setItemSelectedInternal(index, selected);
653    }
654    private void setItemSelectedInternal(int index, boolean selected) {
655        listBox.setItemSelected(index, selected);
656        reload();
657    }
658
659    /**
660     * Sets the text associated with the item at a given index.
661     *
662     * @param index the index of the item to be set
663     * @param text  the item's new text
664     * @throws IndexOutOfBoundsException if the index is out of range
665     */
666    public void setItemText(int index, String text) {
667        index += getIndexOffset();
668        listBox.setItemText(index, text);
669        reload();
670    }
671
672    /**
673     * Sets the text associated with the item at a given index.
674     *
675     * @param index the index of the item to be set
676     * @param text  the item's new text
677     * @param dir   the item's direction.
678     * @throws IndexOutOfBoundsException if the index is out of range
679     */
680    public void setItemText(int index, String text, Direction dir) {
681        index += getIndexOffset();
682        listBox.setItemText(index, text, dir);
683        reload();
684    }
685
686    public void setName(String name) {
687        listBox.setName(name);
688    }
689
690    /**
691     * Sets the currently selected index.
692     * <p>
693     * After calling this method, only the specified item in the list will
694     * remain selected. For a ListBox with multiple selection enabled, see
695     * {@link #setItemSelected(int, boolean)} to select multiple items at a
696     * time.
697     *
698     * @param index the index of the item to be selected
699     */
700    public void setSelectedIndex(int index) {
701        index += getIndexOffset();
702        setSelectedIndexInternal(index);
703    }
704    protected void setSelectedIndexInternal(int index) {
705        listBox.setSelectedIndex(index);
706        reload();
707    }
708
709    /**
710     * Sets the number of items that are visible. If only one item is visible,
711     * then the box will be displayed as a drop-down list.
712     *
713     * @param visibleItems the visible item count
714     */
715    public void setVisibleItemCount(int visibleItems) {
716        listBox.setVisibleItemCount(visibleItems);
717    }
718
719    /**
720     * Gets the number of items present in the list box.
721     *
722     * @return the number of items
723     */
724    public int getItemCount() {
725        return listBox.getItemCount();
726    }
727
728    /**
729     * Gets the text associated with the item at the specified index.
730     *
731     * @param index the index of the item whose text is to be retrieved
732     * @return the text associated with the item
733     * @throws IndexOutOfBoundsException if the index is out of range
734     */
735    public String getItemText(int index) {
736        index += getIndexOffset();
737        return listBox.getItemText(index);
738    }
739
740    /**
741     * Gets the text for currently selected item. If multiple items are
742     * selected, this method will return the text of the first selected item.
743     *
744     * @return the text for selected item, or {@code null} if none is selected
745     */
746    public String getSelectedItemText() {
747        return listBox.getSelectedItemText();
748    }
749
750    public String getName() {
751        return listBox.getName();
752    }
753
754    /**
755     * Gets the currently-selected item. If multiple items are selected, this
756     * method will return the first selected item ({@link #isItemSelected(int)}
757     * can be used to query individual items).
758     *
759     * @return the selected index, or <code>-1</code> if none is selected
760     */
761    public int getSelectedIndex() {
762        int selectedIndex = getSelectedIndexInternal();
763        if(selectedIndex >= 0) {
764            selectedIndex -= getIndexOffset();
765        }
766        return selectedIndex;
767    }
768    protected int getSelectedIndexInternal() {
769        return listBox.getSelectedIndex();
770    }
771
772    /**
773     * Gets the value associated with the item at a given index.
774     *
775     * @param index the index of the item to be retrieved
776     * @return the item's associated value
777     * @throws IndexOutOfBoundsException if the index is out of range
778     */
779    public T getValue(int index) {
780        return getValueInternal(index + getIndexOffset());
781    }
782    protected T getValueInternal(int index) {
783        return values.get(index);
784    }
785
786    /**
787     * Gets the value for currently selected item. If multiple items are
788     * selected, this method will return the value of the first selected item.
789     *
790     * @return the value for selected item, or {@code null} if none is selected
791     */
792    public T getSelectedValue() {
793        try {
794            return values.get(getSelectedIndexInternal());
795        } catch (IndexOutOfBoundsException ex) {
796            return null;
797        }
798    }
799
800    /**
801     * Gets the number of items that are visible. If only one item is visible,
802     * then the box will be displayed as a drop-down list.
803     *
804     * @return the visible item count
805     */
806    public int getVisibleItemCount() {
807        return listBox.getVisibleItemCount();
808    }
809
810    /**
811     * Determines whether an individual list item is selected.
812     *
813     * @param index the index of the item to be tested
814     * @return <code>true</code> if the item is selected
815     * @throws IndexOutOfBoundsException if the index is out of range
816     */
817    public boolean isItemSelected(int index) {
818        return listBox.isItemSelected(index + getIndexOffset());
819    }
820
821
822    @Override
823    public void setEnabled(boolean enabled) {
824        listBox.setEnabled(enabled);
825        reload();
826    }
827
828    @Override
829    public boolean isEnabled() {
830        return listBox.isEnabled();
831    }
832
833    /**
834     * Use your own key factory for value keys.
835     */
836    public void setKeyFactory(KeyFactory<T, String> keyFactory) {
837        this.keyFactory = keyFactory;
838    }
839
840    @Override
841    public void setReadOnly(boolean value) {
842        getReadOnlyMixin().setReadOnly(value);
843        if (!value) {
844            $(listBox.getElement()).material_select("destroy");
845            $(listBox.getElement()).material_select();
846        }
847    }
848
849    @Override
850    public boolean isReadOnly() {
851        return getReadOnlyMixin().isReadOnly();
852    }
853
854    @Override
855    public void setToggleReadOnly(boolean toggle) {
856        getReadOnlyMixin().setToggleReadOnly(toggle);
857    }
858
859    @Override
860    public boolean isToggleReadOnly() {
861        return getReadOnlyMixin().isToggleReadOnly();
862    }
863
864    public ListBox getListBox() {
865        return listBox;
866    }
867
868    @Override
869    public ErrorMixin<AbstractValueWidget, MaterialLabel> getErrorMixin() {
870        if (errorMixin == null) {
871            errorMixin = new ErrorMixin<>(this, errorLabel, this, label);
872        }
873        return errorMixin;
874    }
875
876    public Label getLabel() {
877        return label;
878    }
879
880    public MaterialLabel getErrorLabel() {
881        return errorLabel;
882    }
883
884    /**
885     * Returns all selected values of the list box, or empty array if none.
886     *
887     * @return the selected values of the list box
888     */
889    public String[] getItemsSelected() {
890        List<String> selected = new LinkedList<>();
891        for (int i = getIndexOffset(); i < listBox.getItemCount(); i++) {
892            if (listBox.isItemSelected(i)) {
893                selected.add(listBox.getValue(i));
894            }
895        }
896        return selected.toArray(new String[selected.size()]);
897    }
898
899    /**
900     * Sets the currently selected value.
901     * <p>
902     * After calling this method, only the specified item in the list will
903     * remain selected. For a ListBox with multiple selection enabled, see
904     * {@link #setValueSelected(T, boolean)} to select multiple items at a
905     * time.
906     *
907     * @param value the value of the item to be selected
908     */
909    public void setSelectedValue(T value) {
910        int idx = getIndex(value);
911        if (idx >= 0) {
912            setSelectedIndexInternal(idx);
913        }
914    }
915
916    /**
917     * Sets whether an individual list value is selected.
918     *
919     * @param value    the value of the item to be selected or unselected
920     * @param selected <code>true</code> to select the item
921     */
922    public void setValueSelected(T value, boolean selected) {
923        int idx = getIndex(value);
924        if (idx >= 0) {
925            setItemSelectedInternal(idx, selected);
926        }
927    }
928
929    /**
930     * Gets the index of the specified value.
931     *
932     * @param value the value of the item to be found
933     * @return the index of the value
934     */
935    public int getIndex(T value) {
936        int count = getItemCount();
937        for (int i = 0; i < count; i++) {
938            if (getValueInternal(i).equals(value)) {
939                return i;
940            }
941        }
942        return -1;
943    }
944
945    public ReadOnlyMixin<MaterialListValueBox<T>, ListBox> getReadOnlyMixin() {
946        if (readOnlyMixin == null) {
947            readOnlyMixin = new ReadOnlyMixin<>(this, listBox);
948        }
949        return readOnlyMixin;
950    }
951
952    protected ToggleStyleMixin<ListBox> getToggleOldMixin() {
953        if (toggleOldMixin == null) {
954            toggleOldMixin = new ToggleStyleMixin<>(listBox, "browser-default");
955        }
956        return toggleOldMixin;
957    }
958
959    /**
960     * Checks whether {@link #emptyPlaceHolder} is added/present in both {@link #listBox} and {@link #values} at 0 index.
961     *
962     * @return is {@link #emptyPlaceHolder} added/present in both {@link #listBox} and {@link #values}?
963     */
964    protected boolean isEmptyPlaceHolderListed() {
965        return emptyPlaceHolder.equals(listBox.getValue(0)) &&
966                values.get(0) == null;
967    }
968
969    protected void insertEmptyPlaceHolder(String emptyPlaceHolder) {
970        listBox.insertItem(emptyPlaceHolder, 0);
971        values.add(0, null);
972        getOptionElement(0).setDisabled(true);
973    }
974
975    protected void removeEmptyPlaceHolder() {
976        // indeed the first item/value is emptyPlaceHolder
977        listBox.removeItem(0);
978        values.remove(0);
979
980        OptionElement currentPlaceholder = getOptionElement(0);
981        if (currentPlaceholder != null) {
982            currentPlaceholder.setDisabled(false);
983        }
984    }
985
986    /**
987     * @return index increased by number of special items/values at the start (e.g. {@link #emptyPlaceHolder})
988     */
989    protected int getIndexOffset() {
990        return emptyPlaceHolder != null && isEmptyPlaceHolderListed() ? 1 : 0;
991    }
992}