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.client.ui;
021
022import com.google.gwt.core.client.JsDate;
023import com.google.gwt.core.client.Scheduler;
024import com.google.gwt.core.client.ScriptInjector;
025import com.google.gwt.dom.client.Document;
026import com.google.gwt.dom.client.Element;
027import com.google.gwt.dom.client.Style;
028import com.google.gwt.event.dom.client.BlurEvent;
029import com.google.gwt.event.dom.client.FocusEvent;
030import com.google.gwt.event.logical.shared.*;
031import com.google.gwt.event.shared.HandlerRegistration;
032import com.google.gwt.user.client.DOM;
033import gwt.material.design.client.base.*;
034import gwt.material.design.client.base.mixin.ErrorMixin;
035import gwt.material.design.client.base.mixin.ReadOnlyMixin;
036import gwt.material.design.client.constants.*;
037import gwt.material.design.client.js.JsDatePickerOptions;
038import gwt.material.design.client.js.JsMaterialElement;
039import gwt.material.design.client.ui.html.DateInput;
040import gwt.material.design.client.ui.html.Label;
041
042import java.util.Date;
043
044import static gwt.material.design.client.js.JsMaterialElement.$;
045
046//@formatter:off
047
048/**
049 * Material Date Picker will provide a visual calendar to your apps.
050 * <p/>
051 * <h3>UiBinder Usage:</h3>
052 * {@code
053 * <m:MaterialDatePicker ui:field="datePicker">
054 * }
055 * <h3>Java Usage:</h3>
056 * {@code
057 * datePicker.setDate(new Date());
058 * }
059 *
060 * @author kevzlou7979
061 * @author Ben Dol
062 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#pickers">Material Date Picker</a>
063 * @see <a href="https://material.io/guidelines/components/pickers.html#pickers-date-pickers">Material Design Specification</a>
064 */
065//@formatter:on
066public class MaterialDatePicker extends AbstractValueWidget<Date> implements JsLoader, HasPlaceholder,
067        HasOpenHandlers<MaterialDatePicker>, HasCloseHandlers<MaterialDatePicker>, HasIcon, HasReadOnly {
068
069    /**
070     * Enum for identifying various selection types for the picker.
071     */
072    public enum MaterialDatePickerType {
073        DAY,
074        MONTH_DAY,
075        YEAR_MONTH_DAY,
076        YEAR
077    }
078
079    private String placeholder;
080    private String tabIndex = "0";
081    private Date date;
082    private Date dateMin;
083    private Date dateMax;
084    private DatePickerLanguage language;
085    private Orientation orientation;
086    private DatePickerContainer container = DatePickerContainer.SELF;
087    private MaterialDatePickerType selectionType = MaterialDatePickerType.DAY;
088    private int yearsToDisplay = 10;
089    private boolean autoClose;
090    private boolean suppressChangeEvent;
091    protected Element pickatizedDateInput;
092    private DateInput dateInput = new DateInput();
093    private Label label = new Label();
094    private MaterialLabel placeholderLabel = new MaterialLabel();
095    private MaterialLabel errorLabel = new MaterialLabel();
096    private MaterialIcon icon = new MaterialIcon();
097
098    private JsDatePickerOptions options = new JsDatePickerOptions();
099    private HandlerRegistration autoCloseHandlerRegistration, attachHandler;
100
101    private ErrorMixin<AbstractValueWidget, MaterialLabel> errorMixin;
102    private ReadOnlyMixin<MaterialDatePicker, DateInput> readOnlyMixin;
103
104    public MaterialDatePicker() {
105        super(Document.get().createDivElement(), CssName.INPUT_FIELD);
106
107        add(dateInput);
108        label.add(placeholderLabel);
109        add(label);
110        add(errorLabel);
111    }
112
113    public MaterialDatePicker(String placeholder) {
114        this();
115        setPlaceholder(placeholder);
116    }
117
118    public MaterialDatePicker(String placeholder, Date value) {
119        this(placeholder);
120        setDate(value);
121    }
122
123    public MaterialDatePicker(String placeholder, Date value, MaterialDatePickerType selectionType) {
124        this(placeholder, value);
125        setSelectionType(selectionType);
126    }
127
128    @Override
129    protected void onLoad() {
130        super.onLoad();
131
132        load();
133    }
134
135    @Override
136    public void load() {
137        pickatizedDateInput = $(dateInput.getElement()).pickadate(options).asElement();
138
139        getPicker().on("set", thing -> {
140            if (thing.hasOwnProperty("clear")) {
141                clear();
142            } else if (thing.hasOwnProperty("select")) {
143                select();
144            }
145        });
146
147        getPicker().on(options).on("open", (e, param1) -> {
148            onOpen();
149            return true;
150        }).on("close", (e, param1) -> {
151            onClose();
152            $(pickatizedDateInput).blur();
153            return true;
154        });
155
156        label.getElement().setAttribute("for", getPickerId());
157        setPopupEnabled(isEnabled());
158        setAutoClose(autoClose);
159        setDate(date);
160        setDateMin(dateMin);
161        setDateMax(dateMax);
162    }
163
164    @Override
165    public void onUnload() {
166        super.onUnload();
167
168        unload();
169    }
170
171    @Override
172    public void unload() {
173        JsMaterialElement picker = getPicker();
174        if (picker != null) {
175            picker.off("set");
176            picker.off("open");
177            picker.off("close");
178        }
179    }
180
181    @Override
182    public void reload() {
183        unload();
184        load();
185    }
186
187    /**
188     * As of now use {@link MaterialDatePicker#setSelectionType(MaterialDatePickerType)}
189     *
190     * @param type
191     */
192    @Deprecated
193    public void setDateSelectionType(MaterialDatePickerType type) {
194        if (type != null) {
195            this.selectionType = type;
196        }
197    }
198
199    public String getPickerId() {
200        return getPicker().get("id").toString();
201    }
202
203    public Element getPickerRootElement() {
204        return $("#" + getPickerId() + "_root").asElement();
205    }
206
207    /**
208     * Sets the current date of the picker.
209     *
210     * @param date - must not be <code>null</code>
211     */
212    public void setDate(Date date) {
213        setValue(date);
214    }
215
216    /**
217     * Get the minimum date limit.
218     */
219    public Date getDateMin() {
220        return dateMin;
221    }
222
223    /**
224     * Set the minimum date limit.
225     */
226    public void setDateMin(Date dateMin) {
227        this.dateMin = dateMin;
228
229        if (isAttached() && dateMin != null) {
230            getPicker().set("min", JsDate.create((double) dateMin.getTime()));
231        }
232    }
233
234    /**
235     * Get the maximum date limit.
236     */
237    public Date getDateMax() {
238        return dateMax;
239    }
240
241    /**
242     * Set the maximum date limit.
243     */
244    public void setDateMax(Date dateMax) {
245        this.dateMax = dateMax;
246
247        if (isAttached() && dateMax != null) {
248            getPicker().set("max", JsDate.create((double) dateMax.getTime()));
249        }
250    }
251
252    /**
253     * Set the pickers date.
254     */
255    public void setPickerDate(JsDate date, Element picker) {
256        try {
257            $(picker).pickadate("picker").set("select", date, () -> {
258                DOM.createFieldSet().setPropertyObject("muted", true);
259            });
260        } catch (Exception e) {
261            e.printStackTrace();
262        }
263    }
264
265    /**
266     * Get the pickers date.
267     */
268    protected Date getPickerDate() {
269        try {
270            JsDate pickerDate = getPicker().get("select").obj;
271            return new Date((long) pickerDate.getTime());
272        } catch (Exception e) {
273            e.printStackTrace();
274            return null;
275        }
276    }
277
278    protected JsMaterialElement getPicker() {
279        return $(pickatizedDateInput).pickadate("picker");
280    }
281
282    public Date getDate() {
283        return getPickerDate();
284    }
285
286    @Override
287    public String getPlaceholder() {
288        return placeholder;
289    }
290
291    @Override
292    public void setPlaceholder(String placeholder) {
293        this.placeholder = placeholder;
294
295        if (placeholder != null) {
296            placeholderLabel.setText(placeholder);
297        }
298    }
299
300    /**
301     * Get the pickers selection type.
302     */
303    public MaterialDatePickerType getSelectionType() {
304        return selectionType;
305    }
306
307    /**
308     * Set the pickers selection type.
309     */
310    public void setSelectionType(MaterialDatePickerType selectionType) {
311        this.selectionType = selectionType;
312        switch (selectionType) {
313            case MONTH_DAY:
314                options.selectMonths = true;
315                break;
316            case YEAR_MONTH_DAY:
317                options.selectYears = yearsToDisplay;
318                options.selectMonths = true;
319                break;
320            case YEAR:
321                options.selectYears = yearsToDisplay;
322                options.selectMonths = false;
323                break;
324        }
325    }
326
327    /**
328     * Set the pickers selection type with the ability to set the number of years to display
329     * in the dropdown list.
330     */
331    public void setSelectionType(MaterialDatePickerType selectionType, int yearsToDisplay) {
332        setSelectionType(selectionType);
333        setYearsToDisplay(yearsToDisplay);
334    }
335
336    @Override
337    public void setOrientation(Orientation orientation) {
338        this.orientation = orientation;
339
340        JsMaterialElement picker = getPicker();
341        if (picker != null && orientation != null) {
342            picker.root.removeClass(orientation.getCssName());
343        }
344        if (picker != null && orientation != null) {
345            picker.root.addClass(orientation.getCssName());
346        }
347    }
348
349    @Override
350    public Orientation getOrientation() {
351        return orientation;
352    }
353
354    @Override
355    public void setError(String error) {
356        super.setError(error);
357        dateInput.addStyleName(CssName.INVALID);
358        dateInput.removeStyleName(CssName.VALID);
359    }
360
361    @Override
362    public void setSuccess(String success) {
363        super.setSuccess(success);
364        dateInput.addStyleName(CssName.VALID);
365        dateInput.removeStyleName(CssName.INVALID);
366    }
367
368    @Override
369    public void clearErrorOrSuccess() {
370        super.clearErrorOrSuccess();
371        dateInput.removeStyleName(CssName.VALID);
372        dateInput.removeStyleName(CssName.INVALID);
373    }
374
375    public String getFormat() {
376        return options.format;
377    }
378
379    /**
380     * To call before initialization.
381     */
382    public void setFormat(String format) {
383        options.format = format;
384    }
385
386    @Override
387    public Date getValue() {
388        return getPickerDate();
389    }
390
391    @Override
392    public void setValue(Date value, boolean fireEvents) {
393        if (value == null) {
394            clear();
395            return;
396        }
397        this.date = value;
398        if (isAttached()) {
399            suppressChangeEvent = !fireEvents;
400            setPickerDate(JsDate.create((double) value.getTime()), pickatizedDateInput);
401            suppressChangeEvent = false;
402            label.addStyleName(CssName.ACTIVE);
403        }
404        super.setValue(value, fireEvents);
405    }
406
407    @Override
408    public void setValue(Date value) {
409        setValue(value, false);
410    }
411
412    @Override
413    public void setEnabled(boolean enabled) {
414        super.setEnabled(enabled);
415        dateInput.setEnabled(enabled);
416        if (isAttached()) {
417            setPopupEnabled(enabled);
418        }
419    }
420
421    @Override
422    public boolean isEnabled() {
423        return dateInput.isEnabled();
424    }
425
426    @Override
427    public void setTabIndex(int index) {
428        tabIndex = String.valueOf(index);
429        dateInput.setTabIndex(index);
430    }
431
432    @Override
433    public int getTabIndex() {
434        return dateInput.getTabIndex();
435    }
436
437    public DatePickerLanguage getLanguage() {
438        return language;
439    }
440
441    public void setLanguage(DatePickerLanguage language) {
442        this.language = language;
443
444        if (attachHandler != null) {
445            attachHandler.removeHandler();
446            attachHandler = null;
447        }
448
449        if (isAttached()) {
450            setupLanguage(language);
451        } else {
452            attachHandler = registerHandler(addAttachHandler(attachEvent -> setupLanguage(language)));
453        }
454    }
455
456    protected void setupLanguage(DatePickerLanguage language) {
457        if (language.getJs() != null) {
458            ScriptInjector.fromString(language.getJs().getText()).setWindow(ScriptInjector.TOP_WINDOW).inject();
459            getPicker().stop();
460            Scheduler.get().scheduleDeferred(() -> load());
461        }
462    }
463
464    @Override
465    public MaterialIcon getIcon() {
466        return icon;
467    }
468
469    @Override
470    public void setIconType(IconType iconType) {
471        icon.setIconType(iconType);
472        icon.setIconPrefix(true);
473        errorLabel.setPaddingLeft(44);
474        insert(icon, 0);
475    }
476
477    @Override
478    public void setIconPosition(IconPosition position) {
479        icon.setIconPosition(position);
480    }
481
482    @Override
483    public void setIconSize(IconSize size) {
484        icon.setIconSize(size);
485    }
486
487    @Override
488    public void setIconFontSize(double size, Style.Unit unit) {
489        icon.setIconFontSize(size, unit);
490    }
491
492    @Override
493    public void setIconColor(Color iconColor) {
494        icon.setIconColor(iconColor);
495    }
496
497    @Override
498    public Color getIconColor() {
499        return icon.getIconColor();
500    }
501
502    @Override
503    public void setIconPrefix(boolean prefix) {
504        icon.setIconPrefix(prefix);
505    }
506
507    @Override
508    public boolean isIconPrefix() {
509        return icon.isIconPrefix();
510    }
511
512    @Override
513    public void setReadOnly(boolean value) {
514        getReadOnlyMixin().setReadOnly(value);
515    }
516
517    @Override
518    public boolean isReadOnly() {
519        return getReadOnlyMixin().isReadOnly();
520    }
521
522    @Override
523    public void setToggleReadOnly(boolean toggle) {
524        getReadOnlyMixin().setToggleReadOnly(toggle);
525    }
526
527    @Override
528    public boolean isToggleReadOnly() {
529        return getReadOnlyMixin().isToggleReadOnly();
530    }
531
532    public DateInput getDateInput() {
533        return dateInput;
534    }
535
536    public boolean isAutoClose() {
537        return autoClose;
538    }
539
540    /**
541     * Enables or disables auto closing when selecting a date.
542     */
543    public void setAutoClose(boolean autoClose) {
544        this.autoClose = autoClose;
545
546        if (autoCloseHandlerRegistration != null) {
547            autoCloseHandlerRegistration.removeHandler();
548            autoCloseHandlerRegistration = null;
549        }
550
551        if (autoClose) {
552            autoCloseHandlerRegistration = registerHandler(addValueChangeHandler(event -> close()));
553        }
554    }
555
556    public int getYearsToDisplay() {
557        return options.selectYears;
558    }
559
560    /**
561     * Ability to set the number of years to display
562     * in the dropdown list.
563     */
564    public void setYearsToDisplay(int yearsToDisplay) {
565        options.selectYears = yearsToDisplay;
566    }
567
568    public DatePickerContainer getContainer() {
569        return container;
570    }
571
572    /**
573     * Set the Root Picker Container (Default : SELF)
574     */
575    public void setContainer(DatePickerContainer container) {
576        this.container = container;
577        options.container = container == DatePickerContainer.SELF ? getElement().getId() : container.getCssName();
578    }
579
580    public Label getLabel() {
581        return label;
582    }
583
584    public MaterialLabel getPlaceholderLabel() {
585        return placeholderLabel;
586    }
587
588    public MaterialLabel getErrorLabel() {
589        return errorLabel;
590    }
591
592    /**
593     * Programmatically close the date picker component
594     */
595    public void close() {
596        Scheduler.get().scheduleDeferred(() -> getPicker().close());
597    }
598
599    /**
600     * Programmatically open the date picker component
601     */
602    public void open() {
603        Scheduler.get().scheduleDeferred(() -> getPicker().open());
604    }
605
606    public boolean isOpen() {
607        return Boolean.parseBoolean(getPicker().get("open").toString());
608    }
609
610    protected void select() {
611        label.addStyleName(CssName.ACTIVE);
612        dateInput.addStyleName(CssName.VALID);
613
614        // Ensure the value change event is
615        // triggered on selecting a date if the picker is open
616        // to avoid conflicts on setValue(value, fireEvents).
617        if (isOpen() && !suppressChangeEvent) {
618            ValueChangeEvent.fire(this, getValue());
619        }
620    }
621
622    protected void onClose() {
623        CloseEvent.fire(this, this);
624        fireEvent(new BlurEvent() {});
625    }
626
627    protected void onOpen() {
628        label.addStyleName(CssName.ACTIVE);
629        dateInput.setFocus(true);
630        OpenEvent.fire(this, this);
631        fireEvent(new FocusEvent() {});
632    }
633
634    /**
635     * Replaced by {@link MaterialDatePicker#clear()}
636     */
637    @Deprecated
638    public void clearValues() {
639        clear();
640    }
641
642    /**
643     * Replace by {@link MaterialDatePicker#unload()}
644     */
645    @Deprecated
646    public void stop() {
647        unload();
648    }
649
650    @Override
651    public void clear() {
652        dateInput.clear();
653        if (getPicker() != null) {
654            getPicker().set("select", null);
655        }
656        // Clear all active / error styles on datepicker
657        clearErrorOrSuccess();
658        label.removeStyleName(CssName.ACTIVE);
659        dateInput.removeStyleName(CssName.VALID);
660
661    }
662
663    protected void setPopupEnabled(boolean enabled) {
664        if (getPicker() != null) {
665            if (!enabled) {
666                $(getPickerRootElement()).attr("tabindex", "-1");
667            } else {
668                $(getPickerRootElement()).attr("tabindex", tabIndex);
669            }
670        }
671    }
672
673    @Override
674    public HandlerRegistration addCloseHandler(final CloseHandler<MaterialDatePicker> handler) {
675        return addHandler(handler, CloseEvent.getType());
676    }
677
678    @Override
679    public HandlerRegistration addOpenHandler(final OpenHandler<MaterialDatePicker> handler) {
680        return addHandler(handler, OpenEvent.getType());
681    }
682
683    @Override
684    protected ErrorMixin<AbstractValueWidget, MaterialLabel> getErrorMixin() {
685        if (errorMixin == null) {
686            errorMixin = new ErrorMixin<>(this, errorLabel, dateInput, placeholderLabel);
687        }
688        return errorMixin;
689    }
690
691    protected ReadOnlyMixin<MaterialDatePicker, DateInput> getReadOnlyMixin() {
692        if (readOnlyMixin == null) {
693            readOnlyMixin = new ReadOnlyMixin<>(this, dateInput);
694        }
695        return readOnlyMixin;
696    }
697}