1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2010, 2011, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2010, 2011, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25 * Creates up to three separate DwtSelects for the time (hour, minute, am|pm)
 26 * Showing the AM|PM select widget is dependent on the user's locale
 27 * 
 28 * @author Parag Shah
 29 *
 30 * @param parent		[DwtComposite]	the parent widget
 31 * @param id			[string]*		an ID that is propagated to component select objects
 32  *
 33  * @private
 34 */
 35 DwtTimeSelect = function(parent, id) {
 36 	DwtComposite.call(this, {parent:parent, className: 'DwtTimeSelect'});
 37 
 38 	this.id = id;
 39 	this._isLocale24Hour = true;
 40 	this._createSelects();
 41 };
 42 
 43 // IDs for types of time selects
 44 DwtTimeSelect.START	= 1;
 45 DwtTimeSelect.END	= 2;
 46 
 47 // IDs for time select components
 48 DwtTimeSelect.HOUR	= 1;
 49 DwtTimeSelect.MINUTE	= 2;
 50 DwtTimeSelect.AMPM	= 3;
 51 
 52 DwtTimeSelect.getDateFromFields =
 53 function(hours, minutes, ampm, date) {
 54 	hours = Number(hours);
 55 	if (ampm) {
 56 		if (ampm == "AM" || ampm === 0) {
 57 			hours = (hours == 12) ? 0 : hours;
 58 		} else if (ampm == "PM" || ampm == 1) {
 59 			hours = (hours < 12) ? hours + 12 : hours;
 60 		}
 61 	}
 62 	date = date ? date : new Date();
 63 	date.setHours(hours, Number(minutes), 0, 0);
 64 	return date;
 65 };
 66 
 67 DwtTimeSelect.parse =
 68 function(timeString) {
 69     var date;
 70 	var lTimeString = timeString.toLowerCase();
 71 	if (lTimeString === AjxMsg.midnight.toLowerCase() || lTimeString === AjxMsg.noon.toLowerCase()) {
 72 		date = new Date();
 73 		date.setMinutes(0);
 74 		date.setSeconds(0);
 75 		date.setHours(lTimeString === AjxMsg.noon.toLowerCase() ? 12 : 0);
 76 	} else {
 77 		var timeFormatter = AjxDateFormat.getTimeInstance(AjxDateFormat.SHORT);    
 78 		date = timeFormatter.parse(timeString) || AjxDateFormat.parseTime(timeString);
 79 	}
 80     return date;
 81 };
 82 
 83 DwtTimeSelect.format =
 84 function(date) {
 85 	if (date.getHours() == 0 && date.getMinutes() == 0) {
 86 		return AjxMsg.midnight;
 87 	} else if (date.getHours() == 12 && date.getMinutes() == 0) {
 88 		return AjxMsg.noon;
 89 	} else {
 90 		return AjxDateFormat.getTimeInstance(AjxDateFormat.SHORT).format(date);
 91 	}
 92 };
 93 
 94 /**
 95 * Adjust an appt's start or end based on changes to the other one. If the user changes
 96 * the start time, change the end time so that the appt duration is maintained. If the
 97 * user changes the end time, we leave things alone.
 98 *
 99 * @param ev					[Event]				UI event from a DwtSelect
100 * @param startSelect		[DwtTimeSelect]		start time select
101 * @param endSelect			[DwtTimeSelect]		end time select
102 * @param startDateField		[element]			start date field
103 * @param endDateField		[element]			end date field
104 */
105 DwtTimeSelect.adjustStartEnd =
106 function(ev, startSelect, endSelect, startDateField, endDateField) {
107 	var select = ev._args.selectObj;
108 	var startDate = AjxDateUtil.simpleParseDateStr(startDateField.value);
109 	var endDate = AjxDateUtil.simpleParseDateStr(endDateField.value);
110 	var startDateOrig = startDateField.value;
111 	var endDateOrig = endDateField.value;
112 	if (select.id == DwtTimeSelect.START) {
113 		var hours = (select.compId == DwtTimeSelect.HOUR) ? ev._args.oldValue : startSelect.getHours();
114 		var minutes = (select.compId == DwtTimeSelect.MINUTE) ? ev._args.oldValue : startSelect.getMinutes();
115 		var ampm = (select.compId == DwtTimeSelect.AMPM) ? ev._args.oldValue : startSelect.getAmPm();
116 		var oldStartDateMs = DwtTimeSelect.getDateFromFields(hours, minutes, ampm, startDate).getTime();
117 		var newStartDateMs = DwtTimeSelect.getDateFromFields(startSelect.getHours(), startSelect.getMinutes(), startSelect.getAmPm(), startDate).getTime();
118 		var oldEndDateMs = DwtTimeSelect.getDateFromFields(endSelect.getHours(), endSelect.getMinutes(), endSelect.getAmPm(), endDate).getTime();
119 		var delta = oldEndDateMs - oldStartDateMs;
120 		if (!delta) return null;
121 		var newEndDateMs = newStartDateMs + delta;
122 		var newEndDate = new Date(newEndDateMs);
123 		endSelect.set(newEndDate);
124 		endDateField.value = AjxDateUtil.simpleComputeDateStr(newEndDate);
125 		if (endDateField.value != endDateOrig) {
126 			return endDateField;
127 		}
128 	} else {
129 		return null;
130 	}
131 };
132 
133 /**
134  * Returns true if the start date/time is before the end date/time.
135  *
136  * @param ss				[DwtTimeSelect]		start time select
137  * @param es				[DwtTimeSelect]		end time select
138  * @param startDateField	[element]			start date field
139  * @param endDateField		[element]			end date field
140  */
141 DwtTimeSelect.validStartEnd =
142 function(startDateField, endDateField, ss, es) {
143 	var startDate = AjxDateUtil.simpleParseDateStr(startDateField.value);
144 	var endDate = AjxDateUtil.simpleParseDateStr(endDateField.value);
145 
146     if (startDate && endDate) {
147         if((startDate.valueOf() > endDate.valueOf())){
148             return false;
149         }
150         // bug fix #11329 - dont allow year to be more than the earth will be around :]
151 		if (startDate.getFullYear() > 9999 || endDate.getFullYear() > 9999) {
152 			return false;
153 		}
154         if(ss && es){
155             var startDateMs = DwtTimeSelect.getDateFromFields(ss.getHours(), ss.getMinutes(), ss.getAmPm(), startDate).getTime();
156             var endDateMs = DwtTimeSelect.getDateFromFields(es.getHours(), es.getMinutes(), es.getAmPm(), endDate).getTime();
157             if (startDateMs > endDateMs) {
158                 return false;
159             }
160         }
161     } else {
162 		return false;
163 	}
164 	return true;
165 };
166 
167 DwtTimeSelect.prototype = new DwtComposite;
168 DwtTimeSelect.prototype.constructor = DwtTimeSelect;
169 DwtTimeSelect.prototype.isDwtTimeSelect = true;
170 
171 DwtTimeSelect.prototype.toString = function() {
172     return 'DwtTimeSelect';
173 };
174 
175 /**
176 * Sets the time select according to the given date.
177 *
178 * @param date	[Date]		a Date object
179 */
180 DwtTimeSelect.prototype.set = 
181 function(date) {
182 
183 	var hourIdx = 0, minuteIdx = 0, amPmIdx = 0;
184 	var isLocale24Hour = this.isLocale24Hour();
185 
186 	var hours = date.getHours();
187 	if (!isLocale24Hour && hours > 12) {
188 		hourIdx = hours - 13;
189 	} else if (!isLocale24Hour && hours == 0) {
190 		hourIdx = this.getHourSelectSize() - 1;
191 	} else {
192 		hourIdx = isLocale24Hour ? hours : hours - 1;
193 	}
194 
195 	minuteIdx = Math.floor(date.getMinutes() / 5);
196 
197 	if (!isLocale24Hour) {
198 		amPmIdx = (date.getHours() >= 12) ? 1 : 0;
199 	}
200 
201 	this.setSelected(hourIdx, minuteIdx, amPmIdx);
202 };
203 
204 
205 /**
206  * Returns a date object with the hours and minutes set based on
207  * the values of this time select.
208  *
209  * @param date [Date] Optional. If specified, the hour and minute
210  *                    values will be set on the specified object;
211  *                    else, a new <code>Date</code> object is created.
212  */
213 DwtTimeSelect.prototype.getValue =
214 function(date) {
215 	return (DwtTimeSelect.getDateFromFields(this.getHours(), this.getMinutes(), this.getAmPm(), date));
216 };
217 
218 DwtTimeSelect.prototype.getHours =
219 function() {
220 	return this._hourSelect.getValue();
221 };
222 
223 DwtTimeSelect.prototype.getMinutes =
224 function() {
225 	return this._minuteSelect.getValue();
226 };
227 
228 DwtTimeSelect.prototype.getAmPm =
229 function() {
230 	return this._amPmSelect ? this._amPmSelect.getValue() : null;
231 };
232 
233 DwtTimeSelect.prototype.setSelected = 
234 function(hourIdx, minuteIdx, amPmIdx) {
235 	this._hourSelect.setSelected(hourIdx);
236 	this._minuteSelect.setSelected(minuteIdx);
237 	if (!this._isLocale24Hour) {
238 		this._amPmSelect.setSelected(amPmIdx);
239 	}
240 };
241 
242 DwtTimeSelect.prototype.addChangeListener = 
243 function(listener) {
244 	this._hourSelect.addChangeListener(listener);
245 	this._minuteSelect.addChangeListener(listener);
246 	if (this._amPmSelect)
247 		this._amPmSelect.addChangeListener(listener);
248 };
249 
250 DwtTimeSelect.prototype.isLocale24Hour = 
251 function() {
252 	return this._isLocale24Hour;
253 };
254 
255 DwtTimeSelect.prototype.getHourSelectSize = 
256 function() {	
257 	return this._hourSelect.size();
258 };
259 
260 DwtTimeSelect.prototype.getMinuteSelectSize = 
261 function() {	
262 	return this._minuteSelect.size();
263 };
264 
265 DwtTimeSelect.prototype.getSelectedHourIdx = 
266 function() {
267 	return this._hourSelect.getSelectedIndex();
268 };
269 
270 DwtTimeSelect.prototype.getSelectedMinuteIdx = 
271 function() {
272 	return this._minuteSelect.getSelectedIndex();
273 };
274 
275 DwtTimeSelect.prototype.getSelectedAmPmIdx = 
276 function() {
277 	return this._amPmSelect ? this._amPmSelect.getSelectedIndex() : 0;
278 };
279 
280 DwtTimeSelect.prototype.setEnabled =
281 function(enabled) {
282    DwtComposite.prototype.setEnabled.call(this, enabled);
283 
284    this._hourSelect.setEnabled(enabled);
285    this._minuteSelect.setEnabled(enabled);
286    if (this._amPmSelect) this._amPmSelect.setEnabled(enabled);
287 };
288 
289 DwtTimeSelect.prototype._createSelects =
290 function() {
291 	this._hourSelectId = Dwt.getNextId();
292 	this._minuteSelectId = Dwt.getNextId();
293 	this._amPmSelectId = Dwt.getNextId();
294 
295 	// get the time formatter for the user's locale
296 	var timeFormatter = AjxDateFormat.getTimeInstance(AjxDateFormat.SHORT);
297 	var hourSegmentIdx = 0;
298 	var minuteSegmentIdx = 0;
299 
300 	var html = [];
301 	var i = 0;
302 
303 	html[i++] = "<table border=0 cellpadding=0 cellspacing=0><tr>";
304 
305 	// walk time formatter's segments array to render each segment part in the right order
306 	for (var j = 0; j < timeFormatter._segments.length; j++) {
307 		var segmentStr = timeFormatter._segments[j]._s;
308 
309 		if (timeFormatter._segments[j] instanceof AjxFormat.TextSegment) {
310 			var trimStr = AjxStringUtil.trim(segmentStr);
311 			if (trimStr.length) {
312 				html[i++] = "<td class='TextPadding ZmFieldLabel'>"
313 				html[i++] = segmentStr;
314 				html[i++] = "</td>";
315 			}
316 		} else if (segmentStr.charAt(0) == "h" || segmentStr.charAt(0) == "H") {
317 			hourSegmentIdx = j;
318 			html[i++] = "<td width=42 id='"
319 			html[i++] = this._hourSelectId;
320 			html[i++] = "'></td>";
321 		} else if (segmentStr.charAt(0) == "m") {
322 			minuteSegmentIdx = j;
323 			html[i++] = "<td width=42 id='"
324 			html[i++] = this._minuteSelectId;
325 			html[i++] = "'></td>";
326 		} else if (segmentStr == "a") {	
327 			this._isLocale24Hour = false;
328 			html[i++] = "<td width=42 id='"
329 			html[i++] = this._amPmSelectId;
330 			html[i++] = "'></td>";
331 		}
332 	}
333 	
334 	html[i++] = "</tr></table>";
335 
336 	// append html template to DOM
337 	this.getHtmlElement().innerHTML = html.join("");
338 
339 	// init vars for adding hour DwtSelect
340 	var now = new Date();
341 	var start = this._isLocale24Hour ? 0 : 1;
342 	var limit = this._isLocale24Hour ? 24 : 13;
343 
344 	// create new DwtSelect for hour slot
345 	this._hourSelect = new DwtSelect({parent:this});
346 	this._hourSelect.id = this.id;
347 	this._hourSelect.compId = DwtTimeSelect.HOUR;
348 	for (var i = start; i < limit; i++) {
349 		now.setHours(i);
350 		var label = timeFormatter._segments[hourSegmentIdx].format(now);
351 		this._hourSelect.addOption(label, false, i);
352 	}
353 	this._hourSelect.reparentHtmlElement(this._hourSelectId);
354 	delete this._hourSelectId;
355 
356 	// create new DwtSelect for minute slot
357 	this._minuteSelect = new DwtSelect({parent:this});
358 	this._minuteSelect.id = this.id;
359 	this._minuteSelect.compId = DwtTimeSelect.MINUTE;
360 	for (var i = 0; i < 60; i = i + 5) {
361 		now.setMinutes(i);
362 		var label = timeFormatter._segments[minuteSegmentIdx].format(now);
363 		this._minuteSelect.addOption(label, false, i);
364 	}
365 	this._minuteSelect.reparentHtmlElement(this._minuteSelectId);
366 	delete this._minuteSelectId;
367 
368 	// if locale is 12-hour time, add AM|PM DwtSelect
369 	if (!this._isLocale24Hour) {
370 		this._amPmSelect = new DwtSelect({parent:this});
371 		this._amPmSelect.id = this.id;
372 		this._amPmSelect.compId = DwtTimeSelect.AMPM;
373 		this._amPmSelect.addOption(I18nMsg["periodAm"], false, "AM");
374 		this._amPmSelect.addOption(I18nMsg["periodPm"], false, "PM");
375 		this._amPmSelect.reparentHtmlElement(this._amPmSelectId);
376 		delete this._amPmSelectId;
377 	}
378 };
379 
380 /**
381 * Creates up to three separate DwtSelects for the time (hour, minute, am|pm)
382 * Showing the AM|PM select widget is dependent on the user's locale
383 *
384 * @author Parag Shah
385 *
386 * @param parent		[DwtComposite]	the parent widget
387 * @param id			[string]*		an ID that is propagated to component select objects
388  *
389  * @private
390 */
391 DwtTimeInput = function(parent, id, parentElement, interval) {
392     var params = {parent:parent, id: "DwtTimeInput", className: 'DwtTimeInput'};
393     if(parentElement) {
394         params.parentElement = parentElement;
395     }
396 	DwtComposite.call(this, params);
397 
398     this._interval = interval || DwtTimeInput.FIFTEEN_MIN_INTERVAL;
399 	this.id = id;
400 	this._isLocale24Hour = true;
401 	this._createSelects();
402     this._useTextInput = true;
403 };
404 
405 DwtTimeInput.THIRTY_MIN_INTERVAL = 30;
406 DwtTimeInput.FIFTEEN_MIN_INTERVAL = 15;
407 
408 // IDs for types of time selects
409 DwtTimeInput.START	= 1;
410 DwtTimeInput.END	= 2;
411 
412 // IDs for time select components
413 DwtTimeInput.HOUR	= 1;
414 DwtTimeInput.MINUTE	= 2;
415 DwtTimeInput.AMPM	= 3;
416 
417 DwtTimeInput.ROWS	= 8; // Show 8 rows at a time
418 DwtTimeInput.DEFAULT_TOP_ROW	= 8; // Make row 8 (8 AM) the initial topmost visible row unless overridden
419 
420 DwtTimeInput.getDateFromFields =
421 function(timeStr, date) {
422     var formattedDate = DwtTimeSelect.parse(timeStr);
423     date = date || new Date();
424     date.setHours(formattedDate.getHours(), formattedDate.getMinutes(), 0, 0);
425     return date;
426 };
427 
428 /**
429 * Adjust an appt's start or end based on changes to the other one. If the user changes
430 * the start time, change the end time so that the appt duration is maintained. If the
431 * user changes the end time, we leave things alone.
432 *
433 * @param ev					[Event]				UI event from a DwtSelect
434 * @param startSelect		[DwtTimeInput]		start time select
435 * @param endSelect			[DwtTimeInput]		end time select
436 * @param startDateField		[element]			start date field
437 * @param endDateField		[element]			end date field
438 * @param dateInfo		    [object]			date info used to calculate the old time before changing this
439 * @param id		            [string]			an ID which got changed 
440 */
441 DwtTimeInput.adjustStartEnd =
442 function(ev, startSelect, endSelect, startDateField, endDateField, dateInfo, id) {
443     var startDate = AjxDateUtil.simpleParseDateStr(startDateField.value);
444     var endDate = AjxDateUtil.simpleParseDateStr(endDateField.value);
445     var startDateOrig = startDateField.value;
446     var endDateOrig = endDateField.value;
447     if (id == DwtTimeInput.START) {
448         var timeStr = dateInfo ? dateInfo.startTimeStr : startSelect.getTimeString();
449         var oldStartDateMs = DwtTimeInput.getDateFromFields(timeStr, startDate).getTime();
450         var newStartDateMs = DwtTimeInput.getDateFromFields(startSelect.getTimeString(), startDate).getTime();
451         var oldEndDateMs = DwtTimeInput.getDateFromFields(endSelect.getTimeString(), endDate).getTime();
452 
453         var delta = oldEndDateMs - oldStartDateMs;
454         if (!delta) return null;
455 
456         var newEndDateMs = newStartDateMs + delta;
457         var newEndDate = new Date(newEndDateMs);
458 
459         startSelect.set(new Date(newStartDateMs));
460         endSelect.set(newEndDate);
461         endDateField.value = AjxDateUtil.simpleComputeDateStr(newEndDate);
462 
463         if (endDateField.value != endDateOrig) {
464             return endDateField;
465         }
466     } else if (id == DwtTimeInput.END){
467         var timeStr = dateInfo ? dateInfo.endTimeStr : endSelect.getTimeString();
468         var oldEndDateMs = DwtTimeInput.getDateFromFields(timeStr, endDate).getTime();
469         var newEndDateMs = DwtTimeInput.getDateFromFields(endSelect.getTimeString(), endDate).getTime();
470         var oldStartDateMs = DwtTimeInput.getDateFromFields(startSelect.getTimeString(), startDate).getTime();
471 
472         var delta = oldEndDateMs - oldStartDateMs;
473         if (!delta) return null;
474 
475         //adjust start date only when the end date falls earlier than start date
476         if(newEndDateMs < oldStartDateMs) {
477             var newStartDateMs = newEndDateMs - delta;
478             var newStartDate = new Date(newStartDateMs);
479 
480             startSelect.set(newStartDate);
481             endSelect.set(new Date(newEndDateMs));
482             startDateField.value = AjxDateUtil.simpleComputeDateStr(newStartDate);
483             endDateField.value = AjxDateUtil.simpleComputeDateStr(new Date(newEndDateMs));
484         }
485 
486         if (startDateField.value != startDateOrig) {
487             return startDateField;
488         }
489 
490     } else {
491         return null;
492     }
493 };
494 
495 /**
496  * Returns true if the start date/time is before the end date/time.
497  *
498  * @param ss				[DwtTimeInput]		start time select
499  * @param es				[DwtTimeInput]		end time select
500  * @param startDateField	[element]			start date field
501  * @param endDateField		[element]			end date field
502  */
503 DwtTimeInput.validStartEnd =
504 function(startDateField, endDateField, ss, es) {
505 	var startDate = AjxDateUtil.simpleParseDateStr(startDateField.value);
506 	var endDate = AjxDateUtil.simpleParseDateStr(endDateField.value);
507 
508 	if (startDate && endDate) {
509 		if((startDate.valueOf() > endDate.valueOf())) {
510 			return false;
511 		}
512 		// bug fix #11329 - dont allow year to be more than the earth will be around :]
513 		if (startDate.getFullYear() > 9999 || endDate.getFullYear() > 9999) {
514 			return false;
515 		}
516 		if (ss && es) {
517 			var startTime = ss.getTimeString();
518 			var endTime = es.getTimeString();
519 			if (startTime && endTime) {
520 				var startDateMs = DwtTimeInput.getDateFromFields(startTime, startDate).getTime();
521 				var endDateMs = DwtTimeInput.getDateFromFields(endTime, endDate).getTime();
522 				if (startDateMs > endDateMs) {
523 					return false;
524 				}
525 			}
526 		}
527 	} else {
528 		return false;
529 	}
530 	return true;
531 };
532 
533 DwtTimeInput.prototype = new DwtComposite;
534 DwtTimeInput.prototype.constructor = DwtTimeInput;
535 DwtTimeInput.prototype.isDwtTimeInput = true;
536 
537 DwtTimeInput.prototype.toString = function() {
538     return 'DwtTimeInput';
539 };
540 
541 /**
542 * Sets the time select according to the given date.
543 *
544 * @param date	[Date]		a Date object
545 */
546 DwtTimeInput.prototype.set =
547 function(date) {
548     var timeStr = DwtTimeSelect.format(date);
549     this._originalTimeStr = timeStr;
550     this._timeSelectInput.setValue(timeStr);
551     this._scrollToValue(timeStr);
552 };
553 
554 /**
555 * Sets the time string after validating it
556 *
557 * @param date	[Date]		a Date object
558 */
559 DwtTimeInput.prototype.setValue =
560 function(str) {
561     //sets only if the date is valid
562     var date = DwtTimeSelect.parse(str);
563     if (!date) str = "";
564     this._originalTimeStr = str;
565     this._timeSelectInput.setValue(str);
566     this._scrollToValue(str);
567 };
568 
569 DwtTimeInput.prototype._scrollToValue =
570 function(str) {
571     var index = this.getTimeIndex(str);
572     if (index !== null)
573         this._hoursSelectMenu.setSelectedItem(index);
574 };
575 
576 /**
577  * Returns a date object with the hours and minutes set based on
578  * the values of this time picker.
579  *
580  * @param date [Date] Optional. If specified, the hour and minute
581  *                    values will be set on the specified object;
582  *                    else, a new <code>Date</code> object is created.
583  */
584 DwtTimeInput.prototype.getValue =
585 function(date) {
586 	//return (DwtTimeInput.getDateFromFields(this.getHours(), this.getMinutes(), this.getAmPm(), date));
587     var d = DwtTimeSelect.parse(this._timeSelectInput.getValue());
588 	if(!d) {
589 		d = new Date();
590 	}
591     date = date || new Date();
592     //daylight saving time
593     if(AjxDateUtil.isDayShifted(date)) {
594         AjxDateUtil.rollToNextDay(date);
595     }
596 	
597     date.setHours(d.getHours(), d.getMinutes(), 0, 0);
598     return date;
599 };
600 
601 DwtTimeInput.prototype.getHours =
602 function() {
603     var d = this.getValue();
604     return d ? d.getHours() : null;
605 };
606 
607 DwtTimeInput.prototype.getMinutes =
608 function() {
609     var d = this.getValue();
610     return d ? d.getMinutes() : null;
611 };
612 
613 DwtTimeInput.prototype.addChangeListener =
614 function(listener) {
615     this._changeListener = listener;
616     var callback = AjxCallback.simpleClosure(this.handleTimeChange, this, listener);
617     this._timeSelectInput.setHandler(DwtEvent.ONBLUR, callback);
618 };
619 
620 DwtTimeInput.prototype.handleTimeChange =
621 function(listener, ev) {
622     //restore old value if the new time is not in correct format
623     var str = this._timeSelectInput.getValue();
624     var d = DwtTimeSelect.parse(str);
625     if(!d) {
626         //TODO: Try to guess the time 
627         /*var newDate = this.correctTimeString(str, DwtTimeSelect.parse(this._originalTimeStr));
628         this.setValue(DwtTimeSelect.format(newDate) || "");*/
629         this.setValue(this._originalTimeStr);
630     } else {
631         this._scrollToValue(str);
632     }
633 
634     listener.run(ev, this.id);
635 };
636 
637 DwtTimeInput.prototype.correctTimeString =
638 function(val, originalDate) {
639 
640     var segments = val.split(":");
641 
642     if(!segments) return originalDate;
643 
644     var hrs = (segments.length && segments[0] != null) ? parseInt(segments[0].replace(/\D/g, "")) : null;
645     var mins = (segments.length > 1 && segments[1]!= null) ? parseInt(segments[1].replace(/\D/g, "")) : 0;
646 
647     if(!hrs) hrs = (hrs == 0) ? 0 : originalDate.getHours();
648     if(!mins) mins = 0;
649 
650     originalDate.setHours(hrs, mins, 0, 0);
651 
652     return originalDate;
653 
654 };
655 
656 DwtTimeInput.prototype.isLocale24Hour =
657 function() {
658 	return this._isLocale24Hour;
659 };
660 
661 DwtTimeInput.prototype.setEnabled =
662 function(enabled) {
663    DwtComposite.prototype.setEnabled.call(this, enabled);
664    this._timeSelectInput.setEnabled(enabled);
665    this._timeSelectBtn.setEnabled(enabled);
666 };
667 
668 
669 DwtTimeInput.prototype._timeButtonListener =
670 function(ev) {
671 	var timeFormatter = AjxDateFormat.getTimeInstance(AjxDateFormat.SHORT);
672 	var defaultTopMenuItem;
673     if(!this._menuItemsAdded) {
674         var j,
675             k,
676             mi,
677             smi,
678             text,
679             maxMinutesItem,
680             minutesSelectMenu,
681             now = new Date(),
682             timeSelectButton = this._timeSelectBtn,
683             menuSelectionListener = new AjxListener(this, this._timeSelectionListener);
684 
685         for (j = 0; j < 24; j++) {
686             now.setHours(j);
687             now.setMinutes(0);
688 
689             mi = new DwtMenuItem({parent: this._hoursSelectMenu, style: DwtMenuItem.NO_STYLE});
690             text = timeFormatter.format(now); // Regular formatter, returns the I18nMsg formatted time
691             this.putTimeIndex(text, j);
692 
693 			if (j==0 || j==12) {
694                 text = DwtTimeSelect.format(now); // Specialized formatter, returns AjxMsg.midnight for midnight and AjxMsg.noon for noon
695                 this.putTimeIndex(text, j); // Both should go in the indexer
696             }
697 
698             mi.setText(text);
699             mi.setData("value", j*60);
700             if (menuSelectionListener) mi.addSelectionListener(menuSelectionListener);
701             if (j == DwtTimeInput.DEFAULT_TOP_ROW) defaultTopMenuItem = mi;
702 
703             maxMinutesItem = 60 / this._interval;
704             minutesSelectMenu = new DwtMenu({parent:mi, style:DwtMenu.DROPDOWN_CENTERV_STYLE, layout:DwtMenu.LAYOUT_CASCADE, maxRows:maxMinutesItem, congruent: true});
705             mi.setMenu(minutesSelectMenu, true);
706             mi.setSelectableWithSubmenu(true);
707 			minutesSelectMenu.dontStealFocus(true);
708 			for (k = 1; k < maxMinutesItem; k++) {
709                 now.setMinutes(k*this._interval);
710                 smi = new DwtMenuItem({parent: minutesSelectMenu, style: DwtMenuItem.NO_STYLE});
711                 smi.setText(timeFormatter.format(now));
712                 smi.setData("value", j*60 + k*this._interval);
713                 if (menuSelectionListener) smi.addSelectionListener(menuSelectionListener);
714             }
715         }
716         this._hoursSelectMenu.setWidth(timeSelectButton.getW() + this._timeSelectInput.getW());
717 
718         this._menuItemsAdded = true;
719     }
720 	ev.item.popup();
721 	if (defaultTopMenuItem) {
722 		this._hoursSelectMenu.scrollToItem(defaultTopMenuItem);
723 	}
724 	this._scrollToValue(timeFormatter.format(this.getValue()));
725 };
726 
727 DwtTimeInput.prototype._timeSelectionListener =
728 function(ev) {
729     if(ev.item && ev.item instanceof DwtMenuItem){
730        this._timeSelectInput.setValue(ev.item.getText());
731        this._timeSelectValue = ev.item.getData("value");
732        if(this._changeListener) this._changeListener.run(ev, this.id);
733        return;
734     }
735 };
736 
737 DwtTimeInput.prototype.getTimeString =
738 function() {
739     //validate and returns only valid time string
740     var date = DwtTimeSelect.parse(this._timeSelectInput.getValue());
741     return date ? this._timeSelectInput.getValue() : "";    
742 };
743 
744 DwtTimeInput.prototype.getInputField =
745 function() {
746     return this._timeSelectInput;
747 };
748 
749 DwtTimeInput.prototype.getTabGroupMember =
750 function() {
751     return this._tabGroup;
752 };
753 
754 DwtTimeInput.prototype.putTimeIndex =
755 function(text, value) {
756     this._timeIndex[text.replace(/\:\d\d/, ":00").replace(/\s/,"").toLowerCase()] = value;
757 };
758 
759 DwtTimeInput.prototype.getTimeIndex =
760 function(text) {
761     if (!text) return null;
762     var index = this._timeIndex[text.replace(/\:\d\d/, ":00").replace(/\s/,"").toLowerCase()];
763     return (index || index===0) ? index : null;
764 };
765 
766 DwtTimeInput.prototype._createSelects =
767 function() {
768 	var label = (this.id === DwtTimeSelect.START ? ZmMsg.startTime :
769 	             this.id === DwtTimeSelect.END ? ZmMsg.endTime :
770 	             ZmMsg.time);
771 
772 	// get the time formatter for the user's locale
773 
774 	this.getHtmlElement().innerHTML = AjxTemplate.expand("calendar.Appointment#ApptTimeInput", {id: this._htmlElId});
775 
776     var inputId = Dwt.getNextId("DwtTimeInputSelect_");
777     if (this.id && this.id == DwtTimeSelect.START) {
778        inputId += "_startTimeInput";
779     }
780     else if (this.id && this.id == DwtTimeSelect.END) {
781         inputId += "_endTimeInput";
782     }
783     //create time select input field
784     var params = {
785         parent: this,
786         parentElement: (this._htmlElId + "_timeSelectInput"),
787         type: DwtInputField.STRING,
788         label: label,
789         errorIconStyle: DwtInputField.ERROR_ICON_NONE,
790         validationStyle: DwtInputField.CONTINUAL_VALIDATION,
791         inputId: inputId,
792 	    id: Dwt.getNextId("DwtTimeInputField_")
793     };
794 
795     this._timeSelectInput = new DwtInputField(params);
796     var timeInputEl = this._timeSelectInput.getInputElement();
797     Dwt.setSize(timeInputEl, "80px", "2rem");
798     timeInputEl.typeId = this.id;
799     //listeners
800     var buttonListener = new AjxListener(this, this._timeButtonListener);
801     var buttonId = this._htmlElId + "_timeSelectBtn";
802     //create time select drop down button
803     var timeSelectButton = this._timeSelectBtn = new DwtButton({parent:this});
804     timeSelectButton.addDropDownSelectionListener(buttonListener);
805 
806     timeSelectButton.setData(Dwt.KEY_ID, buttonId);
807     timeSelectButton.setSize("20");
808     timeSelectButton.setAttribute('aria-label', label);
809     
810     this._timeIndex = {};
811     // create menu for button
812     this._hoursSelectMenu = new DwtMenu({parent:timeSelectButton, style:DwtMenu.DROPDOWN_STYLE, layout:DwtMenu.LAYOUT_SCROLL, maxRows:DwtTimeInput.ROWS});
813     timeSelectButton.setMenu(this._hoursSelectMenu, true, false, false, true);
814     this._menuItemsAdded = false;
815     timeSelectButton.reparentHtmlElement(buttonId);
816 
817     this._tabGroup = new DwtTabGroup(this.getHTMLElId());
818     this._tabGroup.addMember(this._timeSelectInput);
819     this._tabGroup.addMember(timeSelectButton);
820 };
821