001/*
002 * #%L
003 * GwtMaterial
004 * %%
005 * Copyright (C) 2015 - 2017 GwtMaterialDesign
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 * 
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 * 
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package gwt.material.design.addins.client.combobox;
021
022import com.google.gwt.core.client.GWT;
023import com.google.gwt.core.client.Scheduler;
024import com.google.gwt.dom.client.Document;
025import com.google.gwt.dom.client.Style;
026import com.google.gwt.event.logical.shared.*;
027import com.google.gwt.event.shared.HandlerRegistration;
028import com.google.gwt.user.client.ui.Widget;
029import gwt.material.design.addins.client.MaterialAddins;
030import gwt.material.design.addins.client.base.constants.AddinsCssName;
031import gwt.material.design.addins.client.combobox.base.HasUnselectItemHandler;
032import gwt.material.design.addins.client.combobox.events.ComboBoxEvents;
033import gwt.material.design.addins.client.combobox.events.SelectItemEvent;
034import gwt.material.design.addins.client.combobox.events.UnselectItemEvent;
035import gwt.material.design.addins.client.combobox.js.JsComboBox;
036import gwt.material.design.addins.client.combobox.js.JsComboBoxOptions;
037import gwt.material.design.addins.client.combobox.js.LanguageOptions;
038import gwt.material.design.client.MaterialDesignBase;
039import gwt.material.design.client.base.*;
040import gwt.material.design.client.base.mixin.EnabledMixin;
041import gwt.material.design.client.base.mixin.ErrorMixin;
042import gwt.material.design.client.base.mixin.ReadOnlyMixin;
043import gwt.material.design.client.base.mixin.WavesMixin;
044import gwt.material.design.client.constants.CssName;
045import gwt.material.design.client.ui.MaterialLabel;
046import gwt.material.design.client.ui.html.Label;
047import gwt.material.design.client.ui.html.OptGroup;
048import gwt.material.design.client.ui.html.Option;
049import gwt.material.design.jquery.client.api.JQueryElement;
050
051import java.util.*;
052
053import static gwt.material.design.addins.client.combobox.js.JsComboBox.$;
054
055//@formatter:off
056
057/**
058 * ComboBox component used on chat module
059 * <p>
060 * <h3>XML Namespace Declaration</h3>
061 * <pre>
062 * {@code
063 * xmlns:ma='urn:import:gwt.material.design.addins.client'
064 * }
065 * </pre>
066 * <p>
067 * <h3>UiBinder Usage:</h3>
068 * <pre>
069 * {@code
070 * <combobox:MaterialComboBox>
071 *   <m:html.Option value="1" text="Sample 1"/>
072 *   <m:html.Option value="2" text="Sample 2"/>
073 *   <m:html.Option value="3" text="Sample 3"/>
074 * </combobox:MaterialComboBox>
075 * }
076 * </pre>
077 *
078 * @author kevzlou7979
079 * @author Ben Dol
080 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#combobox">Material ComboBox</a>
081 * @see <a href="https://github.com/select2/select2">Select2 4.0.3</a>
082 */
083//@formatter:on
084public class MaterialComboBox<T> extends AbstractValueWidget<List<T>> implements JsLoader, HasPlaceholder,
085        HasOpenHandlers<T>, HasCloseHandlers<T>, HasUnselectItemHandler<T>, HasReadOnly {
086
087    static {
088        if (MaterialAddins.isDebug()) {
089            MaterialDesignBase.injectDebugJs(MaterialComboBoxDebugClientBundle.INSTANCE.select2DebugJs());
090            MaterialDesignBase.injectCss(MaterialComboBoxDebugClientBundle.INSTANCE.select2DebugCss());
091        } else {
092            MaterialDesignBase.injectJs(MaterialComboBoxClientBundle.INSTANCE.select2Js());
093            MaterialDesignBase.injectCss(MaterialComboBoxClientBundle.INSTANCE.select2Css());
094        }
095    }
096
097
098    private int selectedIndex;
099    private boolean suppressChangeEvent;
100    protected List<T> values = new ArrayList<>();
101    private Label label = new Label();
102    private MaterialLabel errorLabel = new MaterialLabel();
103    protected MaterialWidget listbox = new MaterialWidget(Document.get().createSelectElement());
104    private KeyFactory<T, String> keyFactory = Object::toString;
105    private JsComboBoxOptions options = JsComboBoxOptions.create();
106
107    private ErrorMixin<AbstractValueWidget, MaterialLabel> errorMixin;
108    private ReadOnlyMixin<MaterialComboBox, MaterialWidget> readOnlyMixin;
109    private EnabledMixin<MaterialWidget> enabledMixin;
110    private WavesMixin<MaterialWidget> wavesMixin;
111
112    public MaterialComboBox() {
113        super(Document.get().createDivElement(), CssName.INPUT_FIELD, AddinsCssName.COMBOBOX);
114    }
115
116    @Override
117    protected void onLoad() {
118        label.setInitialClasses(AddinsCssName.SELECT2LABEL);
119        super.add(listbox);
120        super.add(label);
121        errorLabel.setMarginTop(15);
122        $(errorLabel.getElement()).insertAfter($(getElement()));
123        listbox.setGwtDisplay(Style.Display.BLOCK);
124
125        super.onLoad();
126
127        load();
128
129        registerHandler(addSelectionHandler(valueChangeEvent -> $(getElement()).find("input").val("")));
130    }
131
132    @Override
133    public void load() {
134        JsComboBox jsComboBox = $(listbox.getElement());
135        jsComboBox.select2(options);
136
137        jsComboBox.on(ComboBoxEvents.CHANGE, event -> {
138            if (!suppressChangeEvent) {
139                ValueChangeEvent.fire(this, getValue());
140            }
141            return true;
142        });
143
144        jsComboBox.on(ComboBoxEvents.SELECT, event -> {
145            SelectItemEvent.fire(this, getValue());
146            return true;
147        });
148
149        jsComboBox.on(ComboBoxEvents.UNSELECT, event -> {
150            UnselectItemEvent.fire(this, getValue());
151            return true;
152        });
153
154        jsComboBox.on(ComboBoxEvents.OPEN, (event1, o) -> {
155            OpenEvent.fire(this, null);
156            return true;
157        });
158
159        jsComboBox.on(ComboBoxEvents.CLOSE, (event1, o) -> {
160            CloseEvent.fire(this, null);
161            return true;
162        });
163
164        if (getTextColor() != null) {
165            $(getElement()).find(".select2-selection__rendered").css("color", getTextColor().getCssName());
166        }
167    }
168
169    @Override
170    protected void onUnload() {
171        super.onUnload();
172
173        unload();
174    }
175
176    @Override
177    public void unload() {
178        JsComboBox jsComboBox = $(listbox.getElement());
179        jsComboBox.off(ComboBoxEvents.CHANGE);
180        jsComboBox.off(ComboBoxEvents.SELECT);
181        jsComboBox.off(ComboBoxEvents.UNSELECT);
182        jsComboBox.off(ComboBoxEvents.OPEN);
183        jsComboBox.off(ComboBoxEvents.CLOSE);
184        jsComboBox.select2("destroy");
185    }
186
187    @Override
188    public void reload() {
189        unload();
190        load();
191    }
192
193    @Override
194    public void add(Widget child) {
195        if (child instanceof OptGroup) {
196            for (Widget w : ((OptGroup) child).getChildren()) {
197                if (w instanceof Option) {
198                    values.add((T) ((Option) w).getValue());
199                }
200            }
201        } else if (child instanceof Option) {
202            values.add((T) ((Option) child).getValue());
203        }
204        listbox.add(child);
205    }
206
207    /**
208     * Add OptionGroup directly to combobox component
209     *
210     * @param group - Option Group component
211     */
212    public void addGroup(OptGroup group) {
213        listbox.add(group);
214    }
215
216    /**
217     * Add item directly to combobox component with existing OptGroup
218     *
219     * @param text     - The text you want to labeled on the option item
220     * @param value    - The value you want to pass through in this option
221     * @param optGroup - Add directly this option into the existing group
222     */
223    public void addItem(String text, T value, OptGroup optGroup) {
224        if (!values.contains(value)) {
225            values.add(value);
226            optGroup.add(buildOption(text, value));
227        }
228    }
229
230    /**
231     * Add Value directly to combobox component
232     *
233     * @param text  - The text you want to labeled on the option item
234     * @param value - The value you want to pass through in this option
235     */
236    public Option addItem(String text, T value) {
237        if (!values.contains(value)) {
238            Option option = buildOption(text, value);
239            values.add(value);
240            listbox.add(option);
241            return option;
242        }
243        return null;
244    }
245
246    public Option addItem(T value) {
247        return addItem(keyFactory.generateKey(value), value);
248    }
249
250    public void setItems(Collection<T> items) {
251        clear();
252        addItems(items);
253    }
254
255    public void addItems(Collection<T> items) {
256        items.forEach(this::addItem);
257    }
258
259    /**
260     * Build the Option Element with provided params
261     */
262    protected Option buildOption(String text, T value) {
263        Option option = new Option();
264        option.setText(text);
265        option.setValue(keyFactory.generateKey(value));
266        return option;
267    }
268
269    /**
270     * Programmatically open the combobox component
271     */
272    public void open() {
273        $(listbox.getElement()).select2("open");
274    }
275
276    /**
277     * Programmatically close the combobox component
278     */
279    public void close() {
280        $(listbox.getElement()).select2("close");
281    }
282
283    @Override
284    public void clear() {
285        final Iterator<Widget> it = iterator();
286        while (it.hasNext()) {
287            final Widget widget = it.next();
288            if (widget != label && widget != errorLabel && widget != listbox) {
289                it.remove();
290            }
291        }
292        listbox.clear();
293        values.clear();
294    }
295
296    /**
297     * Sets the parent element of the dropdown
298     */
299    public void setDropdownParent(String dropdownParent) {
300        options.dropdownParent = $(dropdownParent);
301    }
302
303    public JQueryElement getDropdownParent() {
304        return options.dropdownParent;
305    }
306
307    /**
308     * Will get the Selection Results ul element containing all the combobox items.
309     */
310    public JQueryElement getDropdownResultElement() {
311        String dropdownId = getDropdownContainerElement().attr("id").toString();
312        if (dropdownId != null && !(dropdownId.isEmpty())) {
313            dropdownId = dropdownId.replace("container", "results");
314            return $("#" + dropdownId);
315        } else {
316            GWT.log("The element dropdown-result ul element is undefined.", new NullPointerException());
317        }
318        return null;
319    }
320
321    /**
322     * Will get the Selection dropdown container rendered
323     */
324    public JQueryElement getDropdownContainerElement() {
325        JQueryElement element = $(getElement()).find(".select2 .selection .select2-selection__rendered");
326        if (element == null) {
327            GWT.log("The element dropdown-container element is undefined.", new NullPointerException());
328        }
329        return element;
330    }
331
332    /**
333     * Set the upper label above the combobox
334     */
335    public void setLabel(String text) {
336        label.setText(text);
337    }
338
339    @Override
340    public String getPlaceholder() {
341        return options.placeholder;
342    }
343
344    @Override
345    public void setPlaceholder(String placeholder) {
346        options.placeholder = placeholder;
347    }
348
349    /**
350     * Check if allow clear option is enabled
351     */
352    public boolean isAllowClear() {
353        return options.allowClear;
354    }
355
356    /**
357     * Add a clear button on the right side of the combobox
358     */
359    public void setAllowClear(boolean allowClear) {
360        options.allowClear = allowClear;
361    }
362
363    /**
364     * Get the maximum number of items to be entered on multiple combobox
365     */
366    public int getLimit() {
367        return options.maximumSelectionLength;
368    }
369
370    /**
371     * Set the maximum number of items to be entered on multiple combobox
372     */
373    public void setLimit(int limit) {
374        options.maximumSelectionLength = limit;
375    }
376
377    /**
378     * Check whether the search box is enabled on combobox
379     */
380    public boolean isHideSearch() {
381        return options.minimumResultsForSearch.equals("Infinity");
382    }
383
384    /**
385     * Set the option to display the search box inside the combobox component
386     */
387    public void setHideSearch(boolean hideSearch) {
388        if (hideSearch) {
389            options.minimumResultsForSearch = "Infinity";
390        }
391    }
392
393    /**
394     * Check whether the multiple option is enabled
395     */
396    public boolean isMultiple() {
397        if (listbox != null) {
398            return listbox.getElement().hasAttribute("multiple");
399        }
400        return false;
401    }
402
403
404    /**
405     * Sets multi-value select boxes.
406     */
407    public void setMultiple(boolean multiple) {
408        if (multiple) {
409            $(listbox.getElement()).attr("multiple", "multiple");
410        } else {
411            $(listbox.getElement()).removeAttr("multiple");
412        }
413    }
414
415
416    public void setAcceptableValues(Collection<T> values) {
417        setItems(values);
418    }
419
420    @Override
421    public List<T> getValue() {
422        if (!isMultiple()) {
423            int index = getSelectedIndex();
424            T value;
425            if (index != -1) {
426
427                // Check when the value is a custom tag
428                if (isTags()) {
429                    value = (T) $(listbox.getElement()).val();
430                } else {
431                    value = values.get(index);
432                }
433
434                return Collections.singletonList(value);
435            }
436        } else {
437            return getSelectedValues();
438        }
439        return new ArrayList<>();
440    }
441
442    /**
443     * Gets the value for currently selected item. If multiple items are
444     * selected, this method will return the value of the first selected item.
445     *
446     * @return the value for selected item, or {@code null} if none is selected
447     */
448    public List<T> getSelectedValue() {
449        return getValue();
450    }
451
452    /**
453     * Only return a single value even if multi support is activate.
454     */
455    public T getSingleValue() {
456        List<T> values = getSelectedValue();
457        if (!values.isEmpty()) {
458            return values.get(0);
459        }
460        return null;
461    }
462
463    @Override
464    public void setValue(List<T> value) {
465        setValue(value, false);
466    }
467
468    /**
469     * Set the selected value using a single item, generally used
470     * in single selection mode.
471     */
472    public void setSingleValue(T value) {
473        setValue(Collections.singletonList(value));
474    }
475
476    @Override
477    public void setValue(List<T> values, boolean fireEvents) {
478        if (!isMultiple()) {
479            if (!values.isEmpty()) {
480                setSingleValue(values.get(0), fireEvents);
481            }
482        } else {
483            setValues(values, fireEvents);
484        }
485    }
486
487    /**
488     * Set the selected value using a single item, generally used
489     * in single selection mode.
490     */
491    public void setSingleValue(T value, boolean fireEvents) {
492        int index = this.values.indexOf(value);
493        if (index >= 0) {
494            List<T> before = getValue();
495            setSelectedIndex(index);
496
497            if (fireEvents) {
498                ValueChangeEvent.fireIfNotEqual(this, before, Collections.singletonList(value));
499            }
500        }
501    }
502
503    /**
504     * Set directly all the values that will be stored into
505     * combobox and build options into it.
506     */
507    public void setValues(List<T> values) {
508        setValues(values, true);
509    }
510
511    /**
512     * Set directly all the values that will be stored into
513     * combobox and build options into it.
514     */
515    public void setValues(List<T> values, boolean fireEvents) {
516        String[] stringValues = new String[values.size()];
517        for (int i = 0; i < values.size(); i++) {
518            stringValues[i] = keyFactory.generateKey(values.get(i));
519        }
520        suppressChangeEvent = !fireEvents;
521        $(listbox.getElement()).val(stringValues).trigger("change", selectedIndex);
522        suppressChangeEvent = false;
523    }
524
525    /**
526     * Gets the index of the value pass in this method
527     *
528     * @param value - The Object you want to pass as value on combobox
529     */
530    public int getValueIndex(T value) {
531        return values.indexOf(value);
532    }
533
534    /**
535     * Sets the currently selected index.
536     * <p>
537     * After calling this method, only the specified item in the list will
538     * remain selected. For a ListBox with multiple selection enabled.
539     *
540     * @param selectedIndex - the index of the item to be selected
541     */
542    public void setSelectedIndex(int selectedIndex) {
543        this.selectedIndex = selectedIndex;
544        if (values.size() > 0) {
545            T value = values.get(selectedIndex);
546            if (value != null) {
547                $(listbox.getElement()).val(keyFactory.generateKey(value)).trigger("change.select2", selectedIndex);
548            } else {
549                GWT.log("Value index is not found.", new IndexOutOfBoundsException());
550            }
551        }
552    }
553
554    /**
555     * Gets the text for currently selected item. If multiple items are
556     * selected, this method will return the text of the first selected item.
557     *
558     * @return the text for selected item, or {@code null} if none is selected
559     */
560    public int getSelectedIndex() {
561        Object o = $(getElement()).find("option:selected").last().prop("index");
562        if (o != null) {
563            return Integer.parseInt(o.toString());
564        }
565        return -1;
566    }
567
568    /**
569     * Get all the values sets on combobox
570     */
571    public List<T> getValues() {
572        return values;
573    }
574
575    /**
576     * Get the selected vales from multiple combobox
577     */
578    public List<T> getSelectedValues() {
579        Object[] curVal = (Object[]) $(listbox.getElement()).val();
580
581        List<T> selectedValues = new ArrayList<>();
582        if (curVal == null || curVal.length < 1) {
583            return selectedValues;
584        }
585
586        List<String> keyIndex = getValuesKeyIndex();
587        for (Object val : curVal) {
588            if (val instanceof String) {
589                int selectedIndex = keyIndex.indexOf(val);
590                if (selectedIndex != -1) {
591                    selectedValues.add(values.get(selectedIndex));
592                } else {
593                    if (isTags() && val instanceof String) {
594                        selectedValues.add((T) val);
595                    }
596                }
597            }
598        }
599        return selectedValues;
600    }
601
602    protected List<String> getValuesKeyIndex() {
603        List<String> keys = new ArrayList<>();
604        for (T value : values) {
605            keys.add(keyFactory.generateKey(value));
606        }
607        return keys;
608    }
609
610    /**
611     * Use your own key factory for value keys.
612     */
613    public void setKeyFactory(KeyFactory<T, String> keyFactory) {
614        this.keyFactory = keyFactory;
615    }
616
617    @Override
618    public void setReadOnly(boolean value) {
619        getReadOnlyMixin().setReadOnly(value);
620    }
621
622    @Override
623    public boolean isReadOnly() {
624        return getReadOnlyMixin().isReadOnly();
625    }
626
627    @Override
628    public void setToggleReadOnly(boolean toggle) {
629        getReadOnlyMixin().setToggleReadOnly(toggle);
630        registerHandler(addValueChangeHandler(valueChangeEvent -> {
631            if (isToggleReadOnly()) {
632                setReadOnly(true);
633            }
634        }));
635    }
636
637    @Override
638    public boolean isToggleReadOnly() {
639        return getReadOnlyMixin().isToggleReadOnly();
640    }
641
642    /**
643     * Check whether the dropdown will be close or not when result is selected
644     */
645    public boolean isCloseOnSelect() {
646        return options.closeOnSelect;
647    }
648
649    /**
650     * Allow or Prevent the dropdown from closing when a result is selected (Default true)
651     */
652    public void setCloseOnSelect(boolean closeOnSelect) {
653        options.closeOnSelect = closeOnSelect;
654    }
655
656    public MaterialWidget getListbox() {
657        return listbox;
658    }
659
660    public Label getLabel() {
661        return label;
662    }
663
664    public MaterialLabel getErrorLabel() {
665        return errorLabel;
666    }
667
668    public boolean isTags() {
669        return options.tags;
670    }
671
672    /**
673     * Note: Tags will only support String as generic params starting 2.x.
674     */
675    public void setTags(boolean tags) {
676        if (tags) GWT.log("Note: Tags will only support String as generic params.");
677        options.tags = tags;
678    }
679
680    /**
681     * Will provide a set of text objecs that can be used for i18n language support.
682     */
683    public void setLanguage(LanguageOptions language) {
684        options.language = language;
685    }
686
687    public LanguageOptions getLanguage() {
688        return options.language;
689    }
690
691    public void scrollTop(int offset) {
692        Scheduler.get().scheduleDeferred(() -> getDropdownResultElement().scrollTop(offset));
693    }
694
695    @Override
696    public void setEnabled(boolean enabled) {
697        super.setEnabled(enabled);
698
699        getEnabledMixin().updateWaves(enabled, this);
700    }
701
702    public HandlerRegistration addSelectionHandler(SelectItemEvent.SelectComboHandler<T> selectionHandler) {
703        return addHandler(selectionHandler, SelectItemEvent.getType());
704    }
705
706    @Override
707    public HandlerRegistration addOpenHandler(OpenHandler<T> openHandler) {
708        return addHandler(openHandler, OpenEvent.getType());
709    }
710
711    @Override
712    public HandlerRegistration addCloseHandler(CloseHandler<T> closeHandler) {
713        return addHandler(closeHandler, CloseEvent.getType());
714    }
715
716    @Override
717    public HandlerRegistration addRemoveItemHandler(UnselectItemEvent.UnselectComboHandler<T> handler) {
718        return addHandler(handler, UnselectItemEvent.getType());
719    }
720
721    @Override
722    protected EnabledMixin<MaterialWidget> getEnabledMixin() {
723        if (enabledMixin == null) {
724            enabledMixin = new EnabledMixin<>(listbox);
725        }
726        return enabledMixin;
727    }
728
729    @Override
730    public ErrorMixin<AbstractValueWidget, MaterialLabel> getErrorMixin() {
731        if (errorMixin == null) {
732            errorMixin = new ErrorMixin<>(this, errorLabel, this.asWidget());
733        }
734        return errorMixin;
735    }
736
737    public ReadOnlyMixin<MaterialComboBox, MaterialWidget> getReadOnlyMixin() {
738        if (readOnlyMixin == null) {
739            readOnlyMixin = new ReadOnlyMixin<>(this, listbox);
740        }
741        return readOnlyMixin;
742    }
743}