001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.wicket.extensions.yui.calendar;
018:
019: import java.text.DateFormat;
020: import java.text.DateFormatSymbols;
021: import java.text.SimpleDateFormat;
022: import java.util.ArrayList;
023: import java.util.Date;
024: import java.util.HashMap;
025: import java.util.Iterator;
026: import java.util.List;
027: import java.util.Locale;
028: import java.util.Map;
029: import java.util.Properties;
030: import java.util.Map.Entry;
031:
032: import org.apache.wicket.Component;
033: import org.apache.wicket.RequestCycle;
034: import org.apache.wicket.ResourceReference;
035: import org.apache.wicket.Response;
036: import org.apache.wicket.WicketRuntimeException;
037: import org.apache.wicket.ajax.AjaxEventBehavior;
038: import org.apache.wicket.behavior.AbstractBehavior;
039: import org.apache.wicket.datetime.markup.html.form.DateTextField;
040: import org.apache.wicket.extensions.yui.YuiLib;
041: import org.apache.wicket.markup.html.IHeaderContributor;
042: import org.apache.wicket.markup.html.IHeaderResponse;
043: import org.apache.wicket.markup.html.form.AbstractTextComponent.ITextFormatProvider;
044: import org.apache.wicket.markup.html.resources.CompressedResourceReference;
045: import org.apache.wicket.markup.html.resources.JavascriptResourceReference;
046: import org.apache.wicket.util.convert.IConverter;
047: import org.apache.wicket.util.convert.converters.DateConverter;
048: import org.apache.wicket.util.string.Strings;
049: import org.apache.wicket.util.template.PackagedTextTemplate;
050: import org.apache.wicket.util.template.TextTemplate;
051: import org.joda.time.DateTime;
052:
053: /**
054: * Pops up a YUI calendar component so that the user can select a date. On
055: * selection, the date is set in the component it is coupled to, after which the
056: * popup is closed again. This behavior can only be used with components that
057: * either implement {@link ITextFormatProvider} or that use
058: * {@link DateConverter} configured with an instance of {@link SimpleDateFormat}
059: * (like Wicket's default configuration has).
060: *
061: * To use, simply add a new instance to your component, which would typically a
062: * TextField, like {@link DateTextField}.
063: *
064: * @author eelcohillenius
065: */
066: public class DatePicker extends AbstractBehavior implements
067: IHeaderContributor {
068: /**
069: * Exception thrown when the bound component does not produce a format this
070: * date picker can work with.
071: */
072: private static final class UnableToDetermineFormatException extends
073: WicketRuntimeException {
074: private static final long serialVersionUID = 1L;
075:
076: public UnableToDetermineFormatException() {
077: super (
078: "This behavior can only be added to components that either implement "
079: + ITextFormatProvider.class.getName()
080: + " AND produce a non-null format, or that use"
081: + " converters that this datepicker can use to determine"
082: + " the pattern being used. Alternatively, you can extend "
083: + " the date picker and override getDatePattern to provide your own");
084: }
085: }
086:
087: /**
088: * Format to be used when configuring YUI calendar. Can be used when using
089: * the "selected" property.
090: */
091: public static final DateFormat FORMAT_DATE = new SimpleDateFormat(
092: "MM/dd/yyyy");
093:
094: /**
095: * For specifying which page (month/year) to show in the calendar, use this
096: * format for the date. This is to be used together with the property
097: * "pagedate"
098: */
099: public static final DateFormat FORMAT_PAGEDATE = new SimpleDateFormat(
100: "MM/yyyy");
101:
102: private static final long serialVersionUID = 1L;
103:
104: /** The target component. */
105: private Component component;
106:
107: /**
108: * Construct.
109: */
110: public DatePicker() {
111: }
112:
113: /**
114: * @see org.apache.wicket.behavior.AbstractBehavior#bind(org.apache.wicket.Component)
115: */
116: public void bind(Component component) {
117: this .component = component;
118: checkComponentProvidesDateFormat(component);
119: component.setOutputMarkupId(true);
120: }
121:
122: /**
123: * @see org.apache.wicket.behavior.AbstractBehavior#onRendered(org.apache.wicket.Component)
124: */
125: public void onRendered(Component component) {
126: super .onRendered(component);
127: // Append the span and img icon right after the rendering of the
128: // component. Not as pretty as working with a panel etc, but works
129: // for behaviors and is more efficient
130: Response response = component.getResponse();
131: response
132: .write("\n<span class=\"yui-skin-sam\"> <div style=\"display:none;position:absolute;z-index: 99999;\" id=\"");
133: response.write(getEscapedComponentMarkupId());
134: response.write("Dp\"></div><img style=\"");
135: response.write(getIconStyle());
136: response.write("\" id=\"");
137: response.write(getIconId());
138: response.write("\" src=\"");
139: CharSequence iconUrl = getIconUrl();
140: response.write(Strings.escapeMarkup(iconUrl != null ? iconUrl
141: .toString() : ""));
142: response.write("\" /></span><input type=\"hidden\"/>");
143: }
144:
145: /**
146: * @see org.apache.wicket.markup.html.IHeaderContributor#renderHead(org.apache.wicket.markup.html.IHeaderResponse)
147: */
148: public void renderHead(IHeaderResponse response) {
149: YuiLib.load(response);
150: if (enableMonthYearSelection()) {
151: response
152: .renderCSSReference(new CompressedResourceReference(
153: DatePicker.class,
154: "assets/wicket-calendar.css"));
155: }
156:
157: // variables for the initialization script
158: Map variables = new HashMap();
159: String widgetId = getEscapedComponentMarkupId();
160: variables.put("componentId", getComponentMarkupId());
161: variables.put("widgetId", widgetId);
162: variables.put("datePattern", getDatePattern());
163: variables.put("fireChangeEvent", Boolean
164: .valueOf(notifyComponentOnDateSelected()));
165: variables
166: .put("alignWithIcon", Boolean.valueOf(alignWithIcon()));
167: // variables for YUILoader
168: variables.put("pathToWicketDate", RequestCycle.get().urlFor(
169: new JavascriptResourceReference(DatePicker.class,
170: "wicket-date.js")));
171: variables.put("basePath", RequestCycle.get().urlFor(
172: new JavascriptResourceReference(YuiLib.class, "")));
173: variables.put("enableMonthYearSelection", Boolean
174: .valueOf(enableMonthYearSelection()));
175:
176: // print out the initialization properties
177: Properties p = new Properties();
178: configure(p);
179:
180: // ${calendarInit}
181: StringBuffer calendarInit = new StringBuffer();
182: for (Iterator i = p.entrySet().iterator(); i.hasNext();) {
183: Entry entry = (Entry) i.next();
184: calendarInit.append(entry.getKey());
185: Object value = entry.getValue();
186: if (value instanceof CharSequence) {
187: calendarInit.append(":\"");
188: calendarInit.append(Strings.toEscapedUnicode(value
189: .toString()));
190: calendarInit.append("\"");
191: } else if (value instanceof CharSequence[]) {
192: calendarInit.append(":[");
193: CharSequence[] valueArray = (CharSequence[]) value;
194: for (int j = 0; j < valueArray.length; j++) {
195: CharSequence tmpValue = valueArray[j];
196: if (j > 0) {
197: calendarInit.append(",");
198: }
199: if (tmpValue != null) {
200: calendarInit.append("\"");
201: calendarInit.append(Strings
202: .toEscapedUnicode(tmpValue.toString()));
203: calendarInit.append("\"");
204: }
205: }
206: calendarInit.append("]");
207: } else {
208: calendarInit.append(":");
209: calendarInit.append(Strings.toEscapedUnicode(String
210: .valueOf(value)));
211: }
212: if (i.hasNext()) {
213: calendarInit.append(",");
214: }
215: }
216: variables.put("calendarInit", calendarInit.toString());
217:
218: // render initialization script with the variables interpolated
219: TextTemplate datePickerJs = new PackagedTextTemplate(
220: DatePicker.class, "DatePicker.js");
221: datePickerJs.interpolate(variables);
222: response.renderOnDomReadyJavascript(datePickerJs.asString());
223: }
224:
225: /**
226: * Check that this behavior can get a date format out of the component it is
227: * coupled to. It checks whether {@link #getDatePattern()} produces a
228: * non-null value. If that method returns null, and exception will be thrown
229: *
230: * @param component
231: * the component this behavior is being coupled to
232: * @throws UnableToDetermineFormatException
233: * if this date picker is unable to determine a format.
234: */
235: private final void checkComponentProvidesDateFormat(
236: Component component) {
237: if (getDatePattern() == null) {
238: throw new UnableToDetermineFormatException();
239: }
240: }
241:
242: /**
243: * Set widget property if the array is null and has a length greater than 0.
244: *
245: * @param widgetProperties
246: * @param key
247: * @param array
248: */
249: private void setWidgetProperty(Map widgetProperties, String key,
250: String[] array) {
251: if (array != null && array.length > 0) {
252: widgetProperties.put(key, array);
253: }
254: }
255:
256: /**
257: * Whether to position the date picker relative to the trigger icon.
258: *
259: * @return If true, the date picker is aligned with the left position of the
260: * icon, and with the top right under. If false, the date picker
261: * will skip positioning and will let you do the positioning
262: * yourself. Returns true by default.
263: */
264: protected boolean alignWithIcon() {
265: return true;
266: }
267:
268: /**
269: * Append javascript to the initialization function for the YUI widget. Can
270: * be used by subclasses to conveniently extend configuration without having
271: * to write a separate contribution.
272: *
273: * @param markupId
274: * The markup id of the calendar component
275: * @param javascriptId
276: * the non-name spaced javascript id of the widget
277: * @param javascriptWidgetId
278: * the name space id of the widget
279: * @param b
280: * the buffer to append the script to
281: */
282: protected void appendToInit(String markupId, String javascriptId,
283: String javascriptWidgetId, StringBuffer b) {
284: }
285:
286: /**
287: * Gives overriding classes the option of adding (or even changing/
288: * removing) configuration properties for the javascript widget. See <a
289: * href="http://developer.yahoo.com/yui/calendar/">the widget's
290: * documentation</a> for the available options. If you want to override/
291: * remove properties, you should call
292: * {@link super#setWidgetProperties(Properties)} first. If you don't call
293: * that, be aware that you will have to call {@link #localize(Map)} manually
294: * if you like localized strings to be added.
295: *
296: * @param widgetProperties
297: * the current widget properties
298: */
299: protected void configure(Map widgetProperties) {
300: widgetProperties.put("close", Boolean.TRUE);
301: widgetProperties.put("title", " ");
302: // TODO we might want to localize the title nicer in the future, but for
303: // now, people can override this method or put "title" in the map in
304: // localize.
305:
306: // localize date fields
307: localize(widgetProperties);
308:
309: Object modelObject = component.getModelObject();
310: // null and cast check
311: if (modelObject instanceof Date) {
312: Date date = (Date) modelObject;
313: widgetProperties.put("selected", FORMAT_DATE.format(date));
314: widgetProperties.put("pagedate", FORMAT_PAGEDATE
315: .format(date));
316: }
317: }
318:
319: /**
320: * @deprecated Please use {@link #configure(Map)} instead.
321: */
322: // TODO remove this very ugly named method
323: protected final void configureWidgetProperties(Map widgetProperties) {
324: throw new UnsupportedOperationException("");
325: }
326:
327: /**
328: * Filter all empty elements (workaround for {@link DateFormatSymbols}
329: * returning arrays with empty elements).
330: *
331: * @param array
332: * array to filter
333: * @return filtered array (without null or empty string elements)
334: */
335: protected final String[] filterEmpty(String[] array) {
336: if (array == null) {
337: return null;
338: }
339: List l = new ArrayList(array.length);
340: for (int i = 0; i < array.length; i++) {
341: if (!Strings.isEmpty(array[i])) {
342: l.add(array[i]);
343: }
344: }
345: return (String[]) l.toArray(new String[l.size()]);
346: }
347:
348: /**
349: * Gets the id of the component that the calendar widget will get attached
350: * to.
351: *
352: * @return The DOM id of the component
353: */
354: protected final String getComponentMarkupId() {
355: return component.getMarkupId();
356: }
357:
358: /**
359: * @return if true, the base path for all YUI components will be set to
360: * /resources/org.apache.wicket.extensions.yui.YuiLib/. True by
361: * default.
362: */
363: protected boolean getConfigureYUIBasePath() {
364: return true;
365: }
366:
367: /**
368: * Gets the date pattern to use for putting selected values in the coupled
369: * component.
370: *
371: * @return The date pattern
372: */
373: protected String getDatePattern() {
374: String format = null;
375: if (component instanceof ITextFormatProvider) {
376: format = ((ITextFormatProvider) component).getTextFormat();
377: // it is possible that components implement ITextFormatProvider but
378: // don't provide a format
379: }
380:
381: if (format == null) {
382: IConverter converter = component
383: .getConverter(DateTime.class);
384: if (!(converter instanceof DateConverter)) {
385: converter = component.getConverter(Date.class);
386: }
387: format = ((SimpleDateFormat) ((DateConverter) converter)
388: .getDateFormat(component.getLocale())).toPattern();
389: }
390:
391: return format;
392: }
393:
394: /**
395: * Gets the escaped DOM id that the calendar widget will get attached to.
396: * All non word characters (\W) will be removed from the string.
397: *
398: * @return The DOM id of the calendar widget - same as the component's
399: * markup id + 'Dp'}
400: */
401: protected final String getEscapedComponentMarkupId() {
402: return component.getMarkupId().replaceAll("\\W", "");
403: }
404:
405: /**
406: * Gets the id of the icon that triggers the popup.
407: *
408: * @return The id of the icon
409: */
410: protected final String getIconId() {
411: return getEscapedComponentMarkupId() + "Icon";
412: }
413:
414: /**
415: * Gets the style of the icon that triggers the popup.
416: *
417: * @return The style of the icon, e.g. 'cursor: point' etc.
418: */
419: protected String getIconStyle() {
420: return "cursor: pointer; border: none;";
421: }
422:
423: /**
424: * Gets the url for the popup button. Users can override to provide their
425: * own icon URL.
426: *
427: * @return the url to use for the popup button/ icon
428: */
429: protected CharSequence getIconUrl() {
430: return RequestCycle.get().urlFor(
431: new ResourceReference(DatePicker.class, "icon1.gif"));
432: }
433:
434: /**
435: * Gets the locale that should be used to configure this widget.
436: *
437: * @return By default the locale of the bound component.
438: */
439: protected Locale getLocale() {
440: return component.getLocale();
441: }
442:
443: /**
444: * Configure the localized strings for the datepicker widget. This
445: * implementation uses {@link DateFormatSymbols} and some slight string
446: * manupilation to get the strings for months and week days. It should work
447: * well for most locales.
448: * <p>
449: * This method is called from {@link #configureWidgetProperties(Map)} and
450: * can be overriden if you want to customize setting up the localized
451: * strings but are happy with the rest of
452: * {@link #configureWidgetProperties(Map)}'s behavior. Note that you can
453: * call (overridable) method {@link #getLocale()} to get the locale that
454: * should be used for setting up the widget.
455: * </p>
456: * <p>
457: * See YUI Calendar's <a
458: * href="http://developer.yahoo.com/yui/examples/calendar/germany/1.html">
459: * German</a> and <a
460: * href="http://developer.yahoo.com/yui/examples/calendar/japan/1.html">Japanese</a>
461: * examples for more info.
462: * </p>
463: *
464: * @param widgetProperties
465: * the current widget properties
466: */
467: protected void localize(Map widgetProperties) {
468: DateFormatSymbols dfSymbols = new DateFormatSymbols(getLocale());
469: setWidgetProperty(widgetProperties, "MONTHS_SHORT",
470: filterEmpty(dfSymbols.getShortMonths()));
471: setWidgetProperty(widgetProperties, "MONTHS_LONG",
472: filterEmpty(dfSymbols.getMonths()));
473: setWidgetProperty(widgetProperties, "WEEKDAYS_1CHAR",
474: filterEmpty(substring(dfSymbols.getShortWeekdays(), 1)));
475: setWidgetProperty(widgetProperties, "WEEKDAYS_SHORT",
476: filterEmpty(substring(dfSymbols.getShortWeekdays(), 2)));
477: setWidgetProperty(widgetProperties, "WEEKDAYS_MEDIUM",
478: filterEmpty(dfSymbols.getShortWeekdays()));
479: setWidgetProperty(widgetProperties, "WEEKDAYS_LONG",
480: filterEmpty(dfSymbols.getWeekdays()));
481: }
482:
483: /**
484: * Whether to notify the associated component when a date is selected.
485: * Notifying is done by calling the associated component's onchange
486: * Javascript event handler. You can for instance attach an
487: * {@link AjaxEventBehavior} to that component to get a call back to the
488: * server. The default is true.
489: *
490: * @return if true, notifies the associated component when a date is
491: * selected
492: */
493: protected boolean notifyComponentOnDateSelected() {
494: return true;
495: }
496:
497: /**
498: * Makes a copy of the provided array and for each element copy the
499: * substring 0..len to the new array
500: *
501: * @param array
502: * array to copy from
503: * @param len
504: * size of substring for each element to copy
505: * @return copy of the array filled with substrings.
506: */
507: protected final String[] substring(String[] array, int len) {
508: if (array != null) {
509: String[] copy = new String[array.length];
510: for (int i = 0; i < array.length; i++) {
511: String el = array[i];
512: if (el != null) {
513: if (el.length() > len) {
514: copy[i] = el.substring(0, len);
515: } else {
516: copy[i] = el;
517: }
518: }
519: }
520: return copy;
521: }
522: return null;
523: }
524:
525: /**
526: * Indicates whether plain text is rendered or two select boxes are used to
527: * allow direct selection of month and year.
528: *
529: * @return <code>true</code> if select boxes should be rendered to allow
530: * month and year selection.<br/><code>false</code> to render
531: * just plain text.
532: */
533: protected boolean enableMonthYearSelection() {
534: return false;
535: }
536: }
|