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.timepicker;
021
022import com.google.gwt.core.client.Scheduler;
023import com.google.gwt.dom.client.Document;
024import com.google.gwt.dom.client.Style;
025import com.google.gwt.event.dom.client.BlurEvent;
026import com.google.gwt.event.dom.client.FocusEvent;
027import com.google.gwt.event.logical.shared.*;
028import com.google.gwt.event.shared.HandlerRegistration;
029import com.google.gwt.i18n.shared.DateTimeFormat;
030import com.google.gwt.user.client.DOM;
031import gwt.material.design.addins.client.MaterialAddins;
032import gwt.material.design.addins.client.base.constants.AddinsCssName;
033import gwt.material.design.addins.client.timepicker.js.JsTimePicker;
034import gwt.material.design.addins.client.timepicker.js.JsTimePickerOptions;
035import gwt.material.design.client.MaterialDesignBase;
036import gwt.material.design.client.base.*;
037import gwt.material.design.client.base.mixin.*;
038import gwt.material.design.client.constants.*;
039import gwt.material.design.client.ui.*;
040import gwt.material.design.client.ui.html.Label;
041
042import java.util.Date;
043
044import static gwt.material.design.addins.client.timepicker.js.JsTimePicker.$;
045
046//@formatter:off
047
048/**
049 * Material Time Picker - provide a simple way to select a single value from a pre-determined set.
050 * <p>
051 * <h3>XML Namespace Declaration</h3>
052 * <pre>
053 * {@code
054 * xmlns:ma='urn:import:gwt.material.design.addins.client'
055 * }
056 * </pre>
057 * <p>
058 * <h3>UiBinder Usage:</h3>
059 * <pre>
060 * {@code <ma:timepicker.MaterialTimePicker placeholder="Time Arrival" />}
061 * </pre>
062 *
063 * @author kevzlou7979
064 * @author Ben Dol
065 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#timepickers">Material Pickers</a>
066 * @see <a href="https://material.io/guidelines/components/pickers.html#pickers-time-pickers">Material Design Specification</a>
067 * @see <a href="https://github.com/weareoutman/clockpicker">ClockPicker 0.0.7</a>
068 */
069//@formatter:on
070public class MaterialTimePicker extends AbstractValueWidget<Date> implements JsLoader, HasPlaceholder,
071        HasCloseHandlers<Date>, HasOpenHandlers<Date>, HasIcon, HasReadOnly {
072
073    static {
074        if (MaterialAddins.isDebug()) {
075            MaterialDesignBase.injectDebugJs(MaterialTimePickerDebugClientBundle.INSTANCE.timepickerJsDebug());
076            MaterialDesignBase.injectCss(MaterialTimePickerDebugClientBundle.INSTANCE.timepickerCssDebug());
077        } else {
078            MaterialDesignBase.injectJs(MaterialTimePickerClientBundle.INSTANCE.timepickerJs());
079            MaterialDesignBase.injectCss(MaterialTimePickerClientBundle.INSTANCE.timepickerCss());
080        }
081    }
082
083    private Date time;
084    private String placeholder;
085    private MaterialPanel container = new MaterialPanel();
086    private MaterialInput timeInput = new MaterialInput();
087    private MaterialLabel errorLabel = new MaterialLabel();
088    private Label placeholderLabel = new Label();
089    private MaterialIcon icon = new MaterialIcon();
090    private JsTimePickerOptions options = new JsTimePickerOptions();
091
092    private ToggleStyleMixin<MaterialInput> validMixin;
093    private ErrorMixin<AbstractValueWidget, MaterialLabel> errorMixin;
094    private ReadOnlyMixin<MaterialTimePicker, MaterialInput> readOnlyMixin;
095    private EnabledMixin<MaterialWidget> enabledMixin;
096
097
098    public MaterialTimePicker() {
099        super(Document.get().createElement("div"), AddinsCssName.TIMEPICKER, CssName.INPUT_FIELD);
100    }
101
102    public MaterialTimePicker(String placeholder) {
103        this();
104        setPlaceholder(placeholder);
105    }
106
107    public MaterialTimePicker(String placeholder, Date value) {
108        this(placeholder);
109        setValue(value);
110    }
111
112    @Override
113    protected void onLoad() {
114        super.onLoad();
115
116        setUniqueId(DOM.createUniqueId());
117        timeInput.setType(InputType.TEXT);
118        container.add(placeholderLabel);
119        container.add(timeInput);
120        container.add(errorLabel);
121        add(container);
122        timeInput.getElement().setAttribute("type", "text");
123
124        load();
125    }
126
127    @Override
128    public void load() {
129        options.beforeShow = this::beforeShow;
130        options.afterShow = this::afterShow;
131        options.afterHide = this::afterHide;
132
133        $(timeInput.getElement()).lolliclock(options);
134        $(timeInput.getElement()).blur();
135
136        registerHandler(addOrientationChangeHandler(event -> {
137            JsTimePicker.$(timeInput.getElement()).lolliclock("setOrientation", event.getOrientation().getCssName());
138        }));
139    }
140
141    @Override
142    protected void onUnload() {
143        super.onUnload();
144
145        unload();
146    }
147
148    @Override
149    public void unload() {
150        $(timeInput.getElement()).lolliclock("remove");
151    }
152
153    @Override
154    public void reload() {
155        unload();
156        load();
157    }
158
159    /**
160     * Programmatically open the time picker component
161     */
162    public void open() {
163        Scheduler.get().scheduleDeferred(() -> $(timeInput.getElement()).lolliclock("show"));
164    }
165
166    /**
167     * Programmatically close the time picker component
168     */
169    public void close() {
170        Scheduler.get().scheduleDeferred(() -> $(timeInput.getElement()).lolliclock("hide"));
171    }
172
173    @Override
174    public void clear() {
175        time = null;
176        clearErrorOrSuccess();
177        placeholderLabel.removeStyleName(CssName.ACTIVE);
178        timeInput.removeStyleName(CssName.VALID);
179        $(timeInput.getElement()).val("");
180    }
181
182    /**
183     * Side effects:
184     * <ul>
185     * <li>Resets the time to <i>now<i></li>
186     * <li>Clears errors/success message</li>
187     * </ul>
188     */
189    public void reset() {
190        setValue(new Date());
191        clearErrorOrSuccess();
192    }
193
194    public boolean isAutoClose() {
195        return options.autoclose;
196    }
197
198    public void setAutoClose(boolean autoClose) {
199        options.autoclose = autoClose;
200    }
201
202    /**
203     * False (default) change to 24 hours system.
204     *
205     * @return <code>false</code> in case 12 hours mode is set;
206     * <code>true</code> otherwise.
207     */
208    public boolean isHour24() {
209        return options.hour24;
210    }
211
212    /**
213     * Set the time to 24 hour mode.
214     */
215    public void setHour24(boolean hour24) {
216        options.hour24 = hour24;
217    }
218
219    /**
220     * @return The placeholder text.
221     */
222    @Override
223    public String getPlaceholder() {
224        return this.placeholder;
225    }
226
227    /**
228     * @param placeholder The placeholder text to set.
229     */
230    @Override
231    public void setPlaceholder(String placeholder) {
232        this.placeholder = placeholder;
233        placeholderLabel.setText(placeholder);
234    }
235
236    /**
237     * Called after the lolliclock event <code>afterShow</code>.
238     */
239    protected void beforeShow() {
240        timeInput.getElement().blur();
241
242        // Add class 'valid' for visual feedback.
243        getValidMixin().setOn(true);
244    }
245
246    /**
247     * Called after the lolliclock event <code>afterShow</code>.
248     */
249    protected void afterShow() {
250        OpenEvent.fire(this, this.time);
251        fireEvent(new FocusEvent() {});
252    }
253
254    /**
255     * Called after the lolliclock event <code>afterHide</code>.
256     */
257    protected void afterHide() {
258        String timeString = getTime();
259        Date parsedDate = null;
260
261        if (timeString != null && !timeString.equals("")) {
262            try {
263                parsedDate = DateTimeFormat.getFormat(options.hour24 ? "HH:mm" : "hh:mm aa").parse(timeString);
264            } catch (IllegalArgumentException e) {
265                // Silently catch parse errors
266            }
267        }
268
269        setValue(parsedDate, true);
270
271        // Remove class 'valid' after hide.
272        getValidMixin().setOn(false);
273
274        CloseEvent.fire(this, this.time);
275        fireEvent(new BlurEvent() {});
276    }
277
278    protected String getTime() {
279        return $(timeInput.getElement()).val().toString();
280    }
281
282    @Override
283    public Date getValue() {
284        return time;
285    }
286
287    @Override
288    public void setValue(Date time, boolean fireEvents) {
289        this.time = time;
290        if (this.time == null) {
291            return;
292        }
293        placeholderLabel.removeStyleName(CssName.ACTIVE);
294        placeholderLabel.addStyleName(CssName.ACTIVE);
295        $(timeInput.getElement()).val(DateTimeFormat.getFormat(options.hour24 ? "HH:mm" : "hh:mm aa").format(time));
296        super.setValue(time, fireEvents);
297    }
298
299    public String getUniqueId() {
300        return options.uniqueId;
301    }
302
303    public void setUniqueId(String uniqueId) {
304        options.uniqueId = uniqueId;
305    }
306
307    public String getCancelText() {
308        return options.cancelText;
309    }
310
311    /**
312     * Set the "Cancel" text located on TimePicker's action buttons
313     */
314    public void setCancelText(String cancelText) {
315        options.cancelText = cancelText;
316
317        if (isAttached()) {
318            reload();
319        }
320    }
321
322    public String getOkText() {
323        return options.okText;
324    }
325
326    /**
327     * Set the "Ok" text located on TimePicker's action buttons
328     */
329    public void setOkText(String okText) {
330        options.okText = okText;
331
332        if (isAttached()) {
333            reload();
334        }
335    }
336
337    @Override
338    public MaterialIcon getIcon() {
339        return icon;
340    }
341
342    @Override
343    public void setIconType(IconType iconType) {
344        icon.setIconType(iconType);
345        icon.setIconPrefix(true);
346        errorLabel.setPaddingLeft(44);
347        container.insert(icon, 0);
348    }
349
350    @Override
351    public void setIconPosition(IconPosition position) {
352        icon.setIconPosition(position);
353    }
354
355    @Override
356    public void setIconSize(IconSize size) {
357        icon.setIconSize(size);
358    }
359
360    @Override
361    public void setIconFontSize(double size, Style.Unit unit) {
362        icon.setIconFontSize(size, unit);
363    }
364
365    @Override
366    public void setIconColor(Color iconColor) {
367        icon.setIconColor(iconColor);
368    }
369
370    @Override
371    public Color getIconColor() {
372        return getIcon().getIconColor();
373    }
374
375    @Override
376    public void setIconPrefix(boolean prefix) {
377        icon.setIconPrefix(prefix);
378    }
379
380    @Override
381    public boolean isIconPrefix() {
382        return icon.isIconPrefix();
383    }
384
385    @Override
386    public void setEnabled(boolean enabled) {
387        super.setEnabled(enabled);
388
389        getEnabledMixin().updateWaves(enabled, this);
390    }
391
392    @Override
393    protected EnabledMixin<MaterialWidget> getEnabledMixin() {
394        if (enabledMixin == null) {
395            enabledMixin = new EnabledMixin<>(timeInput);
396        }
397        return enabledMixin;
398    }
399
400    @Override
401    public ErrorMixin<AbstractValueWidget, MaterialLabel> getErrorMixin() {
402        if (errorMixin == null) {
403            errorMixin = new ErrorMixin<>(this, errorLabel, timeInput, placeholderLabel);
404        }
405        return errorMixin;
406    }
407
408    protected ReadOnlyMixin<MaterialTimePicker, MaterialInput> getReadOnlyMixin() {
409        if (readOnlyMixin == null) {
410            readOnlyMixin = new ReadOnlyMixin<>(this, timeInput);
411        }
412        return readOnlyMixin;
413    }
414
415    protected ToggleStyleMixin<MaterialInput> getValidMixin() {
416        if (validMixin == null) {
417            validMixin = new ToggleStyleMixin<>(timeInput, CssName.VALID);
418        }
419        return validMixin;
420    }
421
422    @Override
423    public void setReadOnly(boolean value) {
424        getReadOnlyMixin().setReadOnly(value);
425    }
426
427    @Override
428    public boolean isReadOnly() {
429        return getReadOnlyMixin().isReadOnly();
430    }
431
432    @Override
433    public void setToggleReadOnly(boolean toggle) {
434        getReadOnlyMixin().setToggleReadOnly(toggle);
435    }
436
437    @Override
438    public boolean isToggleReadOnly() {
439        return getReadOnlyMixin().isToggleReadOnly();
440    }
441
442    public MaterialInput getTimeInput() {
443        return timeInput;
444    }
445
446    public MaterialPanel getContainer() {
447        return container;
448    }
449
450    public MaterialLabel getErrorLabel() {
451        return errorLabel;
452    }
453
454    public Label getPlaceholderLabel() {
455        return placeholderLabel;
456    }
457
458    @Override
459    public HandlerRegistration addCloseHandler(final CloseHandler<Date> handler) {
460        return addHandler(handler, CloseEvent.getType());
461    }
462
463    @Override
464    public HandlerRegistration addOpenHandler(final OpenHandler<Date> handler) {
465        return addHandler(handler, OpenEvent.getType());
466    }
467}