001package gwt.material.design.client.ui;
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.core.client.GWT;
024import com.google.gwt.dom.client.Document;
025import com.google.gwt.dom.client.Element;
026import com.google.gwt.dom.client.OptionElement;
027import com.google.gwt.dom.client.SelectElement;
028import com.google.gwt.editor.client.EditorError;
029import com.google.gwt.editor.client.HasEditorErrors;
030import com.google.gwt.event.dom.client.BlurEvent;
031import com.google.gwt.event.dom.client.BlurHandler;
032import com.google.gwt.event.dom.client.HasBlurHandlers;
033import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
034import com.google.gwt.event.logical.shared.ValueChangeEvent;
035import com.google.gwt.event.logical.shared.ValueChangeHandler;
036import com.google.gwt.event.shared.HandlerRegistration;
037import com.google.gwt.i18n.client.HasDirection.Direction;
038import com.google.gwt.user.client.ui.FormPanel;
039import com.google.gwt.user.client.ui.HasConstrainedValue;
040import com.google.gwt.user.client.ui.ListBox;
041import gwt.material.design.client.base.*;
042import gwt.material.design.client.base.error.ErrorHandler;
043import gwt.material.design.client.base.error.ErrorHandlerType;
044import gwt.material.design.client.base.error.HasErrorHandler;
045import gwt.material.design.client.base.mixin.BlankValidatorMixin;
046import gwt.material.design.client.base.mixin.ErrorHandlerMixin;
047import gwt.material.design.client.base.mixin.ToggleStyleMixin;
048import gwt.material.design.client.base.validator.HasValidators;
049import gwt.material.design.client.base.validator.ValidationChangedEvent.ValidationChangedHandler;
050import gwt.material.design.client.base.validator.Validator;
051import gwt.material.design.client.ui.html.Label;
052import gwt.material.design.client.ui.html.Option;
053
054import java.util.*;
055
056//@formatter:off
057
058/**
059 * <p>Material ListBox is another dropdown component that will set / get the value depends on the selected index
060 * <h3>UiBinder Usage:</h3>
061 *
062 * <pre>
063 * {@code
064 *    <m:MaterialListBox ui:field="lstBox" />
065 * }
066 * </pre>
067 * <h3>Java Usage:</h3>
068 *
069 * <pre>
070 * {@code
071 *     // functions
072 *    lstBox.setSelectedIndex(2);
073 *    lstBox.getSelectedIndex();
074 *    lstBox.addValueChangeHandler(handler);
075 * }
076 * </pre>
077 * </p>
078 *
079 * @author kevzlou7979
080 * @author Ben Dol
081 * @see <a href="http://gwt-material-demo.herokuapp.com/#forms">Material ListBox</a>
082 */
083//@formatter:on
084public class MaterialListValueBox<T> extends MaterialWidget implements HasId, HasGrid, HasColors, HasPlaceholder,
085        HasValueChangeHandlers<T>, HasConstrainedValue<T>, HasEditorErrors<T>, HasErrorHandler, HasValidators<T>,
086        HasBlurHandlers {
087
088    private final ListBox listBox = new ListBox();
089    private final Label lblName = new Label();
090
091    private boolean initialized;
092
093    protected final List<T> values = new ArrayList<>();
094
095    private ToggleStyleMixin<ListBox> toggleOldMixin;
096    private final ErrorHandlerMixin<T> errorHandlerMixin = new ErrorHandlerMixin<>(this);
097    private final BlankValidatorMixin<MaterialListValueBox<T>, T> validatorMixin = new BlankValidatorMixin<>(this,
098            errorHandlerMixin.getErrorHandler());
099
100    public MaterialListValueBox() {
101        super(Document.get().createDivElement(), "input-field");
102        add(listBox);
103        add(lblName);
104        toggleOldMixin = new ToggleStyleMixin<>(listBox, "browser-default");
105    }
106
107    @Override
108    public void onLoad() {
109        super.onLoad();
110        if (!initialized) {
111            initialized = true;
112            createInternalChangeHandler(listBox.getElement());
113            initializeMaterial(listBox.getElement());
114        }
115    }
116
117    @Override
118    public void setPlaceholder(String placeholder) {
119        lblName.setText(placeholder);
120
121        if (initialized && placeholder != null) {
122            initializeMaterial(listBox.getElement());
123        }
124    }
125
126    @Override
127    public String getPlaceholder() {
128        return lblName.getText();
129    }
130
131    public OptionElement getOptionElement(int index) {
132        return getSelectElement().getOptions().getItem(index);
133    }
134
135    /**
136     * Removes all items from the list box.
137     */
138    @Override
139    public void clear() {
140        listBox.clear();
141        if (initialized) {
142            // reinitialize
143            initializeMaterial(listBox.getElement());
144        }
145    }
146
147    protected SelectElement getSelectElement() {
148        return listBox.getElement().cast();
149    }
150
151    protected void onChangeInternal() {
152        try {
153            ValueChangeEvent.fire(this, values.get(getSelectedIndex()));
154        } catch (IndexOutOfBoundsException ex) {
155            GWT.log("onChangeInternal threw an exception", ex);
156        }
157    }
158
159    /**
160     * Creates the internal change handler needed to trigger change events for
161     * Materialize CSS change events.
162     */
163    protected native void createInternalChangeHandler(Element element) /*-{
164        var that = this;
165        var callback = $entry(function() {
166            that.@gwt.material.design.client.ui.MaterialListValueBox::onChangeInternal()();
167        });
168
169        $wnd.jQuery(element).change(callback);
170    }-*/;
171
172    /**
173     * Initializes the Materialize CSS list box. Should be
174     * called every time the contents of the list box
175     * changes, to keep the Materialize CSS design updated.
176     */
177    protected native void initializeMaterial(Element element) /*-{
178        $wnd.jQuery(element).material_select();
179    }-*/;
180
181    /**
182     * Re initialize the material listbox component
183     */
184    public void reinitialize() {
185        initializeMaterial(getElement());
186    }
187
188    /**
189     * Sets whether this list allows multiple selections.
190     *
191     * @param multipleSelect <code>true</code> to allow multiple selections
192     */
193    public void setMultipleSelect(boolean multipleSelect) {
194        listBox.setMultipleSelect(multipleSelect);
195        if (initialized) {
196            initializeMaterial(listBox.getElement());
197        }
198    }
199
200    /**
201     * Gets whether this list allows multiple selection.
202     *
203     * @return <code>true</code> if multiple selection is allowed
204     */
205    public boolean isMultipleSelect() {
206        return listBox.isMultipleSelect();
207    }
208
209    public void setEmptyPlaceHolder(String value) {
210        listBox.insertItem(value, 0);
211
212        getOptionElement(0).setDisabled(true);
213
214        if (initialized) {
215            initializeMaterial(listBox.getElement());
216        }
217    }
218
219    @Override
220    public HandlerRegistration addValueChangeHandler(final ValueChangeHandler<T> handler) {
221        return addHandler(new ValueChangeHandler<T>() {
222            @Override
223            public void onValueChange(ValueChangeEvent<T> event) {
224                if(isEnabled()){
225                    handler.onValueChange(event);
226                }
227            }
228        }, ValueChangeEvent.getType());
229    }
230
231    @Override
232    public void setAcceptableValues(Collection<T> values) {
233        this.values.clear();
234        clear();
235
236        for(T value : values) {
237            addValue(value);
238        }
239    }
240
241    @Override
242    public T getValue() {
243        return values.get(getSelectedIndex());
244    }
245
246    @Override
247    public void setValue(T value) {
248        setValue(value, true);
249    }
250
251    @Override
252    public void setValue(T value, boolean fireEvents) {
253        int index = getIndex(value.toString());
254        if(index > 0 && values.contains(value)) {
255            T before = getValue();
256            setSelectedIndex(index);
257
258            if (fireEvents) {
259                ValueChangeEvent.fireIfNotEqual(this, before, value);
260            }
261        }
262    }
263
264    public Option addValue(T value) {
265        if(!values.contains(value)) {
266            values.add(value);
267            Option opt = new Option(value.toString());
268            add(opt);
269            return opt;
270        } else {
271            GWT.log("Cannot add duplicate value: " + value);
272        }
273        return null;
274    }
275
276    public boolean isOld() {
277        return toggleOldMixin.isOn();
278    }
279
280    public void setOld(boolean old) {
281        toggleOldMixin.setOn(old);
282    }
283
284    // delegate methods
285
286    /**
287     * Inserts an item into the list box, specifying its direction and an
288     * initial value for the item. If the index is less than zero, or greater
289     * than or equal to the length of the list, then the item will be appended
290     * to the end of the list.
291     *
292     * @param item
293     *            the text of the item to be inserted
294     * @param dir
295     *            the item's direction. If {@code null}, the item is displayed
296     *            in the widget's overall direction, or, if a direction
297     *            estimator has been set, in the item's estimated direction.
298     * @param value
299     *            the item's value, to be submitted if it is part of a
300     *            {@link FormPanel}.
301     * @param index
302     *            the index at which to insert it
303     */
304    public void insertItem(String item, Direction dir, String value, int index) {
305        listBox.insertItem(item, dir, value, index);
306        if (initialized) {
307            // reinitialize
308            initializeMaterial(listBox.getElement());
309        }
310    }
311
312    /**
313     * Sets the value associated with the item at a given index. This value can
314     * be used for any purpose, but is also what is passed to the server when
315     * the list box is submitted as part of a {@link FormPanel}.
316     *
317     * @param index
318     *            the index of the item to be set
319     * @param value
320     *            the item's new value; cannot be <code>null</code>
321     * @throws IndexOutOfBoundsException
322     *             if the index is out of range
323     */
324    public void setValue(int index, String value) {
325        listBox.setValue(index, value);
326        if (initialized) {
327            // reinitialize
328            initializeMaterial(listBox.getElement());
329        }
330    }
331
332    @Override
333    public void setTitle(String title) {
334        listBox.setTitle(title);
335        if (initialized) {
336            // reinitialize
337            initializeMaterial(listBox.getElement());
338        }
339    }
340
341    /**
342     * Adds an item to the list box, specifying its direction. This method has
343     * the same effect as
344     *
345     * <pre>
346     * addItem(item, dir, item)
347     * </pre>
348     *
349     * @param item
350     *            the text of the item to be added
351     * @param dir
352     *            the item's direction
353     */
354    public void addItem(String item, Direction dir) {
355        listBox.addItem(item, dir);
356        if (initialized) {
357            // reinitialize
358            initializeMaterial(listBox.getElement());
359        }
360    }
361
362    /**
363     * Adds an item to the list box. This method has the same effect as
364     *
365     * <pre>
366     * addItem(item, item)
367     * </pre>
368     *
369     * @param item
370     *            the text of the item to be added
371     */
372    public void addItem(String item) {
373        listBox.addItem(item);
374        if (initialized) {
375            // reinitialize
376            initializeMaterial(listBox.getElement());
377        }
378    }
379
380    /**
381     * Adds an item to the list box, specifying an initial value for the item.
382     *
383     * @param item
384     *            the text of the item to be added
385     * @param value
386     *            the item's value, to be submitted if it is part of a
387     *            {@link FormPanel}; cannot be <code>null</code>
388     */
389    public void addItem(String item, String value) {
390        listBox.addItem(item, value);
391        if (initialized) {
392            // reinitialize
393            initializeMaterial(listBox.getElement());
394        }
395    }
396
397    /**
398     * Adds an item to the list box, specifying its direction and an initial
399     * value for the item.
400     *
401     * @param item
402     *            the text of the item to be added
403     * @param dir
404     *            the item's direction
405     * @param value
406     *            the item's value, to be submitted if it is part of a
407     *            {@link FormPanel}; cannot be <code>null</code>
408     */
409    public void addItem(String item, Direction dir, String value) {
410        listBox.addItem(item, dir, value);
411        if (initialized) {
412            // reinitialize
413            initializeMaterial(listBox.getElement());
414        }
415    }
416
417    /**
418     * Inserts an item into the list box. Has the same effect as
419     *
420     * <pre>
421     * insertItem(item, item, index)
422     * </pre>
423     *
424     * @param item
425     *            the text of the item to be inserted
426     * @param index
427     *            the index at which to insert it
428     */
429    public void insertItem(String item, int index) {
430        listBox.insertItem(item, index);
431        if (initialized) {
432            // reinitialize
433            initializeMaterial(listBox.getElement());
434        }
435    }
436
437    /**
438     * Inserts an item into the list box, specifying its direction. Has the same
439     * effect as
440     *
441     * <pre>
442     * insertItem(item, dir, item, index)
443     * </pre>
444     *
445     * @param item
446     *            the text of the item to be inserted
447     * @param dir
448     *            the item's direction
449     * @param index
450     *            the index at which to insert it
451     */
452    public void insertItem(String item, Direction dir, int index) {
453        listBox.insertItem(item, dir, index);
454        if (initialized) {
455            // reinitialize
456            initializeMaterial(listBox.getElement());
457        }
458    }
459
460    /**
461     * Inserts an item into the list box, specifying an initial value for the
462     * item. Has the same effect as
463     *
464     * <pre>
465     * insertItem(item, null, value, index)
466     * </pre>
467     *
468     * @param item
469     *            the text of the item to be inserted
470     * @param value
471     *            the item's value, to be submitted if it is part of a
472     *            {@link FormPanel}.
473     * @param index
474     *            the index at which to insert it
475     */
476    public void insertItem(String item, String value, int index) {
477        listBox.insertItem(item, value, index);
478        if (initialized) {
479            // reinitialize
480            initializeMaterial(listBox.getElement());
481        }
482    }
483
484    /**
485     * Sets whether an individual list item is selected.
486     *
487     * @param index
488     *            the index of the item to be selected or unselected
489     * @param selected
490     *            <code>true</code> to select the item
491     * @throws IndexOutOfBoundsException
492     *             if the index is out of range
493     */
494    public void setItemSelected(int index, boolean selected) {
495        listBox.setItemSelected(index, selected);
496        if (initialized) {
497            // reinitialize
498            initializeMaterial(listBox.getElement());
499        }
500    }
501
502    /**
503     * Sets the text associated with the item at a given index.
504     *
505     * @param index
506     *            the index of the item to be set
507     * @param text
508     *            the item's new text
509     * @throws IndexOutOfBoundsException
510     *             if the index is out of range
511     */
512    public void setItemText(int index, String text) {
513        listBox.setItemText(index, text);
514        if (initialized) {
515            // reinitialize
516            initializeMaterial(listBox.getElement());
517        }
518    }
519
520    /**
521     * Sets the text associated with the item at a given index.
522     *
523     * @param index
524     *            the index of the item to be set
525     * @param text
526     *            the item's new text
527     * @param dir
528     *            the item's direction.
529     * @throws IndexOutOfBoundsException
530     *             if the index is out of range
531     */
532    public void setItemText(int index, String text, Direction dir) {
533        listBox.setItemText(index, text, dir);
534        if (initialized) {
535            // reinitialize
536            initializeMaterial(listBox.getElement());
537        }
538    }
539
540    public void setName(String name) {
541        listBox.setName(name);
542        if (initialized) {
543            // reinitialize
544            initializeMaterial(listBox.getElement());
545        }
546    }
547
548    /**
549     * Sets the currently selected index.
550     *
551     * After calling this method, only the specified item in the list will
552     * remain selected. For a ListBox with multiple selection enabled, see
553     * {@link #setItemSelected(int, boolean)} to select multiple items at a
554     * time.
555     *
556     * @param index
557     *            the index of the item to be selected
558     */
559    public void setSelectedIndex(int index) {
560        listBox.setSelectedIndex(index);
561        if (initialized) {
562            // reinitialize
563            initializeMaterial(listBox.getElement());
564        }
565    }
566
567    /**
568     * Sets the number of items that are visible. If only one item is visible,
569     * then the box will be displayed as a drop-down list.
570     *
571     * @param visibleItems
572     *            the visible item count
573     */
574    public void setVisibleItemCount(int visibleItems) {
575        listBox.setVisibleItemCount(visibleItems);
576        if (initialized) {
577            // reinitialize
578            initializeMaterial(listBox.getElement());
579        }
580    }
581
582    /**
583     * Gets the number of items present in the list box.
584     *
585     * @return the number of items
586     */
587    public int getItemCount() {
588        return listBox.getItemCount();
589    }
590
591    /**
592     * Gets the text associated with the item at the specified index.
593     *
594     * @param index
595     *            the index of the item whose text is to be retrieved
596     * @return the text associated with the item
597     * @throws IndexOutOfBoundsException
598     *             if the index is out of range
599     */
600    public String getItemText(int index) {
601        return listBox.getItemText(index);
602    }
603
604    /**
605     * Gets the text for currently selected item. If multiple items are
606     * selected, this method will return the text of the first selected item.
607     *
608     * @return the text for selected item, or {@code null} if none is selected
609     */
610    public String getSelectedItemText() {
611        return listBox.getSelectedItemText();
612    }
613
614    public String getName() {
615        return listBox.getName();
616    }
617
618    /**
619     * Gets the currently-selected item. If multiple items are selected, this
620     * method will return the first selected item ({@link #isItemSelected(int)}
621     * can be used to query individual items).
622     *
623     * @return the selected index, or <code>-1</code> if none is selected
624     */
625    public int getSelectedIndex() {
626        return listBox.getSelectedIndex();
627    }
628
629    /**
630     * Gets the value associated with the item at a given index.
631     *
632     * @param index
633     *            the index of the item to be retrieved
634     * @return the item's associated value
635     * @throws IndexOutOfBoundsException
636     *             if the index is out of range
637     */
638    public String getValue(int index) {
639        return listBox.getValue(index);
640    }
641
642    /**
643     * Gets the value for currently selected item. If multiple items are
644     * selected, this method will return the value of the first selected item.
645     *
646     * @return the value for selected item, or {@code null} if none is selected
647     */
648    public String getSelectedValue() {
649        return listBox.getSelectedValue();
650    }
651
652    /**
653     * Gets the number of items that are visible. If only one item is visible,
654     * then the box will be displayed as a drop-down list.
655     *
656     * @return the visible item count
657     */
658    public int getVisibleItemCount() {
659        return listBox.getVisibleItemCount();
660    }
661
662    /**
663     * Determines whether an individual list item is selected.
664     *
665     * @param index
666     *            the index of the item to be tested
667     * @return <code>true</code> if the item is selected
668     * @throws IndexOutOfBoundsException
669     *             if the index is out of range
670     */
671    public boolean isItemSelected(int index) {
672        return listBox.isItemSelected(index);
673    }
674
675    /**
676     * Removes the item at the specified index.
677     *
678     * @param index
679     *            the index of the item to be removed
680     * @throws IndexOutOfBoundsException
681     *             if the index is out of range
682     */
683    public void removeItem(int index) {
684        listBox.removeItem(index);
685        if (initialized) {
686            initializeMaterial(listBox.getElement());
687        }
688    }
689
690    // utility methods
691
692    /**
693     * Returns all selected values of the list box, or empty array if none.
694     *
695     * @return the selected values of the list box
696     */
697    public String[] getItemsSelected() {
698        List<String> selected = new LinkedList<>();
699        for (int i = 0; i < listBox.getItemCount(); i++) {
700            if (listBox.isItemSelected(i)) {
701                selected.add(listBox.getValue(i));
702            }
703        }
704        return selected.toArray(new String[selected.size()]);
705    }
706
707    /**
708     * Sets the currently selected value.
709     *
710     * After calling this method, only the specified item in the list will
711     * remain selected. For a ListBox with multiple selection enabled, see
712     * {@link #setValueSelected(String, boolean)} to select multiple items at a
713     * time.
714     *
715     * @param value
716     *            the value of the item to be selected
717     */
718    public void setSelectedValue(String value) {
719        int idx = getIndex(value);
720        if (idx >= 0) {
721            setSelectedIndex(idx);
722        }
723    }
724
725    /**
726     * Gets the index of the specified value.
727     *
728     * @param value
729     *            the value of the item to be found
730     * @return the index of the value
731     */
732    public int getIndex(String value) {
733        int count = getItemCount();
734        for (int i = 0; i < count; i++) {
735            String v = getValue(i);
736            if (v.equals(value)) {
737                return i;
738            }
739        }
740        return -1;
741    }
742
743    /**
744     * Sets whether an individual list value is selected.
745     *
746     * @param value
747     *            the value of the item to be selected or unselected
748     * @param selected
749     *            <code>true</code> to select the item
750     */
751    public void setValueSelected(String value, boolean selected) {
752        int idx = getIndex(value);
753        if (idx >= 0) {
754            setItemSelected(idx, selected);
755        }
756    }
757
758    /**
759     * Removes a value from the list box. Nothing is done if the value isn't on
760     * the list box.
761     *
762     * @param value
763     *            the value to be removed from the list
764     */
765    public void removeValue(String value) {
766        int idx = getIndex(value);
767        if (idx >= 0) {
768            removeItem(idx);
769        }
770    }
771
772    @Override
773    public void setEnabled(boolean enabled) {
774        listBox.setEnabled(enabled);
775         if (initialized) {
776            // reinitialize
777            initializeMaterial(listBox.getElement());
778        }
779    }
780
781    @Override
782    public void showErrors(List<EditorError> errors) {
783        errorHandlerMixin.showErrors(errors);
784    }
785
786    @Override
787    public ErrorHandler getErrorHandler() {
788        return errorHandlerMixin.getErrorHandler();
789    }
790
791    @Override
792    public void setErrorHandler(ErrorHandler errorHandler) {
793        errorHandlerMixin.setErrorHandler(errorHandler);
794    }
795
796    @Override
797    public ErrorHandlerType getErrorHandlerType() {
798        return errorHandlerMixin.getErrorHandlerType();
799    }
800
801    @Override
802    public void setErrorHandlerType(ErrorHandlerType errorHandlerType) {
803        errorHandlerMixin.setErrorHandlerType(errorHandlerType);
804    }
805
806    @Override
807    public void addValidator(Validator<T> validator) {
808        validatorMixin.addValidator(validator);
809    }
810
811    @Override
812    public boolean isValidateOnBlur() {
813        return validatorMixin.isValidateOnBlur();
814    }
815
816    @Override
817    public boolean removeValidator(Validator<T> validator) {
818        return validatorMixin.removeValidator(validator);
819    }
820
821    @Override
822    public void reset() {
823        validatorMixin.reset();
824    }
825
826    @Override
827    public void setValidateOnBlur(boolean validateOnBlur) {
828        validatorMixin.setValidateOnBlur(validateOnBlur);
829    }
830
831    @Override
832    public void setValidators(@SuppressWarnings("unchecked") Validator<T>... validators) {
833        validatorMixin.setValidators(validators);
834    }
835
836    @Override
837    public boolean validate() {
838        return validatorMixin.validate();
839    }
840
841    @Override
842    public boolean validate(boolean show) {
843        return validatorMixin.validate(show);
844    }
845
846    @Override
847    public com.google.web.bindery.event.shared.HandlerRegistration addValidationChangedHandler(ValidationChangedHandler handler) {
848        return validatorMixin.addValidationChangedHandler(handler);
849    }
850
851    @Override
852    public HandlerRegistration addBlurHandler(final BlurHandler handler) {
853        return addDomHandler(new BlurHandler() {
854            @Override
855            public void onBlur(BlurEvent event) {
856                if(isEnabled()) {
857                    handler.onBlur(event);
858                }
859            }
860        }, BlurEvent.getType());
861    }
862}