/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.cedarsolutions.client.gwt.custom.datepicker;
import java.util.Date;
import com.google.gwt.core.client.GWT;
import com.google.gwt.editor.client.IsEditor;
import com.google.gwt.editor.client.LeafValueEditor;
import com.google.gwt.editor.client.adapters.TakesValueEditor;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
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.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.datepicker.client.DatePicker;
/**
* A text box that shows a {@link DatePicker} when the user focuses on it (CUSTOMIZED).
*
* <h3>
* Code Source
* </h3>
*
* <p>
* This is customized code that that was directly copied from GWT under the terms of its license.
* </p>
*
* <blockquote>
* <table border="1" cellpadding="5" cellspacing="0">
* <tbody>
* <tr>
* <td><i>Source:</i></td>
* <td>{@link com.google.gwt.user.datepicker.client.DateBox}</td>
* </tr>
* <tr>
* <td><i>Version:</i></td>
* <td>GWT 2.4.0</td>
* </tr>
* <tr>
* <td><i>Date:</i></td>
* <td>October, 2011</td>
* </tr>
* <tr>
* <td><i>Purpose:</i></td>
* <td>DateBox doesn't notice when a user clears the field.</td>
* </tr>
* <tr>
* <td><i>See also:</i></td>
* <td><a href="http://code.google.com/p/google-web-toolkit/issues/detail?id=4084">GWT Issue #4084</a></td>
* </tr>
* </tbody>
* </table>
* </blockquote>
*
* <p>
* The code was copied rather than extended because the original GWT code
* does not faciliate extension. GWT code is often difficult to extend due
* to design choices like the use of private or final variables, methods,
* or dependent classes.
* </p>
*
* <h3>CSS Style Rules</h3>
*
* <dl>
* <dt>.gwt-DateBox</dt>
* <dd>default style name</dd>
* <dt>.dateBoxPopup</dt>
* <dd>Applied to the popup around the DatePicker</dd>
* <dt>.dateBoxFormatError</dt>
* <dd>Default style for when the date box has bad input. Applied by
* {@link DateBox.DefaultFormat} when the text does not represent a date that
* can be parsed</dd>
* </dl>
*
* @author Google
* @author Kenneth J. Pronovici <pronovic@ieee.org>
*/
@SuppressWarnings("deprecation")
public class DateBox extends Composite implements HasValue<Date>,
IsEditor<LeafValueEditor<Date>> {
/**
* Default {@link DateBox.Format} class. The date is first parsed using the
* {@link DateTimeFormat} supplied by the user, or
* {@link DateTimeFormat#getMediumDateFormat()} by default.
* <p>
* If that fails, we then try to parse again using the default browser date
* parsing.
* </p>
* If that fails, the <code>dateBoxFormatError</code> css style is applied to
* the {@link DateBox}. The style will be removed when either a successful
* {@link #parse(DateBox,String, boolean)} is called or
* {@link #format(DateBox,Date)} is called.
* <p>
* Use a different {@link DateBox.Format} instance to change that behavior.
* </p>
*/
public static class DefaultFormat implements Format {
private final DateTimeFormat dateTimeFormat;
/**
* Creates a new default format instance.
*/
public DefaultFormat() {
dateTimeFormat = DateTimeFormat.getMediumDateTimeFormat();
}
/**
* Creates a new default format instance.
*
* @param dateTimeFormat the {@link DateTimeFormat} to use with this
* {@link Format}.
*/
public DefaultFormat(DateTimeFormat dateTimeFormat) {
this.dateTimeFormat = dateTimeFormat;
}
@Override
public String format(DateBox box, Date date) {
if (date == null) {
return "";
} else {
return dateTimeFormat.format(date);
}
}
/**
* Gets the date time format.
*
* @return the date time format
*/
public DateTimeFormat getDateTimeFormat() {
return dateTimeFormat;
}
@Override
public Date parse(DateBox dateBox, String dateText, boolean reportError) {
Date date = null;
try {
if (dateText.length() > 0) {
date = dateTimeFormat.parse(dateText);
}
} catch (IllegalArgumentException exception) {
try {
date = new Date(dateText);
} catch (IllegalArgumentException e) {
if (reportError) {
dateBox.addStyleName(DATE_BOX_FORMAT_ERROR);
}
return null;
}
}
return date;
}
@Override
public void reset(DateBox dateBox, boolean abandon) {
dateBox.removeStyleName(DATE_BOX_FORMAT_ERROR);
}
}
/**
* Implemented by a delegate to handle the parsing and formating of date
* values. The default {@link Format} uses a new {@link DefaultFormat}
* instance.
*/
public interface Format {
/**
* Formats the provided date. Note, a null date is a possible input.
*
* @param dateBox the date box you are formatting
* @param date the date to format
* @return the formatted date as a string
*/
String format(DateBox dateBox, Date date);
/**
* Parses the provided string as a date.
*
* @param dateBox the date box
* @param text the string representing a date
* @param reportError should the formatter indicate a parse error to the
* user?
* @return the date created, or null if there was a parse error
*/
Date parse(DateBox dateBox, String text, boolean reportError);
/**
* If the format did any modifications to the date box's styling, reset them
* now.
*
* @param abandon true when the current format is being replaced by another
* @param dateBox the date box
*/
void reset(DateBox dateBox, boolean abandon);
}
private class DateBoxHandler implements ValueChangeHandler<Date>,
FocusHandler, BlurHandler, ClickHandler, KeyDownHandler,
CloseHandler<PopupPanel> {
@Override
public void onBlur(BlurEvent event) {
if (isDatePickerShowing() == false) {
updateDateFromTextBox();
}
}
@Override
public void onClick(ClickEvent event) {
showDatePicker();
}
@Override
public void onClose(CloseEvent<PopupPanel> event) {
// If we are not closing because we have picked a new value, make sure the
// current value is updated.
if (allowDPShow) {
updateDateFromTextBox();
}
}
@Override
public void onFocus(FocusEvent event) {
if (allowDPShow && isDatePickerShowing() == false) {
showDatePicker();
}
}
@Override
public void onKeyDown(KeyDownEvent event) {
switch (event.getNativeKeyCode()) {
case KeyCodes.KEY_ENTER:
case KeyCodes.KEY_TAB:
updateDateFromTextBox();
// Deliberate fall through
case KeyCodes.KEY_ESCAPE:
case KeyCodes.KEY_UP:
hideDatePicker();
break;
case KeyCodes.KEY_DOWN:
showDatePicker();
break;
}
}
@Override
public void onValueChange(ValueChangeEvent<Date> event) {
setValue(parseDate(false), event.getValue(), true);
hideDatePicker();
preventDatePickerPopup();
box.setFocus(true);
}
}
/**
* Default style name added when the date box has a format error.
*/
private static final String DATE_BOX_FORMAT_ERROR = "dateBoxFormatError";
/**
* Default style name.
*/
public static final String DEFAULT_STYLENAME = "gwt-DateBox";
private static final DefaultFormat DEFAULT_FORMAT = GWT.create(DefaultFormat.class);
private final PopupPanel popup;
private final TextBox box = new TextBox();
private final DatePicker picker;
private LeafValueEditor<Date> editor;
private Format format;
private boolean allowDPShow = true;
/**
* Create a date box with a new {@link DatePicker}.
*/
public DateBox() {
this(new DatePicker(), null, DEFAULT_FORMAT);
}
/**
* Create a new date box.
*
* @param date the default date.
* @param picker the picker to drop down from the date box
* @param format to use to parse and format dates
*/
public DateBox(DatePicker picker, Date date, Format format) {
this.picker = picker;
this.popup = new PopupPanel(true);
assert format != null : "You may not construct a date box with a null format";
this.format = format;
popup.addAutoHidePartner(box.getElement());
popup.setWidget(picker);
popup.setStyleName("dateBoxPopup");
initWidget(box);
setStyleName(DEFAULT_STYLENAME);
DateBoxHandler handler = new DateBoxHandler();
picker.addValueChangeHandler(handler);
box.addFocusHandler(handler);
box.addBlurHandler(handler);
box.addClickHandler(handler);
box.addKeyDownHandler(handler);
popup.addCloseHandler(handler);
setValue(date);
}
@Override
public HandlerRegistration addValueChangeHandler(
ValueChangeHandler<Date> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
/**
* Returns a {@link TakesValueEditor} backed by the DateBox.
*/
@Override
public LeafValueEditor<Date> asEditor() {
if (editor == null) {
editor = TakesValueEditor.of(this);
}
return editor;
}
/**
* Gets the current cursor position in the date box.
*
* @return the cursor position
*
*/
public int getCursorPos() {
return box.getCursorPos();
}
/**
* Gets the date picker.
*
* @return the date picker
*/
public DatePicker getDatePicker() {
return picker;
}
/**
* Gets the format instance used to control formatting and parsing of this
* {@link DateBox}.
*
* @return the format
*/
public Format getFormat() {
return this.format;
}
/**
* Gets the date box's position in the tab index.
*
* @return the date box's tab index
*/
public int getTabIndex() {
return box.getTabIndex();
}
/**
* Get text box.
*
* @return the text box used to enter the formatted date
*/
public TextBox getTextBox() {
return box;
}
/**
* Get the date displayed, or null if the text box is empty, or cannot be
* interpreted.
*
* @return the current date value
*/
@Override
public Date getValue() {
return parseDate(true);
}
/**
* Hide the date picker.
*/
public void hideDatePicker() {
popup.hide();
}
/**
* Returns true if date picker is currently showing, false if not.
*/
public boolean isDatePickerShowing() {
return popup.isShowing();
}
/**
* Sets the date box's 'access key'. This key is used (in conjunction with a
* browser-specific modifier key) to automatically focus the widget.
*
* @param key the date box's access key
*/
public void setAccessKey(char key) {
box.setAccessKey(key);
}
/**
* Sets whether the date box is enabled.
*
* @param enabled is the box enabled
*/
public void setEnabled(boolean enabled) {
box.setEnabled(enabled);
}
/**
* Explicitly focus/unfocus this widget. Only one widget can have focus at a
* time, and the widget that does will receive all keyboard events.
*
* @param focused whether this widget should take focus or release it
*/
public void setFocus(boolean focused) {
box.setFocus(focused);
}
/**
* Sets the format used to control formatting and parsing of dates in this
* {@link DateBox}. If this {@link DateBox} is not empty, the contents of date
* box will be replaced with current contents in the new format.
*
* @param format the new date format
*/
public void setFormat(Format format) {
assert format != null : "A Date box may not have a null format";
if (this.format != format) {
Date date = getValue();
// This call lets the formatter do whatever other clean up is required to
// switch formatters.
//
this.format.reset(this, true);
// Now update the format and show the current date using the new format.
this.format = format;
setValue(date);
}
}
/**
* Sets the date box's position in the tab index. If more than one widget has
* the same tab index, each such widget will receive focus in an arbitrary
* order. Setting the tab index to <code>-1</code> will cause this widget to
* be removed from the tab order.
*
* @param index the date box's tab index
*/
public void setTabIndex(int index) {
box.setTabIndex(index);
}
/**
* Set the date.
*/
@Override
public void setValue(Date date) {
setValue(date, false);
}
@Override
public void setValue(Date date, boolean fireEvents) {
setValue(picker.getValue(), date, fireEvents);
}
/**
* Parses the current date box's value and shows that date.
*/
public void showDatePicker() {
Date current = parseDate(false);
if (current == null) {
current = new Date();
}
picker.setCurrentMonth(current);
popup.showRelativeTo(this);
}
private Date parseDate(boolean reportError) {
if (reportError) {
getFormat().reset(this, false);
}
String text = box.getText().trim();
return getFormat().parse(this, text, reportError);
}
private void preventDatePickerPopup() {
allowDPShow = false;
DeferredCommand.addCommand(new Command() {
@Override
public void execute() {
allowDPShow = true;
}
});
}
private void setValue(Date oldDate, Date date, boolean fireEvents) {
if (date != null) {
picker.setCurrentMonth(date);
}
picker.setValue(date, false);
format.reset(this, false);
box.setText(getFormat().format(this, date));
if (fireEvents) {
DateChangeEvent.fireIfNotEqualDates(this, oldDate, date);
}
}
// KJP: I took over this class because DateBox doesn't notice when a user clears the field.
// Fix from: http://code.google.com/p/google-web-toolkit/issues/detail?id=4084
private void updateDateFromTextBox() {
if (box.getText().trim().isEmpty()) {
setValue(picker.getValue(), null, true);
} else {
Date parsedDate = parseDate(true);
if (parsedDate != null) {
setValue(picker.getValue(), parsedDate,true);
}
}
}
}