001 /*
002 * Apache License
003 * Version 2.0, January 2004
004 * http://www.apache.org/licenses/
005 *
006 * Copyright 2008-2010 by chenillekit.org
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 */
014
015 package org.chenillekit.tapestry.core.components;
016
017 import java.text.DateFormat;
018 import java.text.ParseException;
019 import java.text.SimpleDateFormat;
020 import java.util.Date;
021 import java.util.Locale;
022
023 import org.apache.tapestry5.Asset;
024 import org.apache.tapestry5.Binding;
025 import org.apache.tapestry5.BindingConstants;
026 import org.apache.tapestry5.ComponentResources;
027 import org.apache.tapestry5.FieldValidationSupport;
028 import org.apache.tapestry5.FieldValidator;
029 import org.apache.tapestry5.MarkupWriter;
030 import org.apache.tapestry5.ValidationException;
031 import org.apache.tapestry5.ValidationTracker;
032 import org.apache.tapestry5.annotations.Environmental;
033 import org.apache.tapestry5.annotations.Import;
034 import org.apache.tapestry5.annotations.Parameter;
035 import org.apache.tapestry5.annotations.Path;
036 import org.apache.tapestry5.corelib.base.AbstractField;
037 import org.apache.tapestry5.ioc.Messages;
038 import org.apache.tapestry5.ioc.annotations.Inject;
039 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
040 import org.apache.tapestry5.json.JSONObject;
041 import org.apache.tapestry5.services.ComponentDefaultProvider;
042 import org.apache.tapestry5.services.FieldValidatorDefaultSource;
043 import org.apache.tapestry5.services.Request;
044 import org.apache.tapestry5.services.javascript.JavaScriptSupport;
045
046 /**
047 * A component used to collect a provided date/time from the user using a client-side JavaScript calendar. Non-JavaScript
048 * clients can simply type into a text field.
049 *
050 * @version $Id: DateTimeField.java 729 2010-11-03 19:51:08Z homburgs $
051 */
052 @Import(stylesheet = "datetimefield/datepicker.css",
053 library = {
054 "datetimefield/datepicker.js",
055 "datetimefield/datepicker_lang.js",
056 "../prototype-base-extensions.js",
057 "../prototype-date-extensions.js"
058 })
059 public class DateTimeField extends AbstractField
060 {
061 /**
062 * The value parameter of a DateField must be a {@link java.util.Date}.
063 */
064 @Parameter(required = true, principal = true)
065 private Date value;
066
067 /**
068 * The object that will perform input validation (which occurs after translation). The translate binding prefix is
069 * generally used to provide this object in a declarative fashion.
070 */
071 @Parameter(defaultPrefix = BindingConstants.VALIDATE)
072 @SuppressWarnings("unchecked")
073 private FieldValidator<Object> validate;
074
075 @Parameter(defaultPrefix = BindingConstants.ASSET, value = "datetimefield/calendar.png")
076 private Asset icon;
077
078 /**
079 * the pattern describing the date and time format {@link java.text.SimpleDateFormat}.
080 */
081 @Parameter(defaultPrefix = BindingConstants.LITERAL, value = "MM/dd/yyyy")
082 private String datePattern;
083
084 /**
085 * a boolean value determining whether to display the date picker. Defaults to true.
086 */
087 @Parameter(defaultPrefix = BindingConstants.PROP, value = "true")
088 private boolean datePicker;
089
090 /**
091 * a boolean value determining whether to display the time picker. Defaults to false.
092 */
093 @Parameter(defaultPrefix = BindingConstants.PROP, value = "false")
094 private boolean timePicker;
095
096 /**
097 * a boolean value determining whether to display the time picker next to the date picker (true) or under it (false, default).
098 */
099 @Parameter(defaultPrefix = BindingConstants.PROP, value = "false")
100 private boolean timePickerAdjacent;
101
102 /**
103 * a boolean value determining whether to display the time in AM/PM or 24 hour notation. Defaults to false.
104 */
105 @Parameter(defaultPrefix = BindingConstants.PROP, value = "false")
106 private boolean use24hrs;
107
108 /**
109 * a named javascript function, that executed after the date selected by the picker.
110 * there should one function parameter that holds the input dom element.
111 * This funtion should returns true or false.
112 */
113 @Parameter(defaultPrefix = BindingConstants.LITERAL, required = false)
114 private String afterUpdateElement;
115
116 /**
117 * Specify whether or not date/time parsing is to be lenient.
118 * With lenient parsing, the parser may use heuristics to interpret inputs that do not precisely match this object's format.
119 * With strict parsing, inputs must match this object's format.
120 */
121 @Parameter(defaultPrefix = BindingConstants.PROP, value = "true")
122 private boolean lenient;
123
124 @Environmental
125 private JavaScriptSupport javascriptSupport;
126
127 @Environmental
128 private ValidationTracker tracker;
129
130 @Inject
131 private ComponentResources resources;
132
133 @Inject
134 private Messages messages;
135
136 @Inject
137 private Request request;
138
139 @Inject
140 private Locale locale;
141
142 @Inject
143 private FieldValidatorDefaultSource fieldValidatorDefaultSource;
144
145 @Inject
146 private FieldValidationSupport fieldValidationSupport;
147
148 @Inject
149 private ComponentDefaultProvider defaultProvider;
150
151 @Inject
152 @Path("datetimefield/clock.png")
153 private Asset clockAsset;
154
155 /**
156 * For output, format nicely and unambiguously as four digits.
157 */
158 private DateFormat outputFormat;
159
160 /**
161 * When the user types a value, they may only type two digits for the year; SimpleDateFormat will do something
162 * reasonable. If they use the popup, it will be unambiguously 4 digits.
163 */
164 private DateFormat inputFormat;
165
166 /**
167 * The default value is a property of the container whose name matches the component's id. May return null if the
168 * container does not have a matching property.
169 */
170 final Binding defaultValue()
171 {
172 return defaultProvider.defaultBinding("value", resources);
173 }
174
175 /**
176 * Computes a default value for the "validate" parameter using {@link org.apache.tapestry5.services.ComponentDefaultProvider}.
177 */
178 final Binding defaultValidate()
179 {
180 return defaultProvider.defaultValidatorBinding("value", resources);
181 }
182
183 /**
184 * Tapestry render phase method.
185 * Initialize temporary instance variables here.
186 */
187 void setupRender()
188 {
189 outputFormat = new SimpleDateFormat(datePattern, locale);
190 }
191
192
193 void beginRender(MarkupWriter writer)
194 {
195 Asset componentIcon;
196 String value = tracker.getInput(this);
197
198 if (value == null) value = formatCurrentValue();
199
200 String clientId = getClientId();
201
202 writer.element("input",
203
204 "type", "text",
205
206 "class", "datepicker",
207
208 "name", getControlName(),
209
210 "id", clientId,
211
212 "value", value);
213
214 writeDisabled(writer);
215
216 validate.render(writer);
217
218 resources.renderInformalParameters(writer);
219
220 decorateInsideField();
221
222 writer.end();
223
224 // The setup parameters passed to Calendar.setup():
225
226 JSONObject setup = new JSONObject();
227
228 if (!datePicker && !timePicker)
229 throw new RuntimeException("both date- and timePicker set to false, that is senseless!");
230
231 if (!datePicker && timePicker)
232 componentIcon = clockAsset;
233 else
234 componentIcon = icon;
235
236 setup.put("icon", componentIcon.toClientURL());
237 setup.put("datePicker", datePicker);
238 setup.put("timePicker", timePicker);
239 setup.put("timePickerAdjacent", timePickerAdjacent);
240 setup.put("use24hrs", use24hrs);
241 setup.put("locale", locale.toString());
242
243 if (afterUpdateElement != null)
244 setup.put("afterUpdateElement", afterUpdateElement);
245
246 if (datePicker && timePicker)
247 setup.put("dateTimeFormat", datePattern);
248 else if (datePicker)
249 setup.put("dateFormat", datePattern);
250 else
251 setup.put("timeFormat", datePattern);
252
253 javascriptSupport.addScript("new Control.DatePicker('%s', %s);", getClientId(), setup);
254 }
255
256 private void writeDisabled(MarkupWriter writer)
257 {
258 if (isDisabled()) writer.attributes("disabled", "disabled");
259 }
260
261
262 private String formatCurrentValue()
263 {
264 if (value == null) return "";
265
266 return outputFormat.format(value);
267 }
268
269 @Override
270 protected void processSubmission(String elementName)
271 {
272 String value = request.getParameter(elementName);
273
274 tracker.recordInput(this, value);
275
276 Date parsedValue = null;
277
278 try
279 {
280 if (InternalUtils.isNonBlank(value))
281 {
282 inputFormat = new SimpleDateFormat(datePattern, locale);
283 inputFormat.setLenient(lenient);
284 parsedValue = inputFormat.parse(value);
285 }
286
287 }
288 catch (ParseException ex)
289 {
290 tracker.recordError(this, messages.format("date.not.parseable", value));
291 return;
292 }
293
294 try
295 {
296 fieldValidationSupport.validate(parsedValue, resources, validate);
297
298 this.value = parsedValue;
299 }
300 catch (ValidationException ex)
301 {
302 tracker.recordError(this, ex.getMessage());
303 }
304 }
305
306 void injectResources(ComponentResources resources)
307 {
308 this.resources = resources;
309 }
310
311 void injectMessages(Messages messages)
312 {
313 this.messages = messages;
314 }
315
316 @Override
317 public boolean isRequired()
318 {
319 return validate.isRequired();
320 }
321 }