Adding Format to DateBox
Pair coded with jlabanca

git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/1.6@4363 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/VisualsForDateBox.java b/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/VisualsForDateBox.java
index a4423a6..132d0f8 100644
--- a/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/VisualsForDateBox.java
+++ b/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/VisualsForDateBox.java
@@ -18,6 +18,8 @@
 

 import com.google.gwt.event.dom.client.ClickEvent;

 import com.google.gwt.event.dom.client.ClickHandler;

+import com.google.gwt.event.dom.client.KeyDownEvent;

+import com.google.gwt.event.dom.client.KeyDownHandler;

 import com.google.gwt.event.logical.shared.ValueChangeEvent;

 import com.google.gwt.event.logical.shared.ValueChangeHandler;

 import com.google.gwt.i18n.client.DateTimeFormat;

@@ -26,11 +28,12 @@
 import com.google.gwt.user.client.ui.HTML;

 import com.google.gwt.user.client.ui.HorizontalPanel;

 import com.google.gwt.user.client.ui.Label;

+import com.google.gwt.user.client.ui.TextBox;

 import com.google.gwt.user.client.ui.VerticalPanel;

 import com.google.gwt.user.client.ui.Widget;

 import com.google.gwt.user.datepicker.client.DateBox;

 import com.google.gwt.user.datepicker.client.DatePicker;

-import com.google.gwt.user.datepicker.client.DateBox.InvalidDateReporter;

+import com.google.gwt.user.datepicker.client.DateBox.DefaultFormat;

 

 import java.util.Date;

 

@@ -38,6 +41,36 @@
  * Visuals for date box.

  */

 public class VisualsForDateBox extends AbstractIssue {

+  class FormatWithNewYearsEve extends DefaultFormat {

+    public FormatWithNewYearsEve() {

+    }

+

+    public FormatWithNewYearsEve(DateTimeFormat format) {

+      super(format);

+    }

+

+    @Override

+    public String format(DateBox box, Date d) {

+      if (d == null) {

+        return "Please Change me";

+      } else {

+        return super.format(box, d);

+      }

+    }

+

+    @SuppressWarnings("deprecation")

+    @Override

+    public Date parse(DateBox box, String input, boolean reportError) {

+      if (input.equalsIgnoreCase("new year's eve")) {

+        Date d = new Date();

+        d.setDate(31);

+        d.setMonth(12);

+        return d;

+      } else {

+        return super.parse(box, input, reportError);

+      }

+    }

+  }

 

   @Override

   public Widget createIssue() {

@@ -45,31 +78,17 @@
     v.add(new HTML("<div style='height:25px'></div>"));

     v.add(dateRange());

     v.add(new HTML("<div style='height:25px'></div>"));

-    final Label startErrors = makeErrorLabel();

-

-    Widget errorReportingDateBox = dateRange(new DateBox.InvalidDateReporter() {

-      public void clearError() {

-        startErrors.setText("");

-      }

-      public void reportError(String input) {

-        startErrors.setText("\"" + input + "\" is not a date");

-      }

-    });

-

-    v.add(errorReportingDateBox);

-    v.add(startErrors);

-

     return v;

   }

 

   @Override

   public String getInstructions() {

-    return "Click on first date box, see that date picker is displayed, "

-        + "use arrow keys to navigate to second date box, select a date. "

-        + "The second set includes an error display (one, shared by both "

-        + "fields). See that it notices  when you type garbage, and that "

-        + "its error message is cleared when you empty the field or provide "

-        + "a valid date.";

+    return "Instructions <ul><li>Click on first date box, see that date picker is displayed</li> "

+        + "<li>use arrow keys to navigate to second date box, select a date.</li> "

+        + "<li>type in a bad date then click back to the first date box. Your bad date should now be in red</li>"

+        + "<li>get back to the second box, now type in a valid date and tab away, its text should now be black again. </li>"

+        + "<li>Try typing 'New Year's Eve' in on the start datebox)</li>"

+        + "<li> Hit 'Show values' and confirm that you see the correct values</li></ul>";

   }

 

   @Override

@@ -83,16 +102,14 @@
   }

 

   private Widget dateRange() {

-    return dateRange(null);

-  }

-

-  private Widget dateRange(InvalidDateReporter invalidDateReporter) {

     VerticalPanel v = new VerticalPanel();

     HorizontalPanel p = new HorizontalPanel();

     v.add(p);

-    final DateBox start = newDateBox(invalidDateReporter);

+    final DateBox start = new DateBox(new DatePicker(), null,

+        new FormatWithNewYearsEve());

+

     start.setWidth("13em");

-    final DateBox end = newDateBox(invalidDateReporter);

+    final DateBox end = new DateBox();

     end.setWidth("13em");

 

     end.getDatePicker().addValueChangeHandler(new ValueChangeHandler<Date>() {

@@ -101,8 +118,27 @@
       }

     });

 

-    start.setValue(new Date());

+    final TextBox startText = start.getTextBox();

+    startText.addKeyDownHandler(new KeyDownHandler() {

+      public void onKeyDown(KeyDownEvent e) {

+        if (e.isRightArrow()

+            && start.getCursorPos() == startText.getText().length()) {

+          start.hideDatePicker();

+          end.setFocus(true);

+        }

+      }

+    });

 

+    end.getTextBox().addKeyDownHandler(new KeyDownHandler() {

+      public void onKeyDown(KeyDownEvent e) {

+        if ((e.isLeftArrow()) && end.getCursorPos() == 0) {

+          end.hideDatePicker();

+          start.setFocus(true);

+        }

+      }

+    });

+

+    end.setValue(new Date());

     p.add(start);

     Label l = new Label(" - ");

     l.setStyleName("filler");

@@ -114,15 +150,13 @@
     v.add(h2);

     h2.add(new Button("Short format", new ClickHandler() {

       public void onClick(ClickEvent event) {

-        start.setDateFormat(DateTimeFormat.getShortDateFormat());

-        end.setDateFormat(DateTimeFormat.getShortDateFormat());

+        updateFormat(start, end, DateTimeFormat.getShortDateFormat());

       }

     }));

     h2.add(new Button("Long format", new ClickHandler() {

 

       public void onClick(ClickEvent event) {

-        start.setDateFormat(DateTimeFormat.getLongDateFormat());

-        end.setDateFormat(DateTimeFormat.getLongDateFormat());

+        updateFormat(start, end, DateTimeFormat.getMediumDateFormat());

       }

     }));

 

@@ -132,32 +166,22 @@
         end.setValue(null);

       }

     }));

-    

-    h2.add(new Button("Get Value", new ClickHandler() {

+

+    h2.add(new Button("Show Values", new ClickHandler() {

       public void onClick(ClickEvent event) {

         DateTimeFormat f = DateTimeFormat.getShortDateFormat();

         Date d1 = start.getValue();

         Date d2 = end.getValue();

-        value.setText("Start: \"" 

-            + (d1 == null ? "null" : f.format(d1))

-            + "\" End: \"" 

-            + (d2 == null ? "null" : f.format(d2)) 

-            + "\"");

+        value.setText("Start: \"" + (d1 == null ? "null" : f.format(d1))

+            + "\" End: \"" + (d2 == null ? "null" : f.format(d2)) + "\"");

       }

     }));

     return v;

   }

 

-  private Label makeErrorLabel() {

-    final Label startErrors = new Label();

-    startErrors.getElement().getStyle().setProperty("color", "red");

-    return startErrors;

+  private void updateFormat(DateBox start, DateBox end, DateTimeFormat format) {

+    // You can replace the format itself.

+    start.setFormat(new FormatWithNewYearsEve(format));

+    end.setFormat(new DefaultFormat(format));

   }

-

-  private DateBox newDateBox(InvalidDateReporter invalidDateReporter) {

-    DateBox dateBox =

-        invalidDateReporter == null ? new DateBox() : new DateBox(

-            new DatePicker(), invalidDateReporter);

-    return dateBox;

-  }

-}

+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/user/datepicker/client/DateBox.java b/user/src/com/google/gwt/user/datepicker/client/DateBox.java
index b0667f1..d9fc83d 100644
--- a/user/src/com/google/gwt/user/datepicker/client/DateBox.java
+++ b/user/src/com/google/gwt/user/datepicker/client/DateBox.java
@@ -25,6 +25,8 @@
 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;
@@ -43,13 +45,16 @@
  * 
  * <h3>CSS Style Rules</h3>
  * 
- * <ul class="css">
- * 
- * <li>.gwt-DateBox { }</li>
- * 
- * <li>.dateBoxPopup { Applied to the popup around the DatePicker }</li>
- * 
- * </ul>
+ * <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>
  * 
  * <p>
  * <h3>Example</h3>
@@ -57,34 +62,128 @@
  * </p>
  */
 public class DateBox extends Composite implements HasValue<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;
+    }
+
+    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;
+    }
+
+    @SuppressWarnings("deprecation")
+    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;
+    }
+
+    public void reset(DateBox dateBox, boolean abandon) {
+      dateBox.removeStyleName(DATE_BOX_FORMAT_ERROR);
+    }
+  }
 
   /**
-   * Implemented by a delegate to report errors parsing date values from the
-   * user's input.
-   * 
-   * @deprecated, is going to be replaced with a format interface shortly.
+   * Implemented by a delegate to handle the parsing and formating of date
+   * values. The default {@link Format} uses a new {@link DefaultFormat}
+   * instance.
    */
-  @Deprecated
-  public interface InvalidDateReporter {
-    /**
-     * Called when a valid date has been parsed, or the datebox has been
-     * cleared.
-     */
-    void clearError();
+  public interface Format {
 
     /**
-     * Given an unparseable string, explain the situation to the user.
+     * Formats the provided date. Note, a null date is a possible input.
      * 
-     * @param input what the user typed
+     * @param dateBox the date box you are formatting
+     * @param date the date to format
+     * @return the formatted date as a string
      */
-    void reportError(String input);
+    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 {
+      FocusHandler, BlurHandler, ClickHandler, KeyDownHandler,
+      CloseHandler<PopupPanel> {
 
     public void onBlur(BlurEvent event) {
-      if (!popup.isVisible()) {
+      if (isDatePickerVisible() == false) {
         updateDateFromTextBox();
       }
     }
@@ -93,8 +192,16 @@
       showDatePicker();
     }
 
-    public void onFocus(FocusEvent event) {
+    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();
+      }
+    }
+
+    public void onFocus(FocusEvent event) {
+      if (allowDPShow && isDatePickerVisible() == false) {
         showDatePicker();
       }
     }
@@ -103,9 +210,10 @@
       switch (event.getNativeKeyCode()) {
         case KeyCodes.KEY_ENTER:
         case KeyCodes.KEY_TAB:
+          updateDateFromTextBox();
+          // Deliberate fall through
         case KeyCodes.KEY_ESCAPE:
         case KeyCodes.KEY_UP:
-          updateDateFromTextBox();
           hideDatePicker();
           break;
         case KeyCodes.KEY_DOWN:
@@ -123,49 +231,46 @@
   }
 
   /**
+   * 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";
-
-  public static final InvalidDateReporter DEFAULT_INVALID_DATE_REPORTER = new InvalidDateReporter() {
-    public void clearError() {
-    }
-
-    public void reportError(String input) {
-    }
-  };
-  private static final DateTimeFormat DEFAULT_FORMATTER = DateTimeFormat.getMediumDateFormat();
-
+  private static final DefaultFormat DEFAULT_FORMAT = new DefaultFormat();
   private final PopupPanel popup;
   private final TextBox box = new TextBox();
   private final DatePicker picker;
-
-  private final InvalidDateReporter invalidDateReporter;
-  private DateTimeFormat dateFormatter = DEFAULT_FORMATTER;
-
+  private Format format;
   private boolean allowDPShow = true;
 
   /**
-   * Create a new date box with a new {@link DatePicker} and the
-   * {@link #DEFAULT_INVALID_DATE_REPORTER}, which does nothing.
+   * Create a date box with a new {@link DatePicker}.
    */
   public DateBox() {
-    this(new DatePicker(), DEFAULT_INVALID_DATE_REPORTER);
+    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, InvalidDateReporter invalidDateReporter) {
+  public DateBox(DatePicker picker, Date date, Format format) {
     this.picker = picker;
-    this.invalidDateReporter = invalidDateReporter;
     this.popup = new PopupPanel();
+    assert format != null : "You may not construct a date box with a null format";
+    this.format = format;
+
     popup.setAutoHideEnabled(true);
     popup.setAutoHidePartner(box.getElement());
     popup.setWidget(picker);
     popup.setStyleName("dateBoxPopup");
+
     initWidget(box);
     setStyleName(DEFAULT_STYLENAME);
 
@@ -175,6 +280,8 @@
     box.addBlurHandler(handler);
     box.addClickHandler(handler);
     box.addKeyDownHandler(handler);
+    popup.addCloseHandler(handler);
+    setValue(date);
   }
 
   public HandlerRegistration addValueChangeHandler(
@@ -202,6 +309,16 @@
   }
 
   /**
+   * 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
@@ -221,10 +338,9 @@
 
   /**
    * Get the date displayed, or null if the text box is empty, or cannot be
-   * interpretted. The {@link InvalidDateReporter} may fire as a side effect of
-   * this call.
+   * interpreted.
    * 
-   * @return the Date
+   * @return the current date value
    */
   public Date getValue() {
     return parseDate(true);
@@ -255,22 +371,6 @@
   }
 
   /**
-   * Sets the date format to the given format. If date box is not empty,
-   * contents of date box will be replaced with current date in new format. If
-   * the date cannot be parsed, the current value will be preserved and the
-   * InvalidDateReporter notified as usual.
-   * 
-   * @param format format.
-   */
-  public void setDateFormat(DateTimeFormat format) {
-    if (format != dateFormatter) {
-      Date date = getValue();
-      dateFormatter = format;
-      setValue(date);
-    }
-  }
-
-  /**
    * Sets whether the date box is enabled.
    * 
    * @param enabled is the box enabled
@@ -290,6 +390,29 @@
   }
 
   /**
+   * 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
@@ -309,18 +432,13 @@
   }
 
   public void setValue(Date date, boolean fireEvents) {
-    Date oldDate = getValue();
-
-    if (date == null) {
-      picker.setValue(null);
-      box.setText("");
-    } else {
-      picker.setValue(date, false);
+    Date oldDate = parseDate(false);
+    if (date != null) {
       picker.setCurrentMonth(date);
-      setDate(date);
     }
+    picker.setValue(date, false);
+    setDate(date);
 
-    invalidDateReporter.clearError();
     if (fireEvents) {
       DateChangeEvent.fireIfNotEqualDates(this, oldDate, date);
     }
@@ -339,18 +457,11 @@
   }
 
   private Date parseDate(boolean reportError) {
-    Date d = null;
-    String text = box.getText().trim();
-    if (!text.equals("")) {
-      try {
-        d = dateFormatter.parse(text);
-      } catch (IllegalArgumentException exception) {
-        if (reportError) {
-          invalidDateReporter.reportError(text);
-        }
-      }
+    if (reportError) {
+      getFormat().reset(this, false);
     }
-    return d;
+    String text = box.getText().trim();
+    return getFormat().parse(this, text, reportError);
   }
 
   private void preventDatePickerPopup() {
@@ -367,7 +478,8 @@
    * events.
    */
   private void setDate(Date value) {
-    box.setText(dateFormatter.format(value));
+    format.reset(this, false);
+    box.setText(getFormat().format(this, value));
   }
 
   private void updateDateFromTextBox() {
diff --git a/user/src/com/google/gwt/user/theme/standard/public/gwt/standard/standard.css b/user/src/com/google/gwt/user/theme/standard/public/gwt/standard/standard.css
index 8b1d7bd..55a517b 100644
--- a/user/src/com/google/gwt/user/theme/standard/public/gwt/standard/standard.css
+++ b/user/src/com/google/gwt/user/theme/standard/public/gwt/standard/standard.css
@@ -1093,6 +1093,10 @@
 .gwt-DateBox input {
   width: 8em;
 }
+.dateBoxFormatError{
+  background:#ffcccc;
+}
+
 .dateBoxPopup {
 }
 
@@ -1171,3 +1175,4 @@
   cursor: pointer;
   padding: 0px 4px;
 }
+
diff --git a/user/src/com/google/gwt/user/theme/standard/public/gwt/standard/standard_rtl.css b/user/src/com/google/gwt/user/theme/standard/public/gwt/standard/standard_rtl.css
index f3b7d26..e3e65af 100644
--- a/user/src/com/google/gwt/user/theme/standard/public/gwt/standard/standard_rtl.css
+++ b/user/src/com/google/gwt/user/theme/standard/public/gwt/standard/standard_rtl.css
@@ -1094,6 +1094,9 @@
 .gwt-DateBox input {
   width: 8em;
 }
+.dateBoxFormatError {
+  background: #ffcccc;
+}
 .dateBoxPopup {
 }