View Javadoc
1   /*
2    * Copyright (c) 2011-2024 PrimeFaces Extensions
3    *
4    *  Permission is hereby granted, free of charge, to any person obtaining a copy
5    *  of this software and associated documentation files (the "Software"), to deal
6    *  in the Software without restriction, including without limitation the rights
7    *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    *  copies of the Software, and to permit persons to whom the Software is
9    *  furnished to do so, subject to the following conditions:
10   *
11   *  The above copyright notice and this permission notice shall be included in
12   *  all copies or substantial portions of the Software.
13   *
14   *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20   *  THE SOFTWARE.
21   */
22  package org.primefaces.extensions.component.timepicker;
23  
24  import java.io.IOException;
25  import java.text.ParseException;
26  import java.text.SimpleDateFormat;
27  import java.time.LocalDateTime;
28  import java.time.LocalTime;
29  import java.time.format.DateTimeFormatter;
30  
31  import javax.el.ValueExpression;
32  import javax.faces.component.UIComponent;
33  import javax.faces.context.FacesContext;
34  import javax.faces.context.ResponseWriter;
35  import javax.faces.convert.Converter;
36  import javax.faces.convert.ConverterException;
37  
38  import org.primefaces.extensions.util.Attrs;
39  import org.primefaces.extensions.util.MessageFactory;
40  import org.primefaces.renderkit.InputRenderer;
41  import org.primefaces.util.Constants;
42  import org.primefaces.util.HTML;
43  import org.primefaces.util.LangUtils;
44  import org.primefaces.util.WidgetBuilder;
45  
46  /**
47   * Renderer for the {@link TimePicker} component.
48   *
49   * @author Oleg Varaksin / last modified by Melloware
50   * @version $Revision$
51   * @since 0.3
52   */
53  public class TimePickerRenderer extends InputRenderer {
54  
55      private static final String BUTTON = "button";
56  
57      @Override
58      public void decode(final FacesContext fc, final UIComponent component) {
59          final TimePicker timepicker = (TimePicker) component;
60  
61          if (!shouldDecode(timepicker)) {
62              return;
63          }
64  
65          final String param = timepicker.getClientId(fc) + "_input";
66          final String submittedValue = fc.getExternalContext().getRequestParameterMap().get(param);
67  
68          if (submittedValue != null) {
69              timepicker.setSubmittedValue(submittedValue);
70          }
71  
72          decodeBehaviors(fc, timepicker);
73      }
74  
75      @Override
76      public void encodeEnd(final FacesContext fc, final UIComponent component) throws IOException {
77          final TimePicker timepicker = (TimePicker) component;
78          final String value = getValueAsString(fc, timepicker);
79  
80          encodeMarkup(fc, timepicker, value);
81          encodeScript(fc, timepicker, value);
82      }
83  
84      @Override
85      public Object getConvertedValue(final FacesContext fc, final UIComponent component, final Object submittedValue) {
86          final String value = (String) submittedValue;
87          if (LangUtils.isBlank(value)) {
88              return null;
89          }
90  
91          final TimePicker timepicker = (TimePicker) component;
92          final Converter<?> converter = timepicker.getConverter();
93  
94          // first ask the converter
95          if (converter != null) {
96              return converter.getAsObject(fc, timepicker, value);
97          }
98  
99          // use built-in conversion
100         try {
101             final Class<?> type = resolveDateType(fc, timepicker);
102             if (type == LocalTime.class) {
103                 final DateTimeFormatter formatter = getDateTimeFormatter(timepicker);
104                 return LocalTime.parse(value, formatter);
105             }
106             else {
107                 final SimpleDateFormat formatter = getSimpleDateFormat(timepicker);
108                 return formatter.parse(value);
109             }
110         }
111         catch (final ParseException e) {
112             throw new ConverterException(
113                         MessageFactory.getMessage(timepicker.calculateLocale(),
114                                     TimePicker.TIME_MESSAGE_KEY,
115                                     value,
116                                     getDateTimeFormatter(timepicker).format(LocalDateTime.now()),
117                                     MessageFactory.getLabel(fc, component)),
118                         e);
119         }
120         catch (final Exception e) {
121             throw new ConverterException(e.getMessage(), e);
122         }
123     }
124 
125     protected void encodeMarkup(final FacesContext fc, final TimePicker timepicker, final String value)
126                 throws IOException {
127         final ResponseWriter writer = fc.getResponseWriter();
128         final String clientId = timepicker.getClientId(fc);
129         final String inputId = clientId + "_input";
130 
131         writer.startElement("span", timepicker);
132         writer.writeAttribute("id", clientId, null);
133 
134         String containerClass = TimePicker.CONTAINER_CLASS;
135         if (timepicker.isSpinner()) {
136             containerClass += " ui-spinner";
137         }
138         if (timepicker.isShowOnButton()) {
139             containerClass += " ui-inputgroup";
140         }
141         writer.writeAttribute(Attrs.CLASS, containerClass, null);
142 
143         if (timepicker.isInline()) {
144             // inline container
145             writer.startElement("div", null);
146             writer.writeAttribute("id", clientId + "_inline", null);
147             writer.endElement("div");
148         }
149 
150         writer.startElement("input", null);
151         writer.writeAttribute("id", inputId, null);
152         writer.writeAttribute("name", inputId, null);
153         writer.writeAttribute("type", timepicker.isInline() ? "hidden" : "text", null);
154         if (timepicker.getSize() > 0) {
155             writer.writeAttribute("size", timepicker.getSize(), null);
156         }
157         writer.writeAttribute("autocomplete", "off", null);
158 
159         if (timepicker.isReadonlyInput()) {
160             writer.writeAttribute("readonly", "readonly", null);
161         }
162 
163         if (LangUtils.isNotBlank(value)) {
164             writer.writeAttribute("value", value, null);
165         }
166 
167         if (!timepicker.isInline()) {
168             final String styleClass = getStyleClassBuilder(fc)
169                         .add(TimePicker.INPUT_CLASS)
170                         .add(timepicker.getStyleClass())
171                         .add(timepicker.isSpinner(), "ui-spinner-input")
172                         .add(timepicker.isShowOnButton(), "ui-inputtext")
173                         .add(!timepicker.isValid(), "ui-state-error")
174                         .build();
175 
176             writer.writeAttribute(Attrs.CLASS, styleClass, null);
177 
178             if (timepicker.getStyle() != null) {
179                 writer.writeAttribute(Attrs.STYLE, timepicker.getStyle(), null);
180             }
181         }
182 
183         renderAccessibilityAttributes(fc, timepicker);
184         renderPassThruAttributes(fc, timepicker, HTML.INPUT_TEXT_ATTRS_WITHOUT_EVENTS);
185         renderDomEvents(fc, timepicker, HTML.INPUT_TEXT_EVENTS);
186         renderValidationMetadata(fc, timepicker);
187 
188         writer.endElement("input");
189 
190         if (timepicker.isSpinner()) {
191             final boolean disabled = timepicker.isDisabled() || timepicker.isReadonly();
192             encodeSpinnerButton(fc, TimePicker.UP_BUTTON_CLASS, TimePicker.UP_ICON_CLASS, disabled);
193             encodeSpinnerButton(fc, TimePicker.DOWN_BUTTON_CLASS, TimePicker.DOWN_ICON_CLASS, disabled);
194         }
195 
196         if (timepicker.isShowOnButton()) {
197             writer.startElement(BUTTON, null);
198             writer.writeAttribute(Attrs.CLASS, TimePicker.BUTTON_TRIGGER_CLASS, null);
199             writer.writeAttribute("type", BUTTON, null);
200             writer.writeAttribute("role", BUTTON, null);
201 
202             writer.startElement("span", null);
203             writer.writeAttribute(Attrs.CLASS, TimePicker.BUTTON_TRIGGER_ICON_CLASS, null);
204             writer.endElement("span");
205 
206             writer.startElement("span", null);
207             writer.writeAttribute(Attrs.CLASS, TimePicker.BUTTON_TRIGGER_TEXT_CLASS, null);
208             writer.write("ui-button");
209             writer.endElement("span");
210 
211             writer.endElement(BUTTON);
212         }
213 
214         writer.endElement("span");
215     }
216 
217     protected void encodeScript(final FacesContext fc, final TimePicker timepicker, final String value)
218                 throws IOException {
219         final String clientId = timepicker.getClientId(fc);
220 
221         final WidgetBuilder wb = getWidgetBuilder(fc);
222         wb.init("ExtTimePicker", timepicker);
223         wb.attr("timeSeparator", timepicker.getTimeSeparator());
224         wb.attr("myPosition", timepicker.getDialogPosition());
225         wb.attr("atPosition", timepicker.getInputPosition());
226         wb.attr("showPeriod", timepicker.isShowPeriod());
227         wb.attr("showPeriodLabels", timepicker.isShowPeriod());
228         wb.attr("modeInline", timepicker.isInline());
229         wb.attr("modeSpinner", timepicker.isSpinner());
230         wb.nativeAttr("hours", "{starts:" + timepicker.getStartHours() + ",ends:" + timepicker.getEndHours() + "}");
231         wb.nativeAttr("minutes", "{starts:" + timepicker.getStartMinutes() + ",ends:" + timepicker.getEndMinutes()
232                     + ",interval:" + timepicker.getIntervalMinutes() + "}");
233         wb.attr("rows", timepicker.getRows());
234         wb.attr("showHours", timepicker.isShowHours());
235         wb.attr("showMinutes", timepicker.isShowMinutes());
236         wb.attr("showCloseButton", timepicker.isShowCloseButton());
237         wb.attr("showNowButton", timepicker.isShowNowButton());
238         wb.attr("showDeselectButton", timepicker.isShowDeselectButton());
239 
240         if (timepicker.getOnHourShow() != null) {
241             wb.nativeAttr("onHourShow", timepicker.getOnHourShow());
242         }
243 
244         if (timepicker.getOnMinuteShow() != null) {
245             wb.nativeAttr("onMinuteShow", timepicker.getOnMinuteShow());
246         }
247 
248         if (timepicker.isShowOnButton()) {
249             wb.attr("showOn", timepicker.getShowOn());
250             wb.selectorAttr(BUTTON, "#" + clientId + " .pe-timepicker-trigger");
251         }
252 
253         wb.attr("locale", timepicker.calculateLocale().toString());
254         wb.attr("disabled", timepicker.isDisabled() || timepicker.isReadonly());
255 
256         if (LangUtils.isBlank(value)) {
257             wb.attr("defaultTime", Constants.EMPTY_STRING);
258         }
259         else if (timepicker.isInline()) {
260             wb.attr("defaultTime", value);
261         }
262 
263         if (timepicker.getMinHour() != null || timepicker.getMinMinute() != null) {
264             wb.nativeAttr("minTime", "{hour:" + timepicker.getMinHour()
265                         + ",minute:" + timepicker.getMinMinute() + "}");
266         }
267 
268         if (timepicker.getMaxHour() != null || timepicker.getMaxMinute() != null) {
269             wb.nativeAttr("maxTime", "{hour:" + timepicker.getMaxHour()
270                         + ",minute:" + timepicker.getMaxMinute() + "}");
271         }
272 
273         encodeClientBehaviors(fc, timepicker);
274 
275         wb.finish();
276     }
277 
278     protected void encodeSpinnerButton(final FacesContext fc, String styleClass, final String iconClass,
279                 final boolean disabled)
280                 throws IOException {
281         final ResponseWriter writer = fc.getResponseWriter();
282         styleClass = disabled ? styleClass + " ui-state-disabled" : styleClass;
283 
284         writer.startElement("a", null);
285 
286         writer.writeAttribute(Attrs.CLASS, styleClass, null);
287         writer.startElement("span", null);
288         writer.writeAttribute(Attrs.CLASS, "ui-button-text", null);
289         writer.startElement("span", null);
290         writer.writeAttribute(Attrs.CLASS, iconClass, null);
291         writer.endElement("span");
292         writer.endElement("span");
293 
294         writer.endElement("a");
295     }
296 
297     protected String getValueAsString(final FacesContext fc, final TimePicker timepicker) {
298         final Object submittedValue = timepicker.getSubmittedValue();
299         if (submittedValue != null) {
300             return submittedValue.toString();
301         }
302 
303         final Object value = timepicker.getValue();
304         if (value == null) {
305             return null;
306         }
307         else {
308             if (timepicker.getConverter() != null) {
309                 // convert via registered converter
310                 return timepicker.getConverter().getAsString(fc, timepicker, value);
311             }
312             else {
313                 // use built-in converter
314                 if (value instanceof LocalTime) {
315                     final DateTimeFormatter formatter = getDateTimeFormatter(timepicker);
316                     return formatter.format((LocalTime) value);
317                 }
318                 else {
319                     final SimpleDateFormat formatter = getSimpleDateFormat(timepicker);
320                     return formatter.format(value);
321                 }
322             }
323         }
324     }
325 
326     protected String getPattern(final TimePicker timepicker) {
327         if (!timepicker.isShowMinutes()) {
328             return timepicker.getTimePatternWithoutMinutes();
329         }
330         else if (!timepicker.isShowHours()) {
331             return timepicker.getTimePatternWithoutHours();
332         }
333         else if (timepicker.isShowPeriod()) {
334             return timepicker.getTimePattern12();
335         }
336         else {
337             return timepicker.getTimePattern24();
338         }
339     }
340 
341     protected DateTimeFormatter getDateTimeFormatter(final TimePicker timepicker) {
342         return DateTimeFormatter.ofPattern(getPattern(timepicker), timepicker.calculateLocale());
343     }
344 
345     protected SimpleDateFormat getSimpleDateFormat(final TimePicker timepicker) {
346         return new SimpleDateFormat(getPattern(timepicker), timepicker.calculateLocale());
347     }
348 
349     protected Class<?> resolveDateType(final FacesContext context, final TimePicker timePicker) {
350         final ValueExpression ve = timePicker.getValueExpression("value");
351 
352         if (ve == null) {
353             return null;
354         }
355 
356         Class<?> type = ve.getType(context.getELContext());
357 
358         // If type could not be determined via value-expression try it this way. (Very unlikely, this happens in real world.)
359         if (type == null) {
360             type = LocalTime.class;
361         }
362 
363         return type;
364     }
365 }