001package org.gwtbootstrap3.client.ui;
002
003/*
004 * #%L
005 * GwtBootstrap3
006 * %%
007 * Copyright (C) 2015 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 */
022
023import java.util.Collection;
024import java.util.List;
025
026import org.gwtbootstrap3.client.ui.base.HasAutoComplete;
027import org.gwtbootstrap3.client.ui.base.HasId;
028import org.gwtbootstrap3.client.ui.base.HasPlaceholder;
029import org.gwtbootstrap3.client.ui.base.HasResponsiveness;
030import org.gwtbootstrap3.client.ui.base.HasSize;
031import org.gwtbootstrap3.client.ui.base.ValueBoxBase;
032import org.gwtbootstrap3.client.ui.base.helper.StyleHelper;
033import org.gwtbootstrap3.client.ui.base.mixin.BlankValidatorMixin;
034import org.gwtbootstrap3.client.ui.base.mixin.EnabledMixin;
035import org.gwtbootstrap3.client.ui.base.mixin.ErrorHandlerMixin;
036import org.gwtbootstrap3.client.ui.base.mixin.IdMixin;
037import org.gwtbootstrap3.client.ui.constants.DeviceSize;
038import org.gwtbootstrap3.client.ui.constants.InputSize;
039import org.gwtbootstrap3.client.ui.constants.Styles;
040import org.gwtbootstrap3.client.ui.form.error.ErrorHandler;
041import org.gwtbootstrap3.client.ui.form.error.ErrorHandlerType;
042import org.gwtbootstrap3.client.ui.form.error.HasErrorHandler;
043import org.gwtbootstrap3.client.ui.form.validator.HasBlankValidator;
044import org.gwtbootstrap3.client.ui.form.validator.HasValidators;
045import org.gwtbootstrap3.client.ui.form.validator.ValidationChangedEvent.ValidationChangedHandler;
046import org.gwtbootstrap3.client.ui.form.validator.Validator;
047
048import com.google.gwt.core.client.Scheduler;
049import com.google.gwt.core.client.Scheduler.ScheduledCommand;
050import com.google.gwt.dom.client.Element;
051import com.google.gwt.dom.client.Style.Display;
052import com.google.gwt.dom.client.Style.Unit;
053import com.google.gwt.editor.client.EditorError;
054import com.google.gwt.editor.client.HasEditorErrors;
055import com.google.gwt.event.logical.shared.ResizeEvent;
056import com.google.gwt.event.logical.shared.ResizeHandler;
057import com.google.gwt.user.client.Timer;
058import com.google.gwt.user.client.Window;
059import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
060import com.google.gwt.user.client.ui.PopupPanel;
061import com.google.gwt.user.client.ui.SuggestOracle;
062import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
063import com.google.gwt.user.client.ui.Widget;
064import com.google.web.bindery.event.shared.HandlerRegistration;
065
066/**
067 * Wrapper for a {@link com.google.gwt.user.client.ui.SuggestBox}.<br/>
068 * <br/>
069 * The default style is inherited from the {@link Styles#DROPDOWN_MENU}. Styling of the suggestions items need
070 * a bit of css in order to be pleasing to the eye.
071 * 
072 * <pre>
073 * {@code
074 * .dropdown-menu .item {
075 *     padding: 5px;
076 * }
077 *  
078 * .dropdown-menu .item-selected {
079 *     background-color: #eee;
080 * }
081 * 
082 * }
083 * </pre>
084 * 
085 * @author Steven Jardine
086 */
087public class SuggestBox extends com.google.gwt.user.client.ui.SuggestBox implements HasId, HasResponsiveness, HasPlaceholder,
088        HasAutoComplete, HasSize<InputSize>, HasEditorErrors<String>, HasErrorHandler, HasValidators<String>,
089        HasBlankValidator<String> {
090
091    static class CustomSuggestionDisplay extends DefaultSuggestionDisplay {
092
093        private ResizeHandler popupResizeHandler = null;
094
095        public CustomSuggestionDisplay() {
096            super();
097            final PopupPanel popup = getPopupPanel();
098            popup.setStyleName(Styles.DROPDOWN_MENU);
099            popup.getElement().getStyle().setDisplay(Display.BLOCK);
100        }
101
102        /**
103         * Resize the popup panel to the size of the suggestBox and place it below the SuggestBox. This is not
104         * ideal but works better in a mobile environment.
105         *
106         * @param box the box the SuggestBox.
107         */
108        private void resizePopup(final com.google.gwt.user.client.ui.SuggestBox box) {
109            Scheduler.get().scheduleDeferred(new ScheduledCommand() {
110                @Override
111                public void execute() {
112                    PopupPanel panel = getPopupPanel();
113                    if (box.isAttached())
114                    {
115                      Element e = box.getElement();
116                      panel.setWidth((e.getAbsoluteRight() - e.getAbsoluteLeft() - 2) + Unit.PX.getType());
117                      panel.setPopupPosition(e.getAbsoluteLeft(), e.getAbsoluteBottom());
118                    }
119                    else
120                    {
121                      panel.hide();
122                    }  
123                }
124            });
125        }
126
127        /** {@inheritDoc} */
128        @Override
129        protected void showSuggestions(final com.google.gwt.user.client.ui.SuggestBox suggestBox,
130                final Collection<? extends Suggestion> suggestions, final boolean isDisplayStringHTML,
131                final boolean isAutoSelectEnabled, final SuggestionCallback callback) {
132            super.showSuggestions(suggestBox, suggestions, isDisplayStringHTML, isAutoSelectEnabled, callback);
133            resizePopup(suggestBox);
134            if (popupResizeHandler == null) {
135                popupResizeHandler = new ResizeHandler() {
136                    private Timer timer = new Timer() {
137                        public void run() {
138                            resizePopup(suggestBox);
139                        }
140                    };
141
142                    @Override
143                    public void onResize(ResizeEvent event) {
144                        timer.schedule(250);
145                    }
146                };
147                Window.addResizeHandler(popupResizeHandler);
148            }
149            // Try and set the z-index of the popup to the same as the SuggestBox.
150            if (!suggestBox.getElement().getStyle().getZIndex().equals("")) {
151                try {
152                    getPopupPanel().getElement().getStyle()
153                            .setZIndex(Integer.valueOf(suggestBox.getElement().getStyle().getZIndex()));
154                } catch (Exception e) {
155                    // Do nothing. We tried....
156                }
157            }
158        }
159    }
160
161    private final EnabledMixin<SuggestBox> enabledMixin = new EnabledMixin<SuggestBox>(this);
162
163    private final ErrorHandlerMixin<String> errorHandlerMixin = new ErrorHandlerMixin<String>(this);
164
165    private final IdMixin<SuggestBox> idMixin = new IdMixin<SuggestBox>(this);
166
167    private final BlankValidatorMixin<SuggestBox, String> validatorMixin = new BlankValidatorMixin<SuggestBox, String>(this,
168        errorHandlerMixin.getErrorHandler());
169
170    /**
171     * Constructor for {@link SuggestBox}. Creates a {@link MultiWordSuggestOracle} and {@link TextBox} to use
172     * with this {@link SuggestBox}.
173     */
174    public SuggestBox() {
175        this(new MultiWordSuggestOracle());
176    }
177
178    /**
179     * Constructor for {@link SuggestBox}. Creates a {@link TextBox} to use with this {@link SuggestBox}.
180     *
181     * @param oracle the oracle for this <code>SuggestBox</code>
182     */
183    public SuggestBox(SuggestOracle oracle) {
184        this(oracle, new TextBox());
185    }
186
187    /**
188     * Constructor for {@link SuggestBox}. The text box will be removed from it's current location and wrapped
189     * by the {@link SuggestBox}.
190     *
191     * @param oracle supplies suggestions based upon the current contents of the text widget
192     * @param box the text widget
193     */
194    public SuggestBox(SuggestOracle oracle, ValueBoxBase<String> box) {
195        this(oracle, box, new CustomSuggestionDisplay());
196    }
197
198    /**
199     * Constructor for {@link SuggestBox}. The text box will be removed from it's current location and wrapped
200     * by the {@link SuggestBox}.
201     *
202     * @param oracle supplies suggestions based upon the current contents of the text widget
203     * @param box the text widget
204     * @param suggestDisplay the class used to display suggestions
205     */
206    public SuggestBox(SuggestOracle oracle, ValueBoxBase<String> box, SuggestionDisplay suggestDisplay) {
207        super(oracle, box, suggestDisplay);
208        setStyleName(Styles.FORM_CONTROL);
209    }
210
211    /** {@inheritDoc} */
212    @Override
213    public HandlerRegistration addValidationChangedHandler(ValidationChangedHandler handler) {
214        return validatorMixin.addValidationChangedHandler(handler);
215    }
216
217    /** {@inheritDoc} */
218    @Override
219    public void addValidator(Validator<String> validator) {
220        validatorMixin.addValidator(validator);
221    }
222
223    /** {@inheritDoc} */
224    @Override
225    public boolean getAllowBlank() {
226        return validatorMixin.getAllowBlank();
227    }
228
229    /** {@inheritDoc} */
230    @Override
231    public String getAutoComplete() {
232        return getElement().getAttribute(AUTO_COMPLETE);
233    }
234
235    /** {@inheritDoc} */
236    @Override
237    public ErrorHandler getErrorHandler() {
238        return errorHandlerMixin.getErrorHandler();
239    }
240
241    /** {@inheritDoc} */
242    @Override
243    public ErrorHandlerType getErrorHandlerType() {
244        return errorHandlerMixin.getErrorHandlerType();
245    }
246
247    /**
248     * {@inheritDoc}
249     */
250    @Override
251    public String getId() {
252        return idMixin.getId();
253    }
254
255    /** {@inheritDoc} */
256    @Override
257    public String getPlaceholder() {
258        return getElement().getAttribute(PLACEHOLDER);
259    }
260
261    /** {@inheritDoc} */
262    @Override
263    public InputSize getSize() {
264        return InputSize.fromStyleName(getStyleName());
265    }
266
267    @Override
268    public boolean getValidateOnBlur() {
269        return validatorMixin.getValidateOnBlur();
270    }
271
272    /** {@inheritDoc} */
273    @Override
274    public boolean isEnabled() {
275        return enabledMixin.isEnabled();
276    }
277
278    @Override
279    protected void onAttach() {
280        super.onAttach();
281        // Try and set the z-index.
282        Integer zIndex = null;
283        Widget widget = this;
284        while (zIndex == null && widget != null) {
285            if (!widget.getElement().getStyle().getZIndex().equals("")) {
286                try {
287                    zIndex = Integer.valueOf(widget.getElement().getStyle().getZIndex());
288                    zIndex += 10;
289                } catch (Exception e) {
290                    zIndex = null;
291                }
292            }
293            widget = widget.getParent();
294        }
295        if (zIndex != null) {
296            getElement().getStyle().setZIndex(zIndex);
297        }
298    }
299
300    /** {@inheritDoc} */
301    @Override
302    public boolean removeValidator(Validator<String> validator) {
303        return validatorMixin.removeValidator(validator);
304    }
305
306    @Override
307    public void reset() {
308        validatorMixin.reset();
309    }
310
311    @Override
312    public void setAllowBlank(boolean allowBlank) {
313        validatorMixin.setAllowBlank(allowBlank);
314    }
315
316    /** {@inheritDoc} */
317    @Override
318    public void setAutoComplete(final boolean autoComplete) {
319        getElement().setAttribute(AUTO_COMPLETE, autoComplete ? ON : OFF);
320    }
321
322    /** {@inheritDoc} */
323    @Override
324    public void setEnabled(final boolean enabled) {
325        enabledMixin.setEnabled(enabled);
326    }
327
328    /** {@inheritDoc} */
329    @Override
330    public void setErrorHandler(ErrorHandler handler) {
331        errorHandlerMixin.setErrorHandler(handler);
332        validatorMixin.setErrorHandler(handler);
333    }
334
335    /** {@inheritDoc} */
336    @Override
337    public void setErrorHandlerType(ErrorHandlerType type) {
338        errorHandlerMixin.setErrorHandlerType(type);
339    }
340
341    /** {@inheritDoc} */
342    @Override
343    public void setHiddenOn(final DeviceSize deviceSize) {
344        StyleHelper.setHiddenOn(this, deviceSize);
345    }
346
347    /** {@inheritDoc} */
348    @Override
349    public void setId(final String id) {
350        idMixin.setId(id);
351    }
352
353    /** {@inheritDoc} */
354    @Override
355    public void setPlaceholder(final String placeHolder) {
356        getElement().setAttribute(PLACEHOLDER, placeHolder != null ? placeHolder : "");
357    }
358
359    /** {@inheritDoc} */
360    @Override
361    public void setSize(final InputSize size) {
362        StyleHelper.addUniqueEnumStyleName(this, InputSize.class, size);
363    }
364
365    @Override
366    public void setValidateOnBlur(boolean validateOnBlur) {
367        validatorMixin.setValidateOnBlur(validateOnBlur);
368    }
369
370    @Override
371    public void setValidators(@SuppressWarnings("unchecked") Validator<String>... validators) {
372        validatorMixin.setValidators(validators);
373    }
374
375    /** {@inheritDoc} */
376    @Override
377    public void setVisibleOn(final DeviceSize deviceSize) {
378        StyleHelper.setVisibleOn(this, deviceSize);
379    }
380
381    /** {@inheritDoc} */
382    @Override
383    public void showErrors(List<EditorError> errors) {
384        errorHandlerMixin.showErrors(errors);
385    }
386
387    /** {@inheritDoc} */
388    @Override
389    public boolean validate() {
390        return validatorMixin.validate();
391    }
392
393    /** {@inheritDoc} */
394    @Override
395    public boolean validate(boolean show) {
396        return validatorMixin.validate(show);
397    }
398
399}