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