001package org.gwtbootstrap3.extras.select.client.ui;
002
003/*
004 * #%L
005 * GwtBootstrap3
006 * %%
007 * Copyright (C) 2016 GwtBootstrap3
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 */
022import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.CONTAINER;
023import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.DROPDOWN_ALIGN_RIGHT;
024import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.DROPUP_AUTO;
025import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.HEADER;
026import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.HIDE_DISABLED;
027import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.LIVE_SEARCH;
028import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.LIVE_SEARCH_NORMALIZE;
029import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.LIVE_SEARCH_PLACEHOLDER;
030import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.LIVE_SEARCH_STYLE;
031import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.MOBILE;
032import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.SELECT_ON_TAB;
033import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.SHOW_CONTENT;
034import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.SHOW_ICON;
035import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.SHOW_SUBTEXT;
036import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.SIZE;
037import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.STYLE;
038import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.WIDTH;
039import static org.gwtbootstrap3.extras.select.client.ui.SelectOptions.WINDOW_PADDING;
040
041import java.util.ArrayList;
042import java.util.HashMap;
043import java.util.List;
044import java.util.Map;
045import java.util.Map.Entry;
046
047import org.gwtbootstrap3.client.ui.base.ComplexWidget;
048import org.gwtbootstrap3.client.ui.base.HasSize;
049import org.gwtbootstrap3.client.ui.base.HasType;
050import org.gwtbootstrap3.client.ui.base.mixin.AttributeMixin;
051import org.gwtbootstrap3.client.ui.base.mixin.EnabledMixin;
052import org.gwtbootstrap3.client.ui.constants.ButtonSize;
053import org.gwtbootstrap3.client.ui.constants.ButtonType;
054import org.gwtbootstrap3.client.ui.constants.Styles;
055import org.gwtbootstrap3.extras.select.client.ui.constants.DropdownAlignRight;
056import org.gwtbootstrap3.extras.select.client.ui.constants.LiveSearchStyle;
057import org.gwtbootstrap3.extras.select.client.ui.constants.MenuSize;
058import org.gwtbootstrap3.extras.select.client.ui.constants.SelectStyles;
059import org.gwtbootstrap3.extras.select.client.ui.constants.SelectWidth;
060import org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers;
061import org.gwtbootstrap3.extras.select.client.ui.event.HiddenEvent;
062import org.gwtbootstrap3.extras.select.client.ui.event.HiddenHandler;
063import org.gwtbootstrap3.extras.select.client.ui.event.HideEvent;
064import org.gwtbootstrap3.extras.select.client.ui.event.HideHandler;
065import org.gwtbootstrap3.extras.select.client.ui.event.LoadedEvent;
066import org.gwtbootstrap3.extras.select.client.ui.event.LoadedHandler;
067import org.gwtbootstrap3.extras.select.client.ui.event.RefreshedEvent;
068import org.gwtbootstrap3.extras.select.client.ui.event.RefreshedHandler;
069import org.gwtbootstrap3.extras.select.client.ui.event.RenderedEvent;
070import org.gwtbootstrap3.extras.select.client.ui.event.RenderedHandler;
071import org.gwtbootstrap3.extras.select.client.ui.event.ShowEvent;
072import org.gwtbootstrap3.extras.select.client.ui.event.ShowHandler;
073import org.gwtbootstrap3.extras.select.client.ui.event.ShownEvent;
074import org.gwtbootstrap3.extras.select.client.ui.event.ShownHandler;
075
076import com.google.gwt.core.client.JavaScriptObject;
077import com.google.gwt.core.client.JsArrayNumber;
078import com.google.gwt.core.client.JsonUtils;
079import com.google.gwt.core.client.ScriptInjector;
080import com.google.gwt.dom.client.Document;
081import com.google.gwt.dom.client.Element;
082import com.google.gwt.dom.client.NodeList;
083import com.google.gwt.dom.client.OptionElement;
084import com.google.gwt.dom.client.SelectElement;
085import com.google.gwt.editor.client.IsEditor;
086import com.google.gwt.editor.client.LeafValueEditor;
087import com.google.gwt.editor.client.adapters.TakesValueEditor;
088import com.google.gwt.event.logical.shared.ValueChangeEvent;
089import com.google.gwt.event.logical.shared.ValueChangeHandler;
090import com.google.gwt.event.shared.HandlerRegistration;
091import com.google.gwt.user.client.ui.Focusable;
092import com.google.gwt.user.client.ui.HasEnabled;
093import com.google.gwt.user.client.ui.HasValue;
094import com.google.gwt.user.client.ui.Widget;
095import com.google.gwt.user.client.ui.impl.FocusImpl;
096
097/**
098 * Bootstrap select widget base
099 *
100 * @param <T> select value type
101 *
102 * @see http://silviomoreto.github.io/bootstrap-select/
103 * @author Xiaodong Sun
104 */
105public abstract class SelectBase<T> extends ComplexWidget implements HasValue<T>, HasEnabled, Focusable,
106        HasType<ButtonType>, HasSize<ButtonSize>, IsEditor<LeafValueEditor<T>>, HasAllSelectHandlers<T> {
107
108    private LeafValueEditor<T> editor;
109    private ButtonType type;
110    private ButtonSize size;
111
112    /**
113     * Default language: {@link SelectLanguage#EN}
114     */
115    private static final SelectLanguage DEFAULT_LANGUAGE = SelectLanguage.EN;
116
117    /**
118     * Language; defaults to {@value #DEFAULT_LANGUAGE}
119     */
120    private SelectLanguage language = DEFAULT_LANGUAGE;
121
122    protected final SelectElement selectElement;
123    protected final Map<OptionElement, Option> itemMap = new HashMap<>(0);
124    protected final AttributeMixin<SelectBase<T>> attrMixin = new AttributeMixin<>(this);
125    private final EnabledMixin<SelectBase<T>> enabledMixin = new EnabledMixin<>(this);
126    private final FocusImpl focusImpl = FocusImpl.getFocusImplForWidget();
127
128    /**
129     * Initialize options
130     */
131    protected SelectOptions options = SelectOptions.newOptions();
132
133    protected SelectBase() {
134        this.selectElement = Document.get().createSelectElement();
135        setElement(selectElement);
136        setStyleName(SelectStyles.SELECT_PICKER);
137        addStyleName(Styles.FORM_CONTROL);
138    }
139
140    /**
141     * Returns <code>true</code> if multiple selection is allowed.
142     *
143     * @return <code>true</code> if multiple selection is allowed
144     */
145    public abstract boolean isMultiple();
146
147    @Override
148    protected void onLoad() {
149        super.onLoad();
150        // Inject the language JS is necessary
151        if (language.getJs() != null) {
152            ScriptInjector.fromString(language.getJs().getText())
153                .setWindow(ScriptInjector.TOP_WINDOW).inject();
154        }
155        initialize(getElement(), options);
156        bindSelectEvents(getElement());
157    }
158
159    @Override
160    protected void onUnload() {
161        super.onUnload();
162        unbindSelectEvents(getElement());
163        command(getElement(), SelectCommand.DESTROY);
164    }
165
166    @Override
167    public void add(Widget child) {
168        super.add(child);
169        updateItemMap(child, true);
170    }
171
172    @Override
173    public void insert(Widget child, int beforeIndex) {
174        super.insert(child, beforeIndex);
175        updateItemMap(child, true);
176    }
177
178    @Override
179    public boolean remove(Widget w) {
180        boolean removed = super.remove(w);
181        if (removed) {
182            updateItemMap(w, false);
183        }
184        return removed;
185    }
186
187    void updateItemMap(Widget widget, boolean toAdd) {
188        // Option ==> update with this option
189        if (widget instanceof Option) {
190            Option option = (Option) widget;
191            if (toAdd)
192                itemMap.put(option.getSelectElement(), option);
193            else
194                itemMap.remove(option.getSelectElement());
195        } else if (widget instanceof OptGroup) {
196            // OptGroup ==> update with all optGroup options
197            OptGroup optGroup = (OptGroup) widget;
198            if (toAdd)
199                itemMap.putAll(optGroup.getItemMap());
200            else
201                for (Entry<OptionElement, Option> entry : optGroup.getItemMap().entrySet()) {
202                    OptionElement optElem = entry.getKey();
203                    itemMap.remove(optElem);
204                }
205        }
206    }
207
208    /**
209     * Set the select language.
210     *
211     * @param language
212     */
213    public void setLanguage(final SelectLanguage language) {
214        this.language = (language == null) ? DEFAULT_LANGUAGE : language;
215    }
216
217    /**
218     * Returns the select language.
219     *
220     * @return
221     */
222    public SelectLanguage getLanguage() {
223        return language;
224    }
225
226    /**
227     * Sets a container to which the select will be appended.
228     *
229     * @param container specific element or selector, e.g., "body", ".my-container"
230     */
231    public void setContainer(final String container) {
232        if (container != null)
233            attrMixin.setAttribute(CONTAINER, container);
234        else
235            attrMixin.removeAttribute(CONTAINER);
236    }
237
238    /**
239     * Sets the handler to get the text displayed when selectedTextFormat
240     * is <code>count</code> or <code>count > #</code>, or <code>null</code>
241     * to use the default text: <code>X of Y selected</code>.
242     *
243     * @param handler
244     */
245    public void setCountSelectedTextHandler(final CountSelectedTextHandler handler) {
246        options.setCountSelectedTextHandler(handler);
247    }
248
249    /**
250     * Sets the drop-down menu right alignment.<br>
251     * <br>
252     * Defaults to {@link DropdownAlignRight#FALSE}.
253     *
254     * @param dropdownAlignRight
255     * @see DropdownAlignRight
256     */
257    public void setDropdownAlignRight(final DropdownAlignRight dropdownAlignRight) {
258        if (dropdownAlignRight != null)
259            attrMixin.setAttribute(DROPDOWN_ALIGN_RIGHT, dropdownAlignRight.getValue());
260        else
261            attrMixin.removeAttribute(DROPDOWN_ALIGN_RIGHT);
262    }
263
264    /**
265     * Checks to see which has more room, above or below.
266     * If the drop-up has enough room to fully open normally,
267     * but there is more room above, the drop-up still opens
268     * normally. Otherwise, it becomes a drop-up.
269     * If dropupAuto is set to <code>false</code>, drop-ups
270     * must be called manually.<br>
271     * <br>
272     * Defaults to <code>true</code>.
273     *
274     * @param dropupAuto
275     */
276    public void setDropupAuto(final boolean dropupAuto) {
277        if (!dropupAuto)
278            attrMixin.setAttribute(DROPUP_AUTO, Boolean.toString(false));
279        else
280            attrMixin.removeAttribute(DROPUP_AUTO);
281    }
282
283    /**
284     * If drop-up auto is set to <code>false</code>, force to make
285     * the select a drop-up menu if set to <code>true</code>.
286     *
287     * @param forceDropup
288     * @see #setDropupAuto(boolean)
289     */
290    public void setForceDropup(final boolean forceDropup) {
291        if (forceDropup) {
292            addStyleName(SelectStyles.DROPUP);
293        } else {
294            removeStyleName(SelectStyles.DROPUP);
295        }
296    }
297
298    /**
299     * Adds a header to the top of the menu; includes
300     * a close button by default.<br>
301     * <br>
302     * No header by default.
303     *
304     * @param header
305     */
306    public void setHeader(final String header) {
307        if (header != null)
308            attrMixin.setAttribute(HEADER, header);
309        else
310            attrMixin.removeAttribute(HEADER);
311    }
312
313    /**
314     * Removes disabled options and optgroups from the menu<br>
315     * <br>
316     * Defaults to <code>false</code>.
317     *
318     * @param hideDisabled
319     */
320    public void setHideDisabled(final boolean hideDisabled) {
321        if (hideDisabled)
322            attrMixin.setAttribute(HIDE_DISABLED, Boolean.toString(true));
323        else
324            attrMixin.removeAttribute(HIDE_DISABLED);
325    }
326
327    /**
328     * When set to <code>true</code>, adds a search box to the
329     * top of the select picker drop-down.<br>
330     * <br>
331     * Defaults to <code>false</code>.
332     *
333     * @param liveSearch
334     */
335    public void setLiveSearch(final boolean liveSearch) {
336        if (liveSearch)
337            attrMixin.setAttribute(LIVE_SEARCH, Boolean.toString(true));
338        else
339            attrMixin.removeAttribute(LIVE_SEARCH);
340    }
341
342    /**
343     * Setting liveSearchNormalize to <code>true</code> allows for
344     * accent-insensitive searching.<br>
345     * <br>
346     * Defaults to <code>false</code>.
347     *
348     * @param liveSearchNormalize
349     */
350    public void setLiveSearchNormalize(final boolean liveSearchNormalize) {
351        if (liveSearchNormalize)
352            attrMixin.setAttribute(LIVE_SEARCH_NORMALIZE, Boolean.toString(true));
353        else
354            attrMixin.removeAttribute(LIVE_SEARCH_NORMALIZE);
355    }
356
357    /**
358     * Set live search style.<br>
359     * <br>
360     * Defaults to {@link LiveSearchStyle#CONTAINS}.
361     *
362     * @param liveSearchStyle
363     * @see LiveSearchStyle
364     */
365    public void setLiveSearchStyle(final LiveSearchStyle liveSearchStyle) {
366        if (liveSearchStyle != null)
367            attrMixin.setAttribute(LIVE_SEARCH_STYLE, liveSearchStyle.getValue());
368        else
369            attrMixin.removeAttribute(LIVE_SEARCH_STYLE);
370    }
371
372    /**
373     * Set a placeholder to the live search input.<br>
374     * <br>
375     * Defaults to <code>null</code>.
376     *
377     * @param liveSearchPlaceholder
378     */
379    public void setLiveSearchPlaceholder(final String liveSearchPlaceholder) {
380        if (liveSearchPlaceholder != null)
381            attrMixin.setAttribute(LIVE_SEARCH_PLACEHOLDER, liveSearchPlaceholder);
382        else
383            attrMixin.removeAttribute(LIVE_SEARCH_PLACEHOLDER);
384    }
385
386    /**
387     * When set to <code>true</code>, enables the device's native
388     * menu for select menus.<br>
389     * <br>
390     * Defaults to <code>false</code>.
391     *
392     * @param mobile
393     */
394    public void setMobile(final boolean mobile) {
395        if (mobile)
396            attrMixin.setAttribute(MOBILE, Boolean.toString(true));
397        else
398            attrMixin.removeAttribute(MOBILE);
399    }
400
401    /**
402     * When set to <code>true</code>, treats the tab character like the
403     * <code>enter</code> or <code>Space</code> characters within the
404     * select picker drop-down.<br>
405     * <br>
406     * Defaults to <code>false</code>.
407     *
408     * @param selectOnTab
409     */
410    public void setSelectOnTab(final boolean selectOnTab) {
411        if (selectOnTab)
412            attrMixin.setAttribute(SELECT_ON_TAB, Boolean.toString(true));
413        else
414            attrMixin.removeAttribute(SELECT_ON_TAB);
415    }
416
417    /**
418     * When set to <code>true</code>, display custom HTML associated with
419     * selected option(s) in the button. When set to <code>false</code>,
420     * the option value will be displayed instead.<br>
421     * <br>
422     * Defaults to <code>true</code>.
423     *
424     * @param showContent
425     */
426    public void setShowContent(final boolean showContent) {
427        if (!showContent)
428            attrMixin.setAttribute(SHOW_CONTENT, Boolean.toString(false));
429        else
430            attrMixin.removeAttribute(SHOW_CONTENT);
431    }
432
433    /**
434     * When set to <code>true</code>, display icon(s) associated with
435     * selected option(s) in the button.<br>
436     * <br>
437     * Defaults to <code>true</code>.
438     *
439     * @param showIcon
440     */
441    public void setShowIcon(final boolean showIcon) {
442        if (!showIcon)
443            attrMixin.setAttribute(SHOW_ICON, Boolean.toString(false));
444        else
445            attrMixin.removeAttribute(SHOW_ICON);
446    }
447
448    /**
449     * When set to <code>true</code>, display sub-text associated with a
450     * selected option in the button.<br>
451     * <br>
452     * Defaults to <code>false</code>.
453     *
454     * @param showSubtext
455     */
456    public void setShowSubtext(final boolean showSubtext) {
457        if (showSubtext)
458            attrMixin.setAttribute(SHOW_SUBTEXT, Boolean.toString(true));
459        else
460            attrMixin.removeAttribute(SHOW_SUBTEXT);
461    }
462
463    /**
464     * Specifies the number of visible menu items.<br>
465     * <br>
466     * Defaults to {@link MenuSize#AUTO}.
467     *
468     * @param size
469     * @see MenuSize
470     */
471    public void setMenuSize(final MenuSize size) {
472        if (size != null)
473            attrMixin.setAttribute(SIZE, size.getValue());
474        else
475            attrMixin.removeAttribute(SIZE);
476    }
477
478    /**
479     * The menu will show the given number of items, even if
480     * the drop-down is cut off.<br>
481     * <br>
482     * Defaults to {@link MenuSize#AUTO}.
483     *
484     * @param size
485     */
486    public void setFixedMenuSize(final int size) {
487        attrMixin.setAttribute(SIZE, Integer.toString(size));
488    }
489
490    /**
491     * Sets the {@link ButtonType} of the select.<br>
492     * <br>
493     * <b>IMPORTANT</b>: This method will override the style set by
494     * {@link #setStyle(String)}.
495     */
496    @Override
497    public void setType(final ButtonType type) {
498        this.type = type;
499        updateStyle();
500    }
501
502    /**
503     * Returns the {@link ButtonType} of the select; may be <code>null</code>.
504     *
505     * @return the {@link ButtonType} of the select
506     */
507    @Override
508    public ButtonType getType() {
509        return type;
510    }
511
512    /**
513     * Sets the {@link ButtonSize} of the select.<br>
514     * <br>
515     * <b>IMPORTANT</b>: This method will override the style set by
516     * {@link #setStyle(String)}.
517     */
518    @Override
519    public void setSize(final ButtonSize size) {
520        this.size = size;
521        updateStyle();
522    }
523
524    /**
525     * Returns the {@link ButtonSize} of the select; may be <code>null</code>.
526     *
527     * @return the {@link ButtonSize} of the select
528     */
529    @Override
530    public ButtonSize getSize() {
531        return size;
532    }
533
534    private void updateStyle() {
535        StringBuilder sb = new StringBuilder();
536        if (type != null) {
537            sb.append(type.getCssName());
538        }
539        if (size != null) {
540            if (!sb.toString().isEmpty()) {
541                sb.append(" ");
542            }
543            sb.append(size.getCssName());
544        }
545        setStyle(sb.toString());
546    }
547
548    /**
549     * Set the customized style name to the select.<br>
550     * <br>
551     * Defaults to <code>null</code>.<br>
552     * <br>
553     * <b>IMPORTANT</b>: This method will override the style set by
554     * {@link #setType(ButtonType)} and/or {@link #setSize(ButtonSize)}.
555     *
556     * @param styleName
557     */
558    public void setStyle(final String styleName) {
559        if (styleName != null)
560            attrMixin.setAttribute(STYLE, styleName);
561        else
562            attrMixin.removeAttribute(STYLE);
563    }
564
565    /**
566     * Returns the style name applied to the select.
567     *
568     * @return
569     */
570    public String getStyle() {
571        return attrMixin.getAttribute(STYLE);
572    }
573
574    /**
575     * Set the default placeholder text when nothing is selected.
576     * This works for both multiple and standard select boxes.<br>
577     * <br>
578     * Defaults to <code>null</code>.
579     *
580     * @param title
581     * @see #setTitle(String)
582     */
583    public void setPlaceholder(final String placeholder) {
584        setTitle(placeholder);
585    }
586
587    /**
588     * Set the specified width to the select.<br>
589     * <br>
590     * Defaults to {@link SelectWidth#NONE}.
591     *
592     * @param width
593     * @see #setWidth(String)
594     * @see SelectWidth
595     */
596    public void setSelectWidth(final SelectWidth width) {
597        setWidth((width != null) ? width.getValue() : null);
598    }
599
600    /**
601     * Set the select width witch is forced inline to the given value.
602     *
603     * @param cssWidth a CSS width with units, e.g. 100px
604     */
605    @Override
606    public void setWidth(final String cssWidth) {
607        if (cssWidth != null)
608            attrMixin.setAttribute(WIDTH, cssWidth);
609        else
610            attrMixin.removeAttribute(WIDTH);
611    }
612
613    /**
614     * Sets the window padding to all sides. This is useful in cases where
615     * the window has areas that the drop-down menu should not cover - for
616     * instance a fixed header.
617     *
618     * @param padding
619     */
620    public void setWindowPadding(final int padding) {
621        attrMixin.setAttribute(WINDOW_PADDING, Integer.toString(padding));
622    }
623
624    /**
625     * Sets the window padding to top, right, bottom, and right sides. This
626     * is useful in cases where the window has areas that the drop-down menu
627     * should not cover - for instance a fixed header.
628     *
629     * @param top
630     * @param right
631     * @param bottom
632     * @param left
633     */
634    public void setWindowPaddingTopRightBottomLeft(final int top, final int right,
635            final int bottom, final int left) {
636        JsArrayNumber array = JavaScriptObject.createArray(4).cast();
637        array.push(top);
638        array.push(right);
639        array.push(bottom);
640        array.push(left);
641        attrMixin.setAttribute(WINDOW_PADDING, JsonUtils.stringify(array));
642    }
643
644    /**
645     * Set to <code>true</code> to add the Bootstrap menu arrow.
646     *
647     * @param showMenuArrow
648     */
649    public void setShowMenuArrow(final boolean showMenuArrow) {
650        if (showMenuArrow) {
651            addStyleName(SelectStyles.SHOW_MENU_ARROW);
652        } else {
653            removeStyleName(SelectStyles.SHOW_MENU_ARROW);
654        }
655    }
656
657    @Override
658    public LeafValueEditor<T> asEditor() {
659        if (editor == null) {
660            editor = TakesValueEditor.of(this);
661        }
662        return editor;
663    }
664
665    @Override
666    public void setValue(final T value) {
667        setValue(value, false);
668    }
669
670    @Override
671    public void setValue(final T value, final boolean fireEvents) {
672
673        T oldValue = fireEvents ? getValue() : null;
674
675        setSelectedValue(value);
676
677        if (fireEvents) {
678            T newValue = getValue();
679            ValueChangeEvent.fireIfNotEqual(this, oldValue, newValue);
680        }
681    }
682
683    /**
684     * Fires {@link ValueChangeEvent} with the current value.
685     */
686    private void onValueChange() {
687        T newValue = getValue();
688        ValueChangeEvent.fire(this, newValue);
689    }
690
691    /**
692     * Selects the given value. If the value is <code>null</code>
693     * or does not match any option, no option will be selected.
694     *
695     * @param value
696     */
697    protected abstract void setSelectedValue(final T value);
698
699    @Override
700    public void setEnabled(boolean enabled) {
701        enabledMixin.setEnabled(enabled);
702        refresh();
703    }
704
705    @Override
706    public boolean isEnabled() {
707        return enabledMixin.isEnabled();
708    }
709
710    @Override
711    public int getTabIndex() {
712        return focusImpl.getTabIndex(getFocusElement());
713    }
714
715    @Override
716    public void setAccessKey(char key) {
717        focusImpl.setAccessKey(getFocusElement(), key);
718    }
719
720    @Override
721    public void setFocus(boolean focused) {
722        if (focused) {
723            focusImpl.focus(getFocusElement());
724        } else {
725            focusImpl.blur(getFocusElement());
726        }
727    }
728
729    @Override
730    public void setTabIndex(int index) {
731        focusImpl.setTabIndex(getFocusElement(), index);
732    }
733
734    private Element getFocusElement() {
735        if (!isAttached()) {
736            return selectElement;
737        }
738        return getElement().getParentElement().getFirstChildElement();
739    }
740
741    /**
742     * Returns the number of items present in the select.
743     *
744     * @return the number of items
745     */
746    public int getItemCount() {
747        return selectElement.getOptions().getLength();
748    }
749
750    /**
751     * Returns the item list.
752     *
753     * @return the item list
754     */
755    public List<Option> getItems() {
756        List<Option> selectedItems = new ArrayList<>(0);
757        NodeList<OptionElement> items = selectElement.getOptions();
758        for (int i = 0; i < items.getLength(); i++) {
759            OptionElement item = items.getItem(i);
760            Option option = itemMap.get(item);
761            if (option != null)
762                selectedItems.add(option);
763        }
764        return selectedItems;
765    }
766
767    /**
768     * Returns <code>true</code> if the item at the given index is selected.<br>
769     * <br>
770     * <b>Note</b>: if the item at the given index is a divider, this method
771     * always returns <code>false</code>.
772     *
773     * @param index
774     * @return
775     */
776    public boolean isItemSelected(final int index) {
777        checkIndex(index);
778        OptionElement item = selectElement.getOptions().getItem(index);
779        Option option = itemMap.get(item);
780        return option != null && option.isSelected();
781    }
782
783    /**
784     * Returns the {@link Option} at the given index.
785     *
786     * @param index
787     * @return
788     */
789    public Option getItem(final int index) {
790        checkIndex(index);
791        OptionElement item = selectElement.getOptions().getItem(index);
792        return itemMap.get(item);
793    }
794
795    private void checkIndex(final int index) {
796        int max = getItemCount();
797        if (index < 0 || index >= max) {
798            throw new IndexOutOfBoundsException("Index should be in [0, " + max + "]");
799        }
800    }
801
802    @Override
803    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<T> handler) {
804        return addHandler(handler, ValueChangeEvent.getType());
805    }
806
807    @Override
808    public HandlerRegistration addLoadedHandler(LoadedHandler handler) {
809        return addHandler(handler, LoadedEvent.getType());
810    }
811
812    @Override
813    public HandlerRegistration addShowHandler(ShowHandler handler) {
814        return addHandler(handler, ShowEvent.getType());
815    }
816
817    @Override
818    public HandlerRegistration addShownHandler(ShownHandler handler) {
819        return addHandler(handler, ShownEvent.getType());
820    }
821
822    @Override
823    public HandlerRegistration addHideHandler(HideHandler handler) {
824        return addHandler(handler, HideEvent.getType());
825    }
826
827    @Override
828    public HandlerRegistration addHiddenHandler(HiddenHandler handler) {
829        return addHandler(handler, HiddenEvent.getType());
830    }
831
832    @Override
833    public HandlerRegistration addRenderedHandler(RenderedHandler handler) {
834        return addHandler(handler, RenderedEvent.getType());
835    }
836
837    @Override
838    public HandlerRegistration addRefreshedHandler(RefreshedHandler handler) {
839        return addHandler(handler, RefreshedEvent.getType());
840    }
841
842    /**
843     * Force a re-render of the bootstrap-select UI. This is useful if you programmatically
844     * change any underlying values that affect the layout of the element.
845     */
846    public void render() {
847        if (isAttached())
848            command(getElement(), SelectCommand.RENDER);
849    }
850
851    /**
852     * Toggles the select menu open/closed.
853     */
854    public void toggle() {
855        if (isAttached())
856            command(getElement(), SelectCommand.TOGGLE);
857    }
858
859    /**
860     * Enables the device's native menu for select menus.
861     */
862    public void mobile() {
863        if (isAttached())
864            command(getElement(), SelectCommand.MOBILE);
865    }
866
867    /**
868     * WHEN CHANGING ANY SETTINGS CALL REFRESH AFTER!!
869     */
870    public void refresh() {
871        if (isAttached())
872            command(getElement(), SelectCommand.REFRESH);
873    }
874
875    /**
876     * Shows the select. This only affects the visibility of
877     * the select itself.
878     */
879    public void show() {
880        if (isAttached())
881            command(getElement(), SelectCommand.SHOW);
882        else
883            super.setVisible(true);
884    }
885
886    /**
887     * Hides the select. This only affects the visibility of
888     * the select itself.
889     */
890    public void hide() {
891        if (isAttached())
892            command(getElement(), SelectCommand.HIDE);
893        else
894            super.setVisible(false);
895    }
896
897    @Override
898    public void setVisible(boolean visible) {
899        if (visible)
900            show();
901        else
902            hide();
903    }
904
905    @Override
906    public boolean isVisible() {
907        if (isAttached()) {
908            return isVisible(selectElement.getParentElement());
909        }
910        return super.isVisible();
911    }
912
913    private native void initialize(Element e, SelectOptions options) /*-{
914        $wnd.jQuery(e).selectpicker(options);
915    }-*/;
916
917    /**
918     * Binds the select events.
919     *
920     * @param e
921     */
922    private native void bindSelectEvents(Element e) /*-{
923        var select = this;
924        $wnd.jQuery(e).on(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::LOADED_EVENT, function(event) {
925            @org.gwtbootstrap3.extras.select.client.ui.event.LoadedEvent::fire(Lorg/gwtbootstrap3/extras/select/client/ui/event/HasLoadedHandlers;)(select);
926        });
927        $wnd.jQuery(e).on(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::CHANGED_EVENT, function(event, clickedIndex, newValue, oldValue) {
928            select.@org.gwtbootstrap3.extras.select.client.ui.SelectBase::onValueChange()();
929        });
930        $wnd.jQuery(e).on(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::SHOW_EVENT, function(event) {
931            @org.gwtbootstrap3.extras.select.client.ui.event.ShowEvent::fire(Lorg/gwtbootstrap3/extras/select/client/ui/event/HasShowHandlers;)(select);
932        });
933        $wnd.jQuery(e).on(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::SHOWN_EVENT, function(event) {
934            @org.gwtbootstrap3.extras.select.client.ui.event.ShownEvent::fire(Lorg/gwtbootstrap3/extras/select/client/ui/event/HasShownHandlers;)(select);
935        });
936        $wnd.jQuery(e).on(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::HIDE_EVENT, function(event) {
937            @org.gwtbootstrap3.extras.select.client.ui.event.HideEvent::fire(Lorg/gwtbootstrap3/extras/select/client/ui/event/HasHideHandlers;)(select);
938        });
939        $wnd.jQuery(e).on(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::HIDDEN_EVENT, function(event) {
940            @org.gwtbootstrap3.extras.select.client.ui.event.HiddenEvent::fire(Lorg/gwtbootstrap3/extras/select/client/ui/event/HasHiddenHandlers;)(select);
941        });
942        $wnd.jQuery(e).on(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::RENDERED_EVENT, function(event) {
943            @org.gwtbootstrap3.extras.select.client.ui.event.RenderedEvent::fire(Lorg/gwtbootstrap3/extras/select/client/ui/event/HasRenderedHandlers;)(select);
944        });
945        $wnd.jQuery(e).on(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::REFRESHED_EVENT, function(event) {
946            @org.gwtbootstrap3.extras.select.client.ui.event.RefreshedEvent::fire(Lorg/gwtbootstrap3/extras/select/client/ui/event/HasRefreshedHandlers;)(select);
947        });
948    }-*/;
949
950    /**
951     * Unbinds the select events.
952     *
953     * @param e
954     */
955    private native void unbindSelectEvents(Element e) /*-{
956        $wnd.jQuery(e).off(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::LOADED_EVENT);
957        $wnd.jQuery(e).off(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::CHANGED_EVENT);
958        $wnd.jQuery(e).off(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::SHOW_EVENT);
959        $wnd.jQuery(e).off(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::SHOWN_EVENT);
960        $wnd.jQuery(e).off(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::HIDE_EVENT);
961        $wnd.jQuery(e).off(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::HIDDEN_EVENT);
962        $wnd.jQuery(e).off(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::RENDERED_EVENT);
963        $wnd.jQuery(e).off(@org.gwtbootstrap3.extras.select.client.ui.event.HasAllSelectHandlers::REFRESHED_EVENT);
964    }-*/;
965
966    protected native void command(Element e, String command) /*-{
967        $wnd.jQuery(e).selectpicker(command);
968    }-*/;
969}