Test Failed
Branch master (45c182)
by Julien
04:02
created

resources/assets/js/jquery.timepicker.js   F

Complexity

Total Complexity 317
Complexity/F 6.1

Size

Lines of Code 1195
Function Count 52

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 317
dl 0
loc 1195
rs 2.4
c 1
b 0
f 0
cc 0
nc 0
mnd 4
bc 225
fnc 52
bpm 4.3269
cpm 6.0961
noi 38

How to fix   Complexity   

Complexity

Complex classes like resources/assets/js/jquery.timepicker.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/*!
2
 * jquery-timepicker v1.8.9 - A jQuery timepicker plugin inspired by Google Calendar. It supports both mouse and keyboard navigation.
3
 * Copyright (c) 2015 Jon Thornton - http://jonthornton.github.com/jquery-timepicker/
4
 * License: MIT
5
 */
6
7
8
(function (factory) {
9
    if (typeof exports === "object" && exports &&
10
        typeof module === "object" && module && module.exports === exports) {
11
        // Browserify. Attach to jQuery module.
12
        factory(require("jquery"));
13
    } else if (typeof define === 'function' && define.amd) {
14
		// AMD. Register as an anonymous module.
15
		define(['jquery'], factory);
16
	} else {
17
		// Browser globals
18
		factory(jQuery);
19
	}
20
}(function ($) {
21
	var _baseDate = _generateBaseDate();
22
	var _ONE_DAY = 86400;
23
	var _lang = {
24
		am: 'am',
25
		pm: 'pm',
26
		AM: 'AM',
27
		PM: 'PM',
28
		decimal: '.',
29
		mins: 'mins',
30
		hr: 'hr',
31
		hrs: 'hrs'
32
	};
33
34
	var methods = {
35
		init: function(options)
36
		{
37
			return this.each(function()
38
			{
39
				var self = $(this);
40
41
				// pick up settings from data attributes
42
				var attributeOptions = [];
43
				for (var key in $.fn.timepicker.defaults) {
44
					if (self.data(key))  {
45
						attributeOptions[key] = self.data(key);
46
					}
47
				}
48
49
				var settings = $.extend({}, $.fn.timepicker.defaults, attributeOptions, options);
50
51
				if (settings.lang) {
52
					_lang = $.extend(_lang, settings.lang);
53
				}
54
55
				settings = _parseSettings(settings);
56
				self.data('timepicker-settings', settings);
57
				self.addClass('ui-timepicker-input');
58
59
				if (settings.useSelect) {
60
					_render(self);
61
				} else {
62
					self.prop('autocomplete', 'off');
63
					if (settings.showOn) {
64
						for (var i in settings.showOn) {
65
							self.on(settings.showOn[i]+'.timepicker', methods.show);
66
						}
67
					}
68
					self.on('change.timepicker', _formatValue);
69
					self.on('keydown.timepicker', _keydownhandler);
70
					self.on('keyup.timepicker', _keyuphandler);
71
					if (settings.disableTextInput) {
72
						self.on('keydown.timepicker', function(e) { e.preventDefault(); });
73
					}
74
75
					_formatValue.call(self.get(0));
76
				}
77
			});
78
		},
79
80
		show: function(e)
81
		{
82
			var self = $(this);
83
			var settings = self.data('timepicker-settings');
84
85
			if (e) {
86
				e.preventDefault();
87
			}
88
89
			if (settings.useSelect) {
90
				self.data('timepicker-list').focus();
91
				return;
92
			}
93
94
			if (_hideKeyboard(self)) {
95
				// block the keyboard on mobile devices
96
				self.blur();
97
			}
98
99
			var list = self.data('timepicker-list');
100
101
			// check if input is readonly
102
			if (self.prop('readonly')) {
103
				return;
104
			}
105
106
			// check if list needs to be rendered
107
			if (!list || list.length === 0 || typeof settings.durationTime === 'function') {
108
				_render(self);
109
				list = self.data('timepicker-list');
110
			}
111
112
			if (_isVisible(list)) {
113
				return;
114
			}
115
116
			self.data('ui-timepicker-value', self.val());
117
			_setSelected(self, list);
118
119
			// make sure other pickers are hidden
120
			methods.hide();
121
122
			// position the dropdown relative to the input
123
			list.show();
124
			var listOffset = {};
125
126
			if (settings.orientation.match(/r/)) {
127
				// right-align the dropdown
128
				listOffset.left = self.offset().left + self.outerWidth() - list.outerWidth() + parseInt(list.css('marginLeft').replace('px', ''), 10);
129
			} else {
130
				// left-align the dropdown
131
				listOffset.left = self.offset().left + parseInt(list.css('marginLeft').replace('px', ''), 10);
132
			}
133
134
			var verticalOrientation;
135
			if (settings.orientation.match(/t/)) {
136
				verticalOrientation = 't';
137
			} else if (settings.orientation.match(/b/)) {
138
				verticalOrientation = 'b';
139
			} else if ((self.offset().top + self.outerHeight(true) + list.outerHeight()) > $(window).height() + $(window).scrollTop()) {
140
				verticalOrientation = 't';
141
			} else {
142
				verticalOrientation = 'b';
143
			}
144
145
			if (verticalOrientation == 't') {
146
				// position the dropdown on top
147
				list.addClass('ui-timepicker-positioned-top');
148
				listOffset.top = self.offset().top - list.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10);
149
			} else {
150
				// put it under the input
151
				list.removeClass('ui-timepicker-positioned-top');
152
				listOffset.top = self.offset().top + self.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10);
153
			}
154
155
			list.offset(listOffset);
156
157
			// position scrolling
158
			var selected = list.find('.ui-timepicker-selected');
159
160
			if (!selected.length) {
161
				var timeInt = _time2int(_getTimeValue(self));
162
				if (timeInt !== null) {
163
					selected = _findRow(self, list, timeInt);
164
				} else if (settings.scrollDefault) {
165
					selected = _findRow(self, list, settings.scrollDefault());
166
				}
167
			}
168
169
			if (selected && selected.length) {
170
				var topOffset = list.scrollTop() + selected.position().top - selected.outerHeight();
171
				list.scrollTop(topOffset);
172
			} else {
173
				list.scrollTop(0);
174
			}
175
176
			// prevent scroll propagation
177
			if(settings.stopScrollPropagation) {
178
				$(document).on('wheel.ui-timepicker', '.ui-timepicker-wrapper', function(e){
179
					e.preventDefault();
180
					var currentScroll = $(this).scrollTop();
181
					$(this).scrollTop(currentScroll + e.originalEvent.deltaY);
182
				});
183
			}
184
185
			// attach close handlers
186
			$(document).on('touchstart.ui-timepicker mousedown.ui-timepicker', _closeHandler);
187
			$(window).on('resize.ui-timepicker', _closeHandler);
188
			if (settings.closeOnWindowScroll) {
189
				$(document).on('scroll.ui-timepicker', _closeHandler);
190
			}
191
192
			self.trigger('showTimepicker');
193
194
			return this;
195
		},
196
197
		hide: function(e)
198
		{
199
			var self = $(this);
200
			var settings = self.data('timepicker-settings');
201
202
			if (settings && settings.useSelect) {
203
				self.blur();
204
			}
205
206
			$('.ui-timepicker-wrapper').each(function() {
207
				var list = $(this);
208
				if (!_isVisible(list)) {
209
					return;
210
				}
211
212
				var self = list.data('timepicker-input');
213
				var settings = self.data('timepicker-settings');
214
215
				if (settings && settings.selectOnBlur) {
216
					_selectValue(self);
217
				}
218
219
				list.hide();
220
				self.trigger('hideTimepicker');
221
			});
222
223
			return this;
224
		},
225
226
		option: function(key, value)
227
		{
228
			return this.each(function(){
229
				var self = $(this);
230
				var settings = self.data('timepicker-settings');
231
				var list = self.data('timepicker-list');
232
233
				if (typeof key == 'object') {
234
					settings = $.extend(settings, key);
235
236
				} else if (typeof key == 'string' && typeof value != 'undefined') {
237
					settings[key] = value;
238
239
				} else if (typeof key == 'string') {
240
					return settings[key];
241
				}
242
243
				settings = _parseSettings(settings);
244
245
				self.data('timepicker-settings', settings);
246
247
				if (list) {
248
					list.remove();
249
					self.data('timepicker-list', false);
250
				}
251
252
				if (settings.useSelect) {
253
					_render(self);
254
				}
255
			});
256
		},
257
258
		getSecondsFromMidnight: function()
259
		{
260
			return _time2int(_getTimeValue(this));
261
		},
262
263
		getTime: function(relative_date)
264
		{
265
			var self = this;
266
267
			var time_string = _getTimeValue(self);
268
			if (!time_string) {
269
				return null;
270
			}
271
272
			var offset = _time2int(time_string);
273
			if (offset === null) {
274
				return null;
275
			}
276
277
			if (!relative_date) {
278
				relative_date = _baseDate;
279
			}
280
281
			// construct a Date from relative date, and offset's time
282
			var time = new Date(relative_date);
283
			time.setHours(offset / 3600);
284
			time.setMinutes(offset % 3600 / 60);
285
			time.setSeconds(offset % 60);
286
			time.setMilliseconds(0);
287
288
			return time;
289
		},
290
291
		setTime: function(value)
292
		{
293
			var self = this;
294
			var settings = self.data('timepicker-settings');
295
296
			if (settings.forceRoundTime) {
297
				var prettyTime = _roundAndFormatTime(_time2int(value), settings)
298
			} else {
299
				var prettyTime = _int2time(_time2int(value), settings);
300
			}
301
302
			if (value && prettyTime === null && settings.noneOption) {
303
				prettyTime = value;
304
			}
305
306
			_setTimeValue(self, prettyTime);
307
			if (self.data('timepicker-list')) {
308
				_setSelected(self, self.data('timepicker-list'));
309
			}
310
311
			return this;
312
		},
313
314
		remove: function()
315
		{
316
			var self = this;
317
318
			// check if this element is a timepicker
319
			if (!self.hasClass('ui-timepicker-input')) {
320
				return;
321
			}
322
323
			var settings = self.data('timepicker-settings');
324
			self.removeAttr('autocomplete', 'off');
325
			self.removeClass('ui-timepicker-input');
326
			self.removeData('timepicker-settings');
327
			self.off('.timepicker');
328
329
			// timepicker-list won't be present unless the user has interacted with this timepicker
330
			if (self.data('timepicker-list')) {
331
				self.data('timepicker-list').remove();
332
			}
333
334
			if (settings.useSelect) {
335
				self.show();
336
			}
337
338
			self.removeData('timepicker-list');
339
340
			return this;
341
		}
342
	};
343
344
	// private methods
345
346
	function _isVisible(elem)
347
	{
348
		var el = elem[0];
349
		return el.offsetWidth > 0 && el.offsetHeight > 0;
350
	}
351
352
	function _parseSettings(settings)
353
	{
354
		if (settings.minTime) {
355
			settings.minTime = _time2int(settings.minTime);
356
		}
357
358
		if (settings.maxTime) {
359
			settings.maxTime = _time2int(settings.maxTime);
360
		}
361
362
		if (settings.durationTime && typeof settings.durationTime !== 'function') {
363
			settings.durationTime = _time2int(settings.durationTime);
364
		}
365
366
		if (settings.scrollDefault == 'now') {
367
			settings.scrollDefault = function() {
368
				return settings.roundingFunction(_time2int(new Date()), settings);
369
			}
370
		} else if (settings.scrollDefault && typeof settings.scrollDefault != 'function') {
371
			var val = settings.scrollDefault;
372
			settings.scrollDefault = function() {
373
				return settings.roundingFunction(_time2int(val), settings);
374
			}
375
		} else if (settings.minTime) {
376
			settings.scrollDefault = function() {
377
				return settings.roundingFunction(settings.minTime, settings);
378
			}
379
		}
380
381
		if ($.type(settings.timeFormat) === "string" && settings.timeFormat.match(/[gh]/)) {
382
			settings._twelveHourTime = true;
383
		}
384
385
		if (settings.showOnFocus === false && settings.showOn.indexOf('focus') != -1) {
386
			settings.showOn.splice(settings.showOn.indexOf('focus'), 1);
387
		}
388
389
		if (settings.disableTimeRanges.length > 0) {
390
			// convert string times to integers
391
			for (var i in settings.disableTimeRanges) {
392
				settings.disableTimeRanges[i] = [
393
					_time2int(settings.disableTimeRanges[i][0]),
394
					_time2int(settings.disableTimeRanges[i][1])
395
				];
396
			}
397
398
			// sort by starting time
399
			settings.disableTimeRanges = settings.disableTimeRanges.sort(function(a, b){
400
				return a[0] - b[0];
401
			});
402
403
			// merge any overlapping ranges
404
			for (var i = settings.disableTimeRanges.length-1; i > 0; i--) {
405
				if (settings.disableTimeRanges[i][0] <= settings.disableTimeRanges[i-1][1]) {
406
					settings.disableTimeRanges[i-1] = [
407
						Math.min(settings.disableTimeRanges[i][0], settings.disableTimeRanges[i-1][0]),
408
						Math.max(settings.disableTimeRanges[i][1], settings.disableTimeRanges[i-1][1])
409
					];
410
					settings.disableTimeRanges.splice(i, 1);
411
				}
412
			}
413
		}
414
415
		return settings;
416
	}
417
418
	function _render(self)
419
	{
420
		var settings = self.data('timepicker-settings');
421
		var list = self.data('timepicker-list');
422
423
		if (list && list.length) {
424
			list.remove();
425
			self.data('timepicker-list', false);
426
		}
427
428
		if (settings.useSelect) {
429
			list = $('<select />', { 'class': 'ui-timepicker-select' });
430
			var wrapped_list = list;
431
		} else {
432
			list = $('<ul />', { 'class': 'ui-timepicker-list' });
433
434
			var wrapped_list = $('<div />', { 'class': 'ui-timepicker-wrapper', 'tabindex': -1 });
435
			wrapped_list.css({'display':'none', 'position': 'absolute' }).append(list);
436
		}
437
438
		if (settings.noneOption) {
439
			if (settings.noneOption === true) {
440
				settings.noneOption = (settings.useSelect) ? 'Time...' : 'None';
441
			}
442
443
			if ($.isArray(settings.noneOption)) {
444
				for (var i in settings.noneOption) {
445
					if (parseInt(i, 10) == i){
446
						var noneElement = _generateNoneElement(settings.noneOption[i], settings.useSelect);
447
						list.append(noneElement);
448
					}
449
				}
450
			} else {
451
				var noneElement = _generateNoneElement(settings.noneOption, settings.useSelect);
452
				list.append(noneElement);
453
			}
454
		}
455
456
		if (settings.className) {
457
			wrapped_list.addClass(settings.className);
458
		}
459
460
		if ((settings.minTime !== null || settings.durationTime !== null) && settings.showDuration) {
461
			var stepval = typeof settings.step == 'function' ? 'function' : settings.step;
462
			wrapped_list.addClass('ui-timepicker-with-duration');
463
			wrapped_list.addClass('ui-timepicker-step-'+settings.step);
464
		}
465
466
		var durStart = settings.minTime;
467
		if (typeof settings.durationTime === 'function') {
468
			durStart = _time2int(settings.durationTime());
469
		} else if (settings.durationTime !== null) {
470
			durStart = settings.durationTime;
471
		}
472
		var start = (settings.minTime !== null) ? settings.minTime : 0;
473
		var end = (settings.maxTime !== null) ? settings.maxTime : (start + _ONE_DAY - 1);
474
475
		if (end < start) {
476
			// make sure the end time is greater than start time, otherwise there will be no list to show
477
			end += _ONE_DAY;
478
		}
479
480
		if (end === _ONE_DAY-1 && $.type(settings.timeFormat) === "string" && settings.show2400) {
481
			// show a 24:00 option when using military time
482
			end = _ONE_DAY;
483
		}
484
485
		var dr = settings.disableTimeRanges;
486
		var drCur = 0;
487
		var drLen = dr.length;
488
489
		var stepFunc = settings.step;
490
		if (typeof stepFunc != 'function') {
491
			stepFunc = function() {
492
				return settings.step;
493
			}
494
		}
495
496
		for (var i=start, j=0; i <= end; j++, i += stepFunc(j)*60) {
497
			var timeInt = i;
498
			var timeString = _int2time(timeInt, settings);
499
500
			if (settings.useSelect) {
501
				var row = $('<option />', { 'value': timeString });
502
				row.text(timeString);
503
			} else {
504
				var row = $('<li />');
505
				row.addClass(timeInt % 86400 < 43200 ? 'ui-timepicker-am' : 'ui-timepicker-pm');
506
				row.data('time', (timeInt <= 86400 ? timeInt : timeInt % 86400));
507
				row.text(timeString);
508
			}
509
510
			if ((settings.minTime !== null || settings.durationTime !== null) && settings.showDuration) {
511
				var durationString = _int2duration(i - durStart, settings.step);
512
				if (settings.useSelect) {
513
					row.text(row.text()+' ('+durationString+')');
514
				} else {
515
					var duration = $('<span />', { 'class': 'ui-timepicker-duration' });
516
					duration.text(' ('+durationString+')');
517
					row.append(duration);
518
				}
519
			}
520
521
			if (drCur < drLen) {
522
				if (timeInt >= dr[drCur][1]) {
523
					drCur += 1;
524
				}
525
526
				if (dr[drCur] && timeInt >= dr[drCur][0] && timeInt < dr[drCur][1]) {
527
					if (settings.useSelect) {
528
						row.prop('disabled', true);
529
					} else {
530
						row.addClass('ui-timepicker-disabled');
531
					}
532
				}
533
			}
534
535
			list.append(row);
536
		}
537
538
		wrapped_list.data('timepicker-input', self);
539
		self.data('timepicker-list', wrapped_list);
540
541
		if (settings.useSelect) {
542
			if (self.val()) {
543
				list.val(_roundAndFormatTime(_time2int(self.val()), settings));
544
			}
545
546
			list.on('focus', function(){
547
				$(this).data('timepicker-input').trigger('showTimepicker');
548
			});
549
			list.on('blur', function(){
550
				$(this).data('timepicker-input').trigger('hideTimepicker');
551
			});
552
			list.on('change', function(){
553
				_setTimeValue(self, $(this).val(), 'select');
554
			});
555
556
			_setTimeValue(self, list.val(), 'initial');
557
			self.hide().after(list);
558
		} else {
559
			var appendTo = settings.appendTo;
560
			if (typeof appendTo === 'string') {
561
				appendTo = $(appendTo);
562
			} else if (typeof appendTo === 'function') {
563
				appendTo = appendTo(self);
564
			}
565
			appendTo.append(wrapped_list);
566
			_setSelected(self, list);
567
568
			list.on('mousedown', 'li', function(e) {
569
570
				// hack: temporarily disable the focus handler
571
				// to deal with the fact that IE fires 'focus'
572
				// events asynchronously
573
				self.off('focus.timepicker');
574
				self.on('focus.timepicker-ie-hack', function(){
575
					self.off('focus.timepicker-ie-hack');
576
					self.on('focus.timepicker', methods.show);
577
				});
578
579
				if (!_hideKeyboard(self)) {
580
					self[0].focus();
581
				}
582
583
				// make sure only the clicked row is selected
584
				list.find('li').removeClass('ui-timepicker-selected');
585
				$(this).addClass('ui-timepicker-selected');
586
587
				if (_selectValue(self)) {
588
					self.trigger('hideTimepicker');
589
590
					list.on('mouseup.timepicker', 'li', function(e) {
591
						list.off('mouseup.timepicker');
592
						wrapped_list.hide();
593
					});
594
				}
595
			});
596
		}
597
	}
598
599
	function _generateNoneElement(optionValue, useSelect)
600
	{
601
		var label, className, value;
602
603
		if (typeof optionValue == 'object') {
604
			label = optionValue.label;
605
			className = optionValue.className;
606
			value = optionValue.value;
607
		} else if (typeof optionValue == 'string') {
608
			label = optionValue;
609
		} else {
610
			$.error('Invalid noneOption value');
611
		}
612
613
		if (useSelect) {
614
			return $('<option />', {
615
					'value': value,
616
					'class': className,
617
					'text': label
618
				});
619
		} else {
620
			return $('<li />', {
621
					'class': className,
622
					'text': label
623
				}).data('time', value);
624
		}
625
	}
626
627
	function _roundAndFormatTime(seconds, settings)
628
	{
629
		seconds = settings.roundingFunction(seconds, settings);
630
		if (seconds !== null) {
631
			return _int2time(seconds, settings);
632
		}
633
	}
634
635
	function _generateBaseDate()
636
	{
637
		return new Date(1970, 0, 1, 0, 0, 0);
638
	}
639
640
	// event handler to decide whether to close timepicker
641
	function _closeHandler(e)
642
	{
643
		var target = $(e.target);
644
		var input = target.closest('.ui-timepicker-input');
645
		if (input.length === 0 && target.closest('.ui-timepicker-wrapper').length === 0) {
646
			methods.hide();
647
			$(document).unbind('.ui-timepicker');
648
			$(window).unbind('.ui-timepicker');
649
		}
650
	}
651
652
	function _hideKeyboard(self)
653
	{
654
		var settings = self.data('timepicker-settings');
655
		return ((window.navigator.msMaxTouchPoints || 'ontouchstart' in document) && settings.disableTouchKeyboard);
656
	}
657
658
	function _findRow(self, list, value)
659
	{
660
		if (!value && value !== 0) {
661
			return false;
662
		}
663
664
		var settings = self.data('timepicker-settings');
665
		var out = false;
666
		var value = settings.roundingFunction(value, settings);
667
668
		// loop through the menu items
669
		list.find('li').each(function(i, obj) {
670
			var jObj = $(obj);
671
			if (typeof jObj.data('time') != 'number') {
672
				return;
673
			}
674
675
			if (jObj.data('time') == value) {
676
				out = jObj;
677
				return false;
678
			}
679
		});
680
681
		return out;
682
	}
683
684
	function _setSelected(self, list)
685
	{
686
		list.find('li').removeClass('ui-timepicker-selected');
687
688
		var timeValue = _time2int(_getTimeValue(self), self.data('timepicker-settings'));
689
		if (timeValue === null) {
690
			return;
691
		}
692
693
		var selected = _findRow(self, list, timeValue);
694
		if (selected) {
695
696
			var topDelta = selected.offset().top - list.offset().top;
697
698
			if (topDelta + selected.outerHeight() > list.outerHeight() || topDelta < 0) {
699
				list.scrollTop(list.scrollTop() + selected.position().top - selected.outerHeight());
700
			}
701
702
			selected.addClass('ui-timepicker-selected');
703
		}
704
	}
705
706
707
	function _formatValue(e, origin)
708
	{
709
		if (this.value === '' || origin == 'timepicker') {
710
			return;
711
		}
712
713
		var self = $(this);
714
715
		if (self.is(':focus') && (!e || e.type != 'change')) {
716
			return;
717
		}
718
719
		var settings = self.data('timepicker-settings');
720
		var seconds = _time2int(this.value, settings);
721
722
		if (seconds === null) {
723
			self.trigger('timeFormatError');
724
			return;
725
		}
726
727
		var rangeError = false;
728
		// check that the time in within bounds
729
		if (settings.minTime !== null && seconds < settings.minTime) {
730
			rangeError = true;
731
		} else if (settings.maxTime !== null && seconds > settings.maxTime) {
732
			rangeError = true;
733
		}
734
735
		// check that time isn't within disabled time ranges
736
		$.each(settings.disableTimeRanges, function(){
737
			if (seconds >= this[0] && seconds < this[1]) {
738
				rangeError = true;
739
				return false;
740
			}
741
		});
742
743
		if (settings.forceRoundTime) {
744
			seconds = settings.roundingFunction(seconds, settings);
745
		}
746
747
		var prettyTime = _int2time(seconds, settings);
748
749
		if (rangeError) {
750
			if (_setTimeValue(self, prettyTime, 'error')) {
751
				self.trigger('timeRangeError');
752
			}
753
		} else {
754
			_setTimeValue(self, prettyTime);
755
		}
756
	}
757
758
	function _getTimeValue(self)
759
	{
760
		if (self.is('input')) {
761
			return self.val();
762
		} else {
763
			// use the element's data attributes to store values
764
			return self.data('ui-timepicker-value');
765
		}
766
	}
767
768
	function _setTimeValue(self, value, source)
769
	{
770
		if (self.is('input')) {
771
			self.val(value);
772
773
			var settings = self.data('timepicker-settings');
774
			if (settings.useSelect && source != 'select' && source != 'initial') {
775
				self.data('timepicker-list').val(_roundAndFormatTime(_time2int(value), settings));
776
			}
777
		}
778
779
		if (self.data('ui-timepicker-value') != value) {
780
			self.data('ui-timepicker-value', value);
781
			if (source == 'select') {
782
				self.trigger('selectTime').trigger('changeTime').trigger('change', 'timepicker');
783
			} else if (source != 'error') {
784
				self.trigger('changeTime');
785
			}
786
787
			return true;
788
		} else {
789
			self.trigger('selectTime');
790
			return false;
791
		}
792
	}
793
794
	/*
795
	*  Keyboard navigation via arrow keys
796
	*/
797
	function _keydownhandler(e)
798
	{
799
		var self = $(this);
800
		var list = self.data('timepicker-list');
801
802
		if (!list || !_isVisible(list)) {
803
			if (e.keyCode == 40) {
804
				// show the list!
805
				methods.show.call(self.get(0));
806
				list = self.data('timepicker-list');
807
				if (!_hideKeyboard(self)) {
808
					self.focus();
809
				}
810
			} else {
811
				return true;
812
			}
813
		}
814
815
		switch (e.keyCode) {
816
817
			case 13: // return
818
				if (_selectValue(self)) {
819
					methods.hide.apply(this);
820
				}
821
822
				e.preventDefault();
823
				return false;
824
825
			case 38: // up
826
				var selected = list.find('.ui-timepicker-selected');
827
828
				if (!selected.length) {
829
					list.find('li').each(function(i, obj) {
830
						if ($(obj).position().top > 0) {
831
							selected = $(obj);
832
							return false;
833
						}
834
					});
835
					selected.addClass('ui-timepicker-selected');
836
837
				} else if (!selected.is(':first-child')) {
838
					selected.removeClass('ui-timepicker-selected');
839
					selected.prev().addClass('ui-timepicker-selected');
840
841
					if (selected.prev().position().top < selected.outerHeight()) {
842
						list.scrollTop(list.scrollTop() - selected.outerHeight());
843
					}
844
				}
845
846
				return false;
847
848
			case 40: // down
849
				selected = list.find('.ui-timepicker-selected');
850
851
				if (selected.length === 0) {
852
					list.find('li').each(function(i, obj) {
853
						if ($(obj).position().top > 0) {
854
							selected = $(obj);
855
							return false;
856
						}
857
					});
858
859
					selected.addClass('ui-timepicker-selected');
860
				} else if (!selected.is(':last-child')) {
861
					selected.removeClass('ui-timepicker-selected');
862
					selected.next().addClass('ui-timepicker-selected');
863
864
					if (selected.next().position().top + 2*selected.outerHeight() > list.outerHeight()) {
865
						list.scrollTop(list.scrollTop() + selected.outerHeight());
866
					}
867
				}
868
869
				return false;
870
871
			case 27: // escape
872
				list.find('li').removeClass('ui-timepicker-selected');
873
				methods.hide();
874
				break;
875
876
			case 9: //tab
877
				methods.hide();
878
				break;
879
880
			default:
881
				return true;
882
		}
883
	}
884
885
	/*
886
	*	Time typeahead
887
	*/
888
	function _keyuphandler(e)
889
	{
890
		var self = $(this);
891
		var list = self.data('timepicker-list');
892
		var settings = self.data('timepicker-settings');
893
894
		if (!list || !_isVisible(list) || settings.disableTextInput) {
895
			return true;
896
		}
897
898
		switch (e.keyCode) {
899
900
			case 96: // numpad numerals
901
			case 97:
902
			case 98:
903
			case 99:
904
			case 100:
905
			case 101:
906
			case 102:
907
			case 103:
908
			case 104:
909
			case 105:
910
			case 48: // numerals
911
			case 49:
912
			case 50:
913
			case 51:
914
			case 52:
915
			case 53:
916
			case 54:
917
			case 55:
918
			case 56:
919
			case 57:
920
			case 65: // a
921
			case 77: // m
922
			case 80: // p
923
			case 186: // colon
924
			case 8: // backspace
925
			case 46: // delete
926
				if (settings.typeaheadHighlight) {
927
					_setSelected(self, list);
928
				} else {
929
					list.hide();
930
				}
931
				break;
932
		}
933
	}
934
935
	function _selectValue(self)
936
	{
937
		var settings = self.data('timepicker-settings');
938
		var list = self.data('timepicker-list');
939
		var timeValue = null;
940
941
		var cursor = list.find('.ui-timepicker-selected');
942
943
		if (cursor.hasClass('ui-timepicker-disabled')) {
944
			return false;
945
		}
946
947
		if (cursor.length) {
948
			// selected value found
949
			timeValue = cursor.data('time');
950
		}
951
952
		if (timeValue !== null) {
953
			if (typeof timeValue != 'string') {
954
				timeValue = _int2time(timeValue, settings);
955
			}
956
957
			_setTimeValue(self, timeValue, 'select');
958
		}
959
960
		return true;
961
	}
962
963
	function _int2duration(seconds, step)
964
	{
965
		seconds = Math.abs(seconds);
966
		var minutes = Math.round(seconds/60),
967
			duration = [],
968
			hours, mins;
969
970
		if (minutes < 60) {
971
			// Only show (x mins) under 1 hour
972
			duration = [minutes, _lang.mins];
973
		} else {
974
			hours = Math.floor(minutes/60);
975
			mins = minutes%60;
976
977
			// Show decimal notation (eg: 1.5 hrs) for 30 minute steps
978
			if (step == 30 && mins == 30) {
979
				hours += _lang.decimal + 5;
980
			}
981
982
			duration.push(hours);
983
			duration.push(hours == 1 ? _lang.hr : _lang.hrs);
984
985
			// Show remainder minutes notation (eg: 1 hr 15 mins) for non-30 minute steps
986
			// and only if there are remainder minutes to show
987
			if (step != 30 && mins) {
988
				duration.push(mins);
989
				duration.push(_lang.mins);
990
			}
991
		}
992
993
		return duration.join(' ');
994
	}
995
996
	function _int2time(seconds, settings)
997
	{
998
		if (seconds === null) {
999
			return null;
1000
		}
1001
1002
		var time = new Date(_baseDate.valueOf() + (seconds*1000));
1003
1004
		if (isNaN(time.getTime())) {
1005
			return null;
1006
		}
1007
1008
		if ($.type(settings.timeFormat) === "function") {
1009
			return settings.timeFormat(time);
1010
		}
1011
1012
		var output = '';
1013
		var hour, code;
1014
		for (var i=0; i<settings.timeFormat.length; i++) {
1015
1016
			code = settings.timeFormat.charAt(i);
1017
			switch (code) {
1018
1019
				case 'a':
1020
					output += (time.getHours() > 11) ? _lang.pm : _lang.am;
1021
					break;
1022
1023
				case 'A':
1024
					output += (time.getHours() > 11) ? _lang.PM : _lang.AM;
1025
					break;
1026
1027
				case 'g':
1028
					hour = time.getHours() % 12;
1029
					output += (hour === 0) ? '12' : hour;
1030
					break;
1031
1032
				case 'G':
1033
					hour = time.getHours();
1034
					if (seconds === _ONE_DAY) hour = 24;
1035
					output += hour;
1036
					break;
1037
1038
				case 'h':
1039
					hour = time.getHours() % 12;
1040
1041
					if (hour !== 0 && hour < 10) {
1042
						hour = '0'+hour;
1043
					}
1044
1045
					output += (hour === 0) ? '12' : hour;
1046
					break;
1047
1048
				case 'H':
1049
					hour = time.getHours();
1050
					if (seconds === _ONE_DAY) hour = settings.show2400 ? 24 : 0;
1051
					output += (hour > 9) ? hour : '0'+hour;
1052
					break;
1053
1054
				case 'i':
1055
					var minutes = time.getMinutes();
1056
					output += (minutes > 9) ? minutes : '0'+minutes;
1057
					break;
1058
1059
				case 's':
1060
					seconds = time.getSeconds();
1061
					output += (seconds > 9) ? seconds : '0'+seconds;
1062
					break;
1063
1064
				case '\\':
1065
					// escape character; add the next character and skip ahead
1066
					i++;
1067
					output += settings.timeFormat.charAt(i);
1068
					break;
1069
1070
				default:
1071
					output += code;
1072
			}
1073
		}
1074
1075
		return output;
1076
	}
1077
1078
	function _time2int(timeString, settings)
1079
	{
1080
		if (timeString === '') return null;
1081
		if (!timeString || timeString+0 == timeString) return timeString;
1082
1083
		if (typeof(timeString) == 'object') {
1084
			return timeString.getHours()*3600 + timeString.getMinutes()*60 + timeString.getSeconds();
1085
		}
1086
1087
		timeString = timeString.toLowerCase().replace(/[\s\.]/g, '');
1088
1089
		// if the last character is an "a" or "p", add the "m"
1090
		if (timeString.slice(-1) == 'a' || timeString.slice(-1) == 'p') {
1091
			timeString += 'm';
1092
		}
1093
1094
		var ampmRegex = '(' +
1095
			_lang.am.replace('.', '')+'|' +
1096
			_lang.pm.replace('.', '')+'|' +
1097
			_lang.AM.replace('.', '')+'|' +
1098
			_lang.PM.replace('.', '')+')?';
1099
1100
		// try to parse time input
1101
		var pattern = new RegExp('^'+ampmRegex+'([0-9]?[0-9])\\W?([0-5][0-9])?\\W?([0-5][0-9])?'+ampmRegex+'$');
1102
1103
		var time = timeString.match(pattern);
1104
		if (!time) {
1105
			return null;
1106
		}
1107
1108
		var unboundedHour = parseInt(time[2]*1, 10);
1109
		var hour = (unboundedHour > 24) ? unboundedHour % 24 : unboundedHour;
1110
		var ampm = time[1] || time[5];
1111
		var hours = hour;
1112
1113
		if (hour <= 12 && ampm) {
1114
			var isPm = (ampm == _lang.pm || ampm == _lang.PM);
1115
1116
			if (hour == 12) {
1117
				hours = isPm ? 12 : 0;
1118
			} else {
1119
				hours = (hour + (isPm ? 12 : 0));
1120
			}
1121
		}
1122
1123
		var minutes = ( time[3]*1 || 0 );
1124
		var seconds = ( time[4]*1 || 0 );
1125
		var timeInt = hours*3600 + minutes*60 + seconds;
1126
1127
		// if no am/pm provided, intelligently guess based on the scrollDefault
1128
		if (hour < 12 && !ampm && settings && settings._twelveHourTime && settings.scrollDefault) {
1129
			var delta = timeInt - settings.scrollDefault();
1130
			if (delta < 0 && delta >= _ONE_DAY / -2) {
1131
				timeInt = (timeInt + (_ONE_DAY / 2)) % _ONE_DAY;
1132
			}
1133
		}
1134
1135
		return timeInt;
1136
	}
1137
1138
	function _pad2(n) {
1139
		return ("0" + n).slice(-2);
1140
	}
1141
1142
	// Plugin entry
1143
	$.fn.timepicker = function(method)
1144
	{
1145
		if (!this.length) return this;
1146
		if (methods[method]) {
1147
			// check if this element is a timepicker
1148
			if (!this.hasClass('ui-timepicker-input')) {
1149
				return this;
1150
			}
1151
			return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
1152
		}
1153
		else if(typeof method === "object" || !method) { return methods.init.apply(this, arguments); }
1154
		else { $.error("Method "+ method + " does not exist on jQuery.timepicker"); }
1155
	};
1156
	// Global defaults
1157
	$.fn.timepicker.defaults = {
1158
		appendTo: 'body',
1159
		className: null,
1160
		closeOnWindowScroll: false,
1161
		disableTextInput: false,
1162
		disableTimeRanges: [],
1163
		disableTouchKeyboard: false,
1164
		durationTime: null,
1165
		forceRoundTime: false,
1166
		maxTime: null,
1167
		minTime: null,
1168
		noneOption: false,
1169
		orientation: 'l',
1170
		roundingFunction: function(seconds, settings) {
1171
			if (seconds === null) {
1172
				return null;
1173
			} else if (typeof settings.step !== "number") {
1174
				// TODO: nearest fit irregular steps
1175
				return seconds;
1176
			} else {
1177
				var offset = seconds % (settings.step*60); // step is in minutes
1178
1179
				if (offset >= settings.step*30) {
1180
					// if offset is larger than a half step, round up
1181
					seconds += (settings.step*60) - offset;
1182
				} else {
1183
					// round down
1184
					seconds -= offset;
1185
				}
1186
1187
				return seconds;
1188
			}
1189
		},
1190
		scrollDefault: null,
1191
		selectOnBlur: false,
1192
		show2400: false,
1193
		showDuration: false,
1194
		showOn: ['click', 'focus'],
1195
		showOnFocus: true,
1196
		step: 30,
1197
		stopScrollPropagation: false,
1198
		timeFormat: 'g:ia',
1199
		typeaheadHighlight: true,
1200
		useSelect: false
1201
	};
1202
}));
1203