package org.vaadin.touchkit.gwt.client.ui;
import java.util.Date;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.ui.HasEnabled;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.VConsole;
import com.vaadin.shared.VBrowserDetails;
/**
* DatePicker widget. Uses HTML5 date input fields to ask time values from user.
*
*/
public class DatePicker extends SimplePanel implements
HasValueChangeHandlers<Date>, ClickHandler, HasEnabled {
private static final String CLASSNAME = "v-touchkit-datepicker";
private final static String DAY_FORMAT = "yyyy-MM-dd";
private final static String MONTH_FORMAT = "yyyy-MM";
private final static String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm";
private DateParser dateParser;
private Date date;
private Date min;
private Date max;
private InputElement input;
private Resolution resolution;
private Label backUpWidget = null;
private CalendarOverlay overlay = null;
private boolean useNative = true;
private long overlayClosed = 0;
private boolean enabled = true;
/**
* Interface for parsing dates. DatePicker provides a standard
* implementation based on {@link DatePicker#getFormat(Resolution)} which is
* used when in native mode.
*
*/
public interface DateParser {
String dateToString(Date date);
Date stringToDate(String string);
}
/**
* Parses dates according to <a
* href="http://tools.ietf.org/html/rfc3339">RFC3339</a> This is needed
* since {@link #input} takes only date strings in that format
*/
private DateParser standardParser = createStandardDateParser();
private DateParser createStandardDateParser() {
if (BrowserInfo.get().isIOS()) {
return new StandardIosDateParser();
} else {
return new StandardDateParser();
}
}
/**
* Resolution of widget
*/
public enum Resolution {
/**
* Resolution in time (usually minutes)
*/
TIME("datetime-local"),
/**
* Resolution in days
*/
DAY("date"),
/**
* Resolution in months
*/
MONTH("month");
private String type;
Resolution(String type) {
this.type = type;
}
/**
* Get type value used in input field
*
* @return Type value used in input field
*/
public String getType() {
return type;
}
}
/**
* Create new DatePicker
*/
public DatePicker() {
super();
setStyleName(CLASSNAME);
}
/**
* This format must be used for native mode
*
* @param resolution
* @return
* @see <a href="http://www.w3.org/TR/html-markup/input.date.html">W3C spec
* for date input</a>
*/
public static DateTimeFormat getFormat(Resolution resolution) {
switch (resolution) {
case MONTH:
return DateTimeFormat.getFormat(MONTH_FORMAT);
case DAY:
return DateTimeFormat.getFormat(DAY_FORMAT);
case TIME:
default:
return DateTimeFormat.getFormat(TIME_FORMAT);
}
}
/**
* Input element listener for the HTML5 date input element
*/
private final EventListener elementListener = new EventListener() {
@Override
public void onBrowserEvent(com.google.gwt.user.client.Event event) {
String newDateString = input.getValue();
if (newDateString == null || newDateString.isEmpty()) {
// fire event if date value has changed
setDate(null, true, date != null);
} else {
// non-empty value, set if changed
Date newDate = toStandardDate(newDateString);
if (!newDate.equals(date)) {
setDate((newDate), false, true);
}
}
}
};
private Date toStandardDate(String newDateString) {
return (newDateString == null || newDateString.isEmpty()) ? null
: standardParser.stringToDate(newDateString);
}
private String toStandardDateString(Date date) {
return (date == null) ? "" : standardParser.dateToString(date);
}
/**
* Convert Date to string format needed for display
*
* @param date
* Date to convert
* @return String date or empty string for null date
*/
private String dateToString(Date date) {
if (date == null) {
return "";
}
return getDateParser().dateToString(date);
}
/**
* Convert string value from input field to Date
*
* @param string
* String value of input field
* @return Date value or null if failure
*/
private Date stringToDate(String string) {
Date date = null;
try {
date = getDateParser().stringToDate(string);
// break;
} catch (Exception e) {
// Doesn't matter
}
// }
if (date == null) {
VConsole.error("Failed to parse: " + string);
}
return date;
}
/**
* Get the current value of this DatePicker.
*
* @return The current value as a String
*/
public String getValue() {
return dateToString(date);
}
/**
* Get the current value of this DatePicker.
*
* @return The current value as a Date
*/
public Date getDateValue() {
return date;
}
/**
* Set date value of this DatePicker. Some parts of date may be ignored
* based on current resolution.
*
* @param newDate
* New date value
*/
public void setDate(Date newDate) {
setDate(newDate, date == null || !date.equals(newDate), false);
}
/**
* Set date value of this DatePicker
*
* @param newDate
* New value set
* @param updateInput
* If input field should be updated
* @param fire
* If change event should be fired
*/
protected void setDate(Date newDate, boolean updateInput, boolean fire) {
if (date == null || !date.equals(newDate)) {
closeCalendar();
date = newDate;
if (updateInput) {
updateValue(date);
}
if (fire) {
ValueChangeEvent.fire(DatePicker.this, newDate);
}
} else {
updateValue(newDate);
}
}
protected void updateValue(Date date) {
if (input != null) {
input.setValue(toStandardDateString(date));
} else if (backUpWidget != null) {
backUpWidget.setText(dateToString(date));
}
}
@Override
public HandlerRegistration addValueChangeHandler(
ValueChangeHandler<Date> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
/**
* Set resolution of this DatePicker
*
* @param res
* Resolution
*/
public void setResolution(Resolution res) {
if (resolution != res) {
changeResolution(res);
}
}
public Resolution getResolution() {
return resolution;
}
protected void changeResolution(Resolution res) {
closeCalendar();
resolution = res;
if (useNative) {
addHtml5Input();
input.setAttribute("type", res.getType());
if ("text".equals(input.getType()) || isOldAndroid()) {
// HTML5 is not supported, revert to backup.
removeHtml5Input();
addBackupWidget();
} else {
// HTML5 is supported, make sure the backup widget is removed
removeBackupWidget();
}
} else {
removeHtml5Input();
addBackupWidget();
}
if (date != null) {
updateValue(date);
}
}
/**
* @return true if the Android version is older than 4.2, in which case the
* HTML5 date field is not very useful.
*/
private boolean isOldAndroid() {
VBrowserDetails details = new VBrowserDetails(
BrowserInfo.getBrowserString());
int major = details.getBrowserMajorVersion();
int minor = details.getOperatingSystemMinorVersion();
return details.isAndroid() && (major < 4 || (major == 4 && minor < 2));
}
private void addHtml5Input() {
if (input == null) {
input = Document.get().createTextInputElement();
if (min != null) {
input.setAttribute("min", toStandardDateString(min));
}
if (max != null) {
input.setAttribute("max", toStandardDateString(max));
}
input.setDisabled(!enabled);
getElement().appendChild(input);
com.google.gwt.user.client.Element userElement = (com.google.gwt.user.client.Element) Element
.as(input);
DOM.sinkEvents(userElement, Event.ONCHANGE | Event.ONBLUR);
DOM.setEventListener(userElement, elementListener);
}
}
private void removeHtml5Input() {
if (input != null) {
input.removeFromParent();
input = null;
}
}
private void removeBackupWidget() {
if (backUpWidget != null && backUpWidget.isAttached()) {
backUpWidget.removeFromParent();
backUpWidget = null;
}
}
private void addBackupWidget() {
if (backUpWidget == null) {
backUpWidget = new Label();
backUpWidget.setStyleName("v-select-select"); // style like native
// input
add(backUpWidget);
backUpWidget.addClickHandler(this);
updateValue(date);
}
}
protected void openCalendar() {
closeCalendar();
overlay = new CalendarOverlay(resolution, min, max);
overlay.setOwner(DatePicker.this);
overlay.center();
if (date != null) {
overlay.setDate(date);
}
overlay.addValueChangeHandler(new ValueChangeHandler<Date>() {
@Override
public void onValueChange(ValueChangeEvent<Date> event) {
setDate(event.getValue(), true, true);
}
});
overlay.addCloseHandler(new CloseHandler<PopupPanel>() {
@Override
public void onClose(CloseEvent<PopupPanel> event) {
overlayClosed = new Date().getTime();
overlay = null;
}
});
}
protected void closeCalendar() {
if (overlay != null) {
overlay.hide(true);
overlay = null;
}
}
@Override
public void onClick(ClickEvent event) {
if (enabled) {
if (backUpWidget == null) {
return;
}
if (overlay != null) {
closeCalendar();
} else if ((new Date().getTime() - overlayClosed) > 250) {
openCalendar();
}
}
}
/**
* If widget should try to use native presentation
*
* @param useNative
* true to use native if possible
*/
public void setUseNative(boolean useNative) {
if (this.useNative != useNative) {
this.useNative = useNative;
changeResolution(resolution);
}
}
/**
* Set minimal value accepted from user.
*
* @param date
* the first accepted date.
*/
public void setMin(Date date) {
min = date;
if (input != null) {
if (min != null) {
input.setAttribute("min", toStandardDateString(date));
} else {
input.removeAttribute("min");
}
}
}
/**
* Set maximal value accepted from user.
*
* @param date
* the last accepted date.
*/
public void setMax(Date date) {
max = date;
if (input != null) {
if (max != null) {
input.setAttribute("max", toStandardDateString(max));
} else {
input.removeAttribute("max");
}
}
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
if (input != null) {
input.setDisabled(!enabled);
}
}
/**
* @see #setDateParser(DateParser)
*/
public DateParser getDateParser() {
if (useNative) {
return standardParser;
} else {
return dateParser;
}
}
/**
* Set a custom parser for dates. Must be able to parse date<->string.
*
* @param dateParser
*/
public void setDateParser(DateParser dateParser) {
this.dateParser = dateParser;
}
private class StandardDateParser implements DateParser {
@Override
public String dateToString(Date date) {
return getFormat(getResolution()).format(date);
}
@Override
public Date stringToDate(String string) {
return getFormat(getResolution()).parse(string);
}
}
private class StandardIosDateParser extends StandardDateParser {
@Override
public String dateToString(Date date) {
return getFormat(getResolution()).format(date);
}
@Override
public Date stringToDate(String string) {
Date parsedDate = super.stringToDate(string);
// Convert no timezoned times back to local time when iOS
if (string.endsWith("Z")) {
@SuppressWarnings("deprecation")
int minOffset = parsedDate.getTimezoneOffset();
parsedDate.setTime(parsedDate.getTime() - minOffset * 60000);
}
return parsedDate;
}
}
}