hjg
2024-07-09 30304784e82d4bba24121328da8eb8490aec4f4f
提交 | 用户 | 时间
58d006 1 /*!
A 2  * Timepicker Component for Twitter Bootstrap
3  *
4  * Copyright 2013 Joris de Wit
5  *
6  * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11 (function($, window, document) {
12   'use strict';
13
14   // TIMEPICKER PUBLIC CLASS DEFINITION
15   var Timepicker = function(element, options) {
16       this.iconUp = options.iconUp || 'fa fa-chevron-up';//ACE
17     this.iconDown = options.iconDown || 'fa fa-chevron-down';//ACE
18
19     this.widget = '';
20     this.$element = $(element);
21     this.defaultTime = options.defaultTime;
22     this.disableFocus = options.disableFocus;
23     this.disableMousewheel = options.disableMousewheel;
24     this.isOpen = options.isOpen;
25     this.minuteStep = options.minuteStep;
26     this.modalBackdrop = options.modalBackdrop;
27     this.orientation = options.orientation;
28     this.secondStep = options.secondStep;
29     this.snapToStep = options.snapToStep;
30     this.showInputs = options.showInputs;
31     this.showMeridian = options.showMeridian;
32     this.showSeconds = options.showSeconds;
33     this.template = options.template;
34     this.appendWidgetTo = options.appendWidgetTo;
35     this.showWidgetOnAddonClick = options.showWidgetOnAddonClick;
36     this.maxHours = options.maxHours;
37         this.explicitMode = options.explicitMode; // If true 123 = 1:23, 12345 = 1:23:45, else invalid.
38
39     this.handleDocumentClick = function (e) {
40       var self = e.data.scope;
41       // This condition was inspired by bootstrap-datepicker.
42       // The element the timepicker is invoked on is the input but it has a sibling for addon/button.
43       if (!(self.$element.parent().find(e.target).length ||
44           self.$widget.is(e.target) ||
45           self.$widget.find(e.target).length)) {
46         self.hideWidget();
47       }
48     };
49     this._init();
50   };
51
52   Timepicker.prototype = {
53
54     constructor: Timepicker,
55     _init: function() {
56       var self = this;
57
58       if (this.showWidgetOnAddonClick && (this.$element.parent().hasClass('input-group') && this.$element.parent().hasClass('bootstrap-timepicker'))) {
59         this.$element.parent('.input-group.bootstrap-timepicker').find('.input-group-addon').on({
60           'click.timepicker': $.proxy(this.showWidget, this)
61         });
62         this.$element.on({
63           'focus.timepicker': $.proxy(this.highlightUnit, this),
64           'click.timepicker': $.proxy(this.highlightUnit, this),
65           'keydown.timepicker': $.proxy(this.elementKeydown, this),
66           'blur.timepicker': $.proxy(this.blurElement, this),
67           'mousewheel.timepicker DOMMouseScroll.timepicker': $.proxy(this.mousewheel, this)
68         });
69       } else {
70         if (this.template) {
71           this.$element.on({
72             'focus.timepicker': $.proxy(this.showWidget, this),
73             'click.timepicker': $.proxy(this.showWidget, this),
74             'blur.timepicker': $.proxy(this.blurElement, this),
75             'mousewheel.timepicker DOMMouseScroll.timepicker': $.proxy(this.mousewheel, this)
76           });
77         } else {
78           this.$element.on({
79             'focus.timepicker': $.proxy(this.highlightUnit, this),
80             'click.timepicker': $.proxy(this.highlightUnit, this),
81             'keydown.timepicker': $.proxy(this.elementKeydown, this),
82             'blur.timepicker': $.proxy(this.blurElement, this),
83             'mousewheel.timepicker DOMMouseScroll.timepicker': $.proxy(this.mousewheel, this)
84           });
85         }
86       }
87
88       if (this.template !== false) {
89         this.$widget = $(this.getTemplate()).on('click', $.proxy(this.widgetClick, this));
90       } else {
91         this.$widget = false;
92       }
93
94       if (this.showInputs && this.$widget !== false) {
95         this.$widget.find('input').each(function() {
96           $(this).on({
97             'click.timepicker': function() { $(this).select(); },
98             'keydown.timepicker': $.proxy(self.widgetKeydown, self),
99             'keyup.timepicker': $.proxy(self.widgetKeyup, self)
100           });
101         });
102       }
103
104       this.setDefaultTime(this.defaultTime);
105     },
106
107     blurElement: function() {
108       this.highlightedUnit = null;
109       this.updateFromElementVal();
110     },
111
112     clear: function() {
113       this.hour = '';
114       this.minute = '';
115       this.second = '';
116       this.meridian = '';
117
118       this.$element.val('');
119     },
120
121     decrementHour: function() {
122       if (this.showMeridian) {
123         if (this.hour === 1) {
124           this.hour = 12;
125         } else if (this.hour === 12) {
126           this.hour--;
127
128           return this.toggleMeridian();
129         } else if (this.hour === 0) {
130           this.hour = 11;
131
132           return this.toggleMeridian();
133         } else {
134           this.hour--;
135         }
136       } else {
137         if (this.hour <= 0) {
138           this.hour = this.maxHours - 1;
139         } else {
140           this.hour--;
141         }
142       }
143     },
144
145     decrementMinute: function(step) {
146       var newVal;
147
148       if (step) {
149         newVal = this.minute - step;
150       } else {
151         newVal = this.minute - this.minuteStep;
152       }
153
154       if (newVal < 0) {
155         this.decrementHour();
156         this.minute = newVal + 60;
157       } else {
158         this.minute = newVal;
159       }
160     },
161
162     decrementSecond: function() {
163       var newVal = this.second - this.secondStep;
164
165       if (newVal < 0) {
166         this.decrementMinute(true);
167         this.second = newVal + 60;
168       } else {
169         this.second = newVal;
170       }
171     },
172
173     elementKeydown: function(e) {
174       switch (e.which) {
175       case 9: //tab
176         if (e.shiftKey) {
177           if (this.highlightedUnit === 'hour') {
178             break;
179           }
180           this.highlightPrevUnit();
181         } else if ((this.showMeridian && this.highlightedUnit === 'meridian') || (this.showSeconds && this.highlightedUnit === 'second') || (!this.showMeridian && !this.showSeconds && this.highlightedUnit ==='minute')) {
182           break;
183         } else {
184           this.highlightNextUnit();
185         }
186         e.preventDefault();
187         this.updateFromElementVal();
188         break;
189       case 27: // escape
190         this.updateFromElementVal();
191         break;
192       case 37: // left arrow
193         e.preventDefault();
194         this.highlightPrevUnit();
195         this.updateFromElementVal();
196         break;
197       case 38: // up arrow
198         e.preventDefault();
199         switch (this.highlightedUnit) {
200         case 'hour':
201           this.incrementHour();
202           this.highlightHour();
203           break;
204         case 'minute':
205           this.incrementMinute();
206           this.highlightMinute();
207           break;
208         case 'second':
209           this.incrementSecond();
210           this.highlightSecond();
211           break;
212         case 'meridian':
213           this.toggleMeridian();
214           this.highlightMeridian();
215           break;
216         }
217         this.update();
218         break;
219       case 39: // right arrow
220         e.preventDefault();
221         this.highlightNextUnit();
222         this.updateFromElementVal();
223         break;
224       case 40: // down arrow
225         e.preventDefault();
226         switch (this.highlightedUnit) {
227         case 'hour':
228           this.decrementHour();
229           this.highlightHour();
230           break;
231         case 'minute':
232           this.decrementMinute();
233           this.highlightMinute();
234           break;
235         case 'second':
236           this.decrementSecond();
237           this.highlightSecond();
238           break;
239         case 'meridian':
240           this.toggleMeridian();
241           this.highlightMeridian();
242           break;
243         }
244
245         this.update();
246         break;
247       }
248     },
249
250     getCursorPosition: function() {
251       var input = this.$element.get(0);
252
253       if ('selectionStart' in input) {// Standard-compliant browsers
254
255         return input.selectionStart;
256       } else if (document.selection) {// IE fix
257         input.focus();
258         var sel = document.selection.createRange(),
259           selLen = document.selection.createRange().text.length;
260
261         sel.moveStart('character', - input.value.length);
262
263         return sel.text.length - selLen;
264       }
265     },
266
267     getTemplate: function() {
268       var template,
269         hourTemplate,
270         minuteTemplate,
271         secondTemplate,
272         meridianTemplate,
273         templateContent;
274
275       if (this.showInputs) {
276         hourTemplate = '<input type="text" class="bootstrap-timepicker-hour" maxlength="2"/>';
277         minuteTemplate = '<input type="text" class="bootstrap-timepicker-minute" maxlength="2"/>';
278         secondTemplate = '<input type="text" class="bootstrap-timepicker-second" maxlength="2"/>';
279         meridianTemplate = '<input type="text" class="bootstrap-timepicker-meridian" maxlength="2"/>';
280       } else {
281         hourTemplate = '<span class="bootstrap-timepicker-hour"></span>';
282         minuteTemplate = '<span class="bootstrap-timepicker-minute"></span>';
283         secondTemplate = '<span class="bootstrap-timepicker-second"></span>';
284         meridianTemplate = '<span class="bootstrap-timepicker-meridian"></span>';
285       }
286
287       templateContent = '<table>'+
288          '<tr>'+
289            //'<td><a href="#" data-action="incrementHour"><span class="glyphicon glyphicon-chevron-up"></span></a></td>'+
290            '<td><a href="#" data-action="incrementHour"><i class="'+this.iconUp+'"></i></a></td>'+//ACE
291            '<td class="separator">&nbsp;</td>'+
292            //'<td><a href="#" data-action="incrementMinute"><span class="glyphicon glyphicon-chevron-up"></span></a></td>'+
293            '<td><a href="#" data-action="incrementMinute"><i class="'+this.iconUp+'"></i></a></td>'+//ACE
294            (this.showSeconds ?
295              '<td class="separator">&nbsp;</td>'+
296              //'<td><a href="#" data-action="incrementSecond"><span class="glyphicon glyphicon-chevron-up"></span></a></td>'
297              '<td><a href="#" data-action="incrementSecond"><i class="'+this.iconUp+'"></i></a></td>'//ACE
298            : '') +
299            (this.showMeridian ?
300              '<td class="separator">&nbsp;</td>'+
301              //'<td class="meridian-column"><a href="#" data-action="toggleMeridian"><span class="glyphicon glyphicon-chevron-up"></span></a></td>'
302              '<td class="meridian-column"><a href="#" data-action="toggleMeridian"><i class="'+this.iconUp+'"></i></a></td>'//ACE
303            : '') +
304          '</tr>'+
305          '<tr>'+
306            '<td>'+ hourTemplate +'</td> '+
307            '<td class="separator">:</td>'+
308            '<td>'+ minuteTemplate +'</td> '+
309            (this.showSeconds ?
310             '<td class="separator">:</td>'+
311             '<td>'+ secondTemplate +'</td>'
312            : '') +
313            (this.showMeridian ?
314             '<td class="separator">&nbsp;</td>'+
315             '<td>'+ meridianTemplate +'</td>'
316            : '') +
317          '</tr>'+
318          '<tr>'+
319             //'<td><a href="#" data-action="decrementHour"><i class="icon-chevron-down"></i></a></td>'+
320            '<td><a href="#" data-action="decrementHour"><i class="'+this.iconDown+'"></i></a></td>'+//ACE
321            '<td class="separator"></td>'+
322            //'<td><a href="#" data-action="decrementMinute"><i class="icon-chevron-down"></i></a></td>'+
323            '<td><a href="#" data-action="decrementMinute"><i class="'+this.iconDown+'"></i></a></td>'+//ACE
324            (this.showSeconds ?
325             '<td class="separator">&nbsp;</td>'+
326             //'<td><a href="#" data-action="decrementSecond"><i class="icon-chevron-down"></i></a></td>'
327             '<td><a href="#" data-action="decrementSecond"><i class="'+this.iconDown+'"></i></a></td>'//ACE
328            : '') +
329            (this.showMeridian ?
330             '<td class="separator">&nbsp;</td>'+
331             //'<td><a href="#" data-action="toggleMeridian"><i class="icon-chevron-down"></i></a></td>'
332             '<td><a href="#" data-action="toggleMeridian"><i class="'+this.iconDown+'"></i></a></td>'//ACE
333            : '') +
334          '</tr>'+
335        '</table>';
336
337       switch(this.template) {
338       case 'modal':
339         template = '<div class="bootstrap-timepicker-widget modal hide fade in" data-backdrop="'+ (this.modalBackdrop ? 'true' : 'false') +'">'+
340           '<div class="modal-header">'+
341             '<a href="#" class="close" data-dismiss="modal">×</a>'+
342             '<h3>Pick a Time</h3>'+
343           '</div>'+
344           '<div class="modal-content">'+
345             templateContent +
346           '</div>'+
347           '<div class="modal-footer">'+
348             '<a href="#" class="btn btn-primary" data-dismiss="modal">OK</a>'+
349           '</div>'+
350         '</div>';
351         break;
352       case 'dropdown':
353         template = '<div class="bootstrap-timepicker-widget dropdown-menu">'+ templateContent +'</div>';
354         break;
355       }
356
357       return template;
358     },
359
360     getTime: function() {
361       if (this.hour === '') {
362         return '';
363       }
364
365       return this.hour + ':' + (this.minute.toString().length === 1 ? '0' + this.minute : this.minute) + (this.showSeconds ? ':' + (this.second.toString().length === 1 ? '0' + this.second : this.second) : '') + (this.showMeridian ? ' ' + this.meridian : '');
366     },
367
368     hideWidget: function() {
369       if (this.isOpen === false) {
370         return;
371       }
372
373       this.$element.trigger({
374         'type': 'hide.timepicker',
375         'time': {
376           'value': this.getTime(),
377           'hours': this.hour,
378           'minutes': this.minute,
379           'seconds': this.second,
380           'meridian': this.meridian
381         }
382       });
383
384       if (this.template === 'modal' && this.$widget.modal) {
385         this.$widget.modal('hide');
386       } else {
387         this.$widget.removeClass('open');
388       }
389
390       $(document).off('mousedown.timepicker, touchend.timepicker', this.handleDocumentClick);
391
392       this.isOpen = false;
393       // show/hide approach taken by datepicker
394       this.$widget.detach();
395     },
396
397     highlightUnit: function() {
398       this.position = this.getCursorPosition();
399       if (this.position >= 0 && this.position <= 2) {
400         this.highlightHour();
401       } else if (this.position >= 3 && this.position <= 5) {
402         this.highlightMinute();
403       } else if (this.position >= 6 && this.position <= 8) {
404         if (this.showSeconds) {
405           this.highlightSecond();
406         } else {
407           this.highlightMeridian();
408         }
409       } else if (this.position >= 9 && this.position <= 11) {
410         this.highlightMeridian();
411       }
412     },
413
414     highlightNextUnit: function() {
415       switch (this.highlightedUnit) {
416       case 'hour':
417         this.highlightMinute();
418         break;
419       case 'minute':
420         if (this.showSeconds) {
421           this.highlightSecond();
422         } else if (this.showMeridian){
423           this.highlightMeridian();
424         } else {
425           this.highlightHour();
426         }
427         break;
428       case 'second':
429         if (this.showMeridian) {
430           this.highlightMeridian();
431         } else {
432           this.highlightHour();
433         }
434         break;
435       case 'meridian':
436         this.highlightHour();
437         break;
438       }
439     },
440
441     highlightPrevUnit: function() {
442       switch (this.highlightedUnit) {
443       case 'hour':
444         if(this.showMeridian){
445           this.highlightMeridian();
446         } else if (this.showSeconds) {
447           this.highlightSecond();
448         } else {
449           this.highlightMinute();
450         }
451         break;
452       case 'minute':
453         this.highlightHour();
454         break;
455       case 'second':
456         this.highlightMinute();
457         break;
458       case 'meridian':
459         if (this.showSeconds) {
460           this.highlightSecond();
461         } else {
462           this.highlightMinute();
463         }
464         break;
465       }
466     },
467
468     highlightHour: function() {
469       var $element = this.$element.get(0),
470           self = this;
471
472       this.highlightedUnit = 'hour';
473
474       if ($element.setSelectionRange) {
475         setTimeout(function() {
476           if (self.hour < 10) {
477             $element.setSelectionRange(0,1);
478           } else {
479             $element.setSelectionRange(0,2);
480           }
481         }, 0);
482       }
483     },
484
485     highlightMinute: function() {
486       var $element = this.$element.get(0),
487           self = this;
488
489       this.highlightedUnit = 'minute';
490
491       if ($element.setSelectionRange) {
492         setTimeout(function() {
493           if (self.hour < 10) {
494             $element.setSelectionRange(2,4);
495           } else {
496             $element.setSelectionRange(3,5);
497           }
498         }, 0);
499       }
500     },
501
502     highlightSecond: function() {
503       var $element = this.$element.get(0),
504           self = this;
505
506       this.highlightedUnit = 'second';
507
508       if ($element.setSelectionRange) {
509         setTimeout(function() {
510           if (self.hour < 10) {
511             $element.setSelectionRange(5,7);
512           } else {
513             $element.setSelectionRange(6,8);
514           }
515         }, 0);
516       }
517     },
518
519     highlightMeridian: function() {
520       var $element = this.$element.get(0),
521           self = this;
522
523       this.highlightedUnit = 'meridian';
524
525       if ($element.setSelectionRange) {
526         if (this.showSeconds) {
527           setTimeout(function() {
528             if (self.hour < 10) {
529               $element.setSelectionRange(8,10);
530             } else {
531               $element.setSelectionRange(9,11);
532             }
533           }, 0);
534         } else {
535           setTimeout(function() {
536             if (self.hour < 10) {
537               $element.setSelectionRange(5,7);
538             } else {
539               $element.setSelectionRange(6,8);
540             }
541           }, 0);
542         }
543       }
544     },
545
546     incrementHour: function() {
547       if (this.showMeridian) {
548         if (this.hour === 11) {
549           this.hour++;
550           return this.toggleMeridian();
551         } else if (this.hour === 12) {
552           this.hour = 0;
553         }
554       }
555       if (this.hour === this.maxHours - 1) {
556         this.hour = 0;
557
558         return;
559       }
560       this.hour++;
561     },
562
563     incrementMinute: function(step) {
564       var newVal;
565
566       if (step) {
567         newVal = this.minute + step;
568       } else {
569         newVal = this.minute + this.minuteStep - (this.minute % this.minuteStep);
570       }
571
572       if (newVal > 59) {
573         this.incrementHour();
574         this.minute = newVal - 60;
575       } else {
576         this.minute = newVal;
577       }
578     },
579
580     incrementSecond: function() {
581       var newVal = this.second + this.secondStep - (this.second % this.secondStep);
582
583       if (newVal > 59) {
584         this.incrementMinute(true);
585         this.second = newVal - 60;
586       } else {
587         this.second = newVal;
588       }
589     },
590
591     mousewheel: function(e) {
592       if (this.disableMousewheel) {
593         return;
594       }
595
596       e.preventDefault();
597       e.stopPropagation();
598
599       var delta = e.originalEvent.wheelDelta || -e.originalEvent.detail,
600           scrollTo = null;
601
602       if (e.type === 'mousewheel') {
603         scrollTo = (e.originalEvent.wheelDelta * -1);
604       }
605       else if (e.type === 'DOMMouseScroll') {
606         scrollTo = 40 * e.originalEvent.detail;
607       }
608
609       if (scrollTo) {
610         e.preventDefault();
611         $(this).scrollTop(scrollTo + $(this).scrollTop());
612       }
613
614       switch (this.highlightedUnit) {
615       case 'minute':
616         if (delta > 0) {
617           this.incrementMinute();
618         } else {
619           this.decrementMinute();
620         }
621         this.highlightMinute();
622         break;
623       case 'second':
624         if (delta > 0) {
625           this.incrementSecond();
626         } else {
627           this.decrementSecond();
628         }
629         this.highlightSecond();
630         break;
631       case 'meridian':
632         this.toggleMeridian();
633         this.highlightMeridian();
634         break;
635       default:
636         if (delta > 0) {
637           this.incrementHour();
638         } else {
639           this.decrementHour();
640         }
641         this.highlightHour();
642         break;
643       }
644
645       return false;
646     },
647
648     /**
649      * Given a segment value like 43, will round and snap the segment
650      * to the nearest "step", like 45 if step is 15. Segment will
651      * "overflow" to 0 if it's larger than 59 or would otherwise
652      * round up to 60.
653      */
654     changeToNearestStep: function (segment, step) {
655       if (segment % step === 0) {
656         return segment;
657       }
658       if (Math.round((segment % step) / step)) {
659         return (segment + (step - segment % step)) % 60;
660       } else {
661         return segment - segment % step;
662       }
663     },
664
665     // This method was adapted from bootstrap-datepicker.
666     place : function() {
667       if (this.isInline) {
668         return;
669       }
670       var widgetWidth = this.$widget.outerWidth(), widgetHeight = this.$widget.outerHeight(), visualPadding = 10, windowWidth =
671         $(window).width(), windowHeight = $(window).height(), scrollTop = $(window).scrollTop();
672
673       var zIndex = parseInt(this.$element.parents().filter(function() { return $(this).css('z-index') !== 'auto'; }).first().css('z-index'), 10) + 10;
674       var offset = this.component ? this.component.parent().offset() : this.$element.offset();
675       var height = this.component ? this.component.outerHeight(true) : this.$element.outerHeight(false);
676       var width = this.component ? this.component.outerWidth(true) : this.$element.outerWidth(false);
677       var left = offset.left, top = offset.top;
678
679       this.$widget.removeClass('timepicker-orient-top timepicker-orient-bottom timepicker-orient-right timepicker-orient-left');
680
681       if (this.orientation.x !== 'auto') {
682         this.picker.addClass('datepicker-orient-' + this.orientation.x);
683         if (this.orientation.x === 'right') {
684           left -= widgetWidth - width;
685         }
686       } else{
687         // auto x orientation is best-placement: if it crosses a window edge, fudge it sideways
688         // Default to left
689         this.$widget.addClass('timepicker-orient-left');
690         if (offset.left < 0) {
691           left -= offset.left - visualPadding;
692         } else if (offset.left + widgetWidth > windowWidth) {
693           left = windowWidth - widgetWidth - visualPadding;
694         }
695       }
696       // auto y orientation is best-situation: top or bottom, no fudging, decision based on which shows more of the widget
697       var yorient = this.orientation.y, topOverflow, bottomOverflow;
698       if (yorient === 'auto') {
699         topOverflow = -scrollTop + offset.top - widgetHeight;
700         bottomOverflow = scrollTop + windowHeight - (offset.top + height + widgetHeight);
701         if (Math.max(topOverflow, bottomOverflow) === bottomOverflow) {
702           yorient = 'top';
703         } else {
704           yorient = 'bottom';
705         }
706       }
707       this.$widget.addClass('timepicker-orient-' + yorient);
708       if (yorient === 'top'){
709         top += height;
710       } else{
711         top -= widgetHeight + parseInt(this.$widget.css('padding-top'), 10);
712       }
713
714       this.$widget.css({
715         top : top,
716         left : left,
717         zIndex : zIndex
718       });
719     },
720
721     remove: function() {
722       $('document').off('.timepicker');
723       if (this.$widget) {
724         this.$widget.remove();
725       }
726       delete this.$element.data().timepicker;
727     },
728
729     setDefaultTime: function(defaultTime) {
730       if (!this.$element.val()) {
731         if (defaultTime === 'current') {
732           var dTime = new Date(),
733             hours = dTime.getHours(),
734             minutes = dTime.getMinutes(),
735             seconds = dTime.getSeconds(),
736             meridian = 'AM';
737
738           if (seconds !== 0) {
739             seconds = Math.ceil(dTime.getSeconds() / this.secondStep) * this.secondStep;
740             if (seconds === 60) {
741               minutes += 1;
742               seconds = 0;
743             }
744           }
745
746           if (minutes !== 0) {
747             minutes = Math.ceil(dTime.getMinutes() / this.minuteStep) * this.minuteStep;
748             if (minutes === 60) {
749               hours += 1;
750               minutes = 0;
751             }
752           }
753
754           if (this.showMeridian) {
755             if (hours === 0) {
756               hours = 12;
757             } else if (hours >= 12) {
758               if (hours > 12) {
759                 hours = hours - 12;
760               }
761               meridian = 'PM';
762             } else {
763               meridian = 'AM';
764             }
765           }
766
767           this.hour = hours;
768           this.minute = minutes;
769           this.second = seconds;
770           this.meridian = meridian;
771
772           this.update();
773
774         } else if (defaultTime === false) {
775           this.hour = 0;
776           this.minute = 0;
777           this.second = 0;
778           this.meridian = 'AM';
779         } else {
780           this.setTime(defaultTime);
781         }
782       } else {
783         this.updateFromElementVal();
784       }
785     },
786
787     setTime: function(time, ignoreWidget) {
788       if (!time) {
789         this.clear();
790         return;
791       }
792
793       var timeMode,
794           timeArray,
795           hour,
796           minute,
797           second,
798           meridian;
799
800       if (typeof time === 'object' && time.getMonth){
801         // this is a date object
802         hour    = time.getHours();
803         minute  = time.getMinutes();
804         second  = time.getSeconds();
805
806         if (this.showMeridian){
807           meridian = 'AM';
808           if (hour > 12){
809             meridian = 'PM';
810             hour = hour % 12;
811           }
812
813           if (hour === 12){
814             meridian = 'PM';
815           }
816         }
817       } else {
818         timeMode = ((/a/i).test(time) ? 1 : 0) + ((/p/i).test(time) ? 2 : 0); // 0 = none, 1 = AM, 2 = PM, 3 = BOTH.
819         if (timeMode > 2) { // If both are present, fail.
820           this.clear();
821           return;
822         }
823
824         timeArray = time.replace(/[^0-9\:]/g, '').split(':');
825
826         hour = timeArray[0] ? timeArray[0].toString() : timeArray.toString();
827
828         if(this.explicitMode && hour.length > 2 && (hour.length % 2) !== 0 ) {
829           this.clear();
830           return;
831         }
832
833         minute = timeArray[1] ? timeArray[1].toString() : '';
834         second = timeArray[2] ? timeArray[2].toString() : '';
835
836         // adaptive time parsing
837         if (hour.length > 4) {
838           second = hour.slice(-2);
839           hour = hour.slice(0, -2);
840         }
841
842         if (hour.length > 2) {
843           minute = hour.slice(-2);
844           hour = hour.slice(0, -2);
845         }
846
847         if (minute.length > 2) {
848           second = minute.slice(-2);
849           minute = minute.slice(0, -2);
850         }
851
852         hour = parseInt(hour, 10);
853         minute = parseInt(minute, 10);
854         second = parseInt(second, 10);
855
856         if (isNaN(hour)) {
857           hour = 0;
858         }
859         if (isNaN(minute)) {
860           minute = 0;
861         }
862         if (isNaN(second)) {
863           second = 0;
864         }
865
866         // Adjust the time based upon unit boundary.
867         // NOTE: Negatives will never occur due to time.replace() above.
868         if (second > 59) {
869           second = 59;
870         }
871
872         if (minute > 59) {
873           minute = 59;
874         }
875
876         if (hour >= this.maxHours) {
877           // No day/date handling.
878           hour = this.maxHours - 1;
879         }
880
881         if (this.showMeridian) {
882           if (hour > 12) {
883             // Force PM.
884             timeMode = 2;
885             hour -= 12;
886           }
887           if (!timeMode) {
888             timeMode = 1;
889           }
890           if (hour === 0) {
891             hour = 12; // AM or PM, reset to 12.  0 AM = 12 AM.  0 PM = 12 PM, etc.
892           }
893           meridian = timeMode === 1 ? 'AM' : 'PM';
894         } else if (hour < 12 && timeMode === 2) {
895           hour += 12;
896         } else {
897           if (hour >= this.maxHours) {
898             hour = this.maxHours - 1;
899           } else if (hour < 0) {
900             hour = 0;
901           }
902         }
903       }
904
905       this.hour = hour;
906       if (this.snapToStep) {
907         this.minute = this.changeToNearestStep(minute, this.minuteStep);
908         this.second = this.changeToNearestStep(second, this.secondStep);
909       } else {
910         this.minute = minute;
911         this.second = second;
912       }
913       this.meridian = meridian;
914
915       this.update(ignoreWidget);
916     },
917
918     showWidget: function() {
919       if (this.isOpen) {
920         return;
921       }
922
923       if (this.$element.is(':disabled')) {
924         return;
925       }
926
927       // show/hide approach taken by datepicker
928       this.$widget.appendTo(this.appendWidgetTo);
929       $(document).on('mousedown.timepicker, touchend.timepicker', {scope: this}, this.handleDocumentClick);
930
931       this.$element.trigger({
932         'type': 'show.timepicker',
933         'time': {
934           'value': this.getTime(),
935           'hours': this.hour,
936           'minutes': this.minute,
937           'seconds': this.second,
938           'meridian': this.meridian
939         }
940       });
941
942       this.place();
943       if (this.disableFocus) {
944         this.$element.blur();
945       }
946
947       // widget shouldn't be empty on open
948       if (this.hour === '') {
949         if (this.defaultTime) {
950           this.setDefaultTime(this.defaultTime);
951         } else {
952           this.setTime('0:0:0');
953         }
954       }
955
956       if (this.template === 'modal' && this.$widget.modal) {
957         this.$widget.modal('show').on('hidden', $.proxy(this.hideWidget, this));
958       } else {
959         if (this.isOpen === false) {
960           this.$widget.addClass('open');
961         }
962       }
963
964       this.isOpen = true;
965     },
966
967     toggleMeridian: function() {
968       this.meridian = this.meridian === 'AM' ? 'PM' : 'AM';
969     },
970
971     update: function(ignoreWidget) {
972       this.updateElement();
973       if (!ignoreWidget) {
974         this.updateWidget();
975       }
976
977       this.$element.trigger({
978         'type': 'changeTime.timepicker',
979         'time': {
980           'value': this.getTime(),
981           'hours': this.hour,
982           'minutes': this.minute,
983           'seconds': this.second,
984           'meridian': this.meridian
985         }
986       });
987     },
988
989     updateElement: function() {
990       this.$element.val(this.getTime()).change();
991     },
992
993     updateFromElementVal: function() {
994       this.setTime(this.$element.val());
995     },
996
997     updateWidget: function() {
998       if (this.$widget === false) {
999         return;
1000       }
1001
1002       var hour = this.hour,
1003           minute = this.minute.toString().length === 1 ? '0' + this.minute : this.minute,
1004           second = this.second.toString().length === 1 ? '0' + this.second : this.second;
1005
1006       if (this.showInputs) {
1007         this.$widget.find('input.bootstrap-timepicker-hour').val(hour);
1008         this.$widget.find('input.bootstrap-timepicker-minute').val(minute);
1009
1010         if (this.showSeconds) {
1011           this.$widget.find('input.bootstrap-timepicker-second').val(second);
1012         }
1013         if (this.showMeridian) {
1014           this.$widget.find('input.bootstrap-timepicker-meridian').val(this.meridian);
1015         }
1016       } else {
1017         this.$widget.find('span.bootstrap-timepicker-hour').text(hour);
1018         this.$widget.find('span.bootstrap-timepicker-minute').text(minute);
1019
1020         if (this.showSeconds) {
1021           this.$widget.find('span.bootstrap-timepicker-second').text(second);
1022         }
1023         if (this.showMeridian) {
1024           this.$widget.find('span.bootstrap-timepicker-meridian').text(this.meridian);
1025         }
1026       }
1027     },
1028
1029     updateFromWidgetInputs: function() {
1030       if (this.$widget === false) {
1031         return;
1032       }
1033
1034       var t = this.$widget.find('input.bootstrap-timepicker-hour').val() + ':' +
1035               this.$widget.find('input.bootstrap-timepicker-minute').val() +
1036               (this.showSeconds ? ':' + this.$widget.find('input.bootstrap-timepicker-second').val() : '') +
1037               (this.showMeridian ? this.$widget.find('input.bootstrap-timepicker-meridian').val() : '')
1038       ;
1039
1040       this.setTime(t, true);
1041     },
1042
1043     widgetClick: function(e) {
1044       e.stopPropagation();
1045       e.preventDefault();
1046
1047       var $input = $(e.target),
1048           action = $input.closest('a').data('action');
1049
1050       if (action) {
1051         this[action]();
1052       }
1053       this.update();
1054
1055       if ($input.is('input')) {
1056         $input.get(0).setSelectionRange(0,2);
1057       }
1058     },
1059
1060     widgetKeydown: function(e) {
1061       var $input = $(e.target),
1062           name = $input.attr('class').replace('bootstrap-timepicker-', '');
1063
1064       switch (e.which) {
1065       case 9: //tab
1066         if (e.shiftKey) {
1067           if (name === 'hour') {
1068             return this.hideWidget();
1069           }
1070         } else if ((this.showMeridian && name === 'meridian') || (this.showSeconds && name === 'second') || (!this.showMeridian && !this.showSeconds && name === 'minute')) {
1071           return this.hideWidget();
1072         }
1073         break;
1074       case 27: // escape
1075         this.hideWidget();
1076         break;
1077       case 38: // up arrow
1078         e.preventDefault();
1079         switch (name) {
1080         case 'hour':
1081           this.incrementHour();
1082           break;
1083         case 'minute':
1084           this.incrementMinute();
1085           break;
1086         case 'second':
1087           this.incrementSecond();
1088           break;
1089         case 'meridian':
1090           this.toggleMeridian();
1091           break;
1092         }
1093         this.setTime(this.getTime());
1094         $input.get(0).setSelectionRange(0,2);
1095         break;
1096       case 40: // down arrow
1097         e.preventDefault();
1098         switch (name) {
1099         case 'hour':
1100           this.decrementHour();
1101           break;
1102         case 'minute':
1103           this.decrementMinute();
1104           break;
1105         case 'second':
1106           this.decrementSecond();
1107           break;
1108         case 'meridian':
1109           this.toggleMeridian();
1110           break;
1111         }
1112         this.setTime(this.getTime());
1113         $input.get(0).setSelectionRange(0,2);
1114         break;
1115       }
1116     },
1117
1118     widgetKeyup: function(e) {
1119       if ((e.which === 65) || (e.which === 77) || (e.which === 80) || (e.which === 46) || (e.which === 8) || (e.which >= 48 && e.which <= 57) || (e.which >= 96 && e.which <= 105)) {
1120         this.updateFromWidgetInputs();
1121       }
1122     }
1123   };
1124
1125   //TIMEPICKER PLUGIN DEFINITION
1126   $.fn.timepicker = function(option) {
1127     var args = Array.apply(null, arguments);
1128     args.shift();
1129     return this.each(function() {
1130       var $this = $(this),
1131         data = $this.data('timepicker'),
1132         options = typeof option === 'object' && option;
1133
1134       if (!data) {
1135         $this.data('timepicker', (data = new Timepicker(this, $.extend({}, $.fn.timepicker.defaults, options, $(this).data()))));
1136       }
1137
1138       if (typeof option === 'string') {
1139         data[option].apply(data, args);
1140       }
1141     });
1142   };
1143
1144   $.fn.timepicker.defaults = {
1145     defaultTime: 'current',
1146     disableFocus: false,
1147     disableMousewheel: false,
1148     isOpen: false,
1149     minuteStep: 15,
1150     modalBackdrop: false,
1151     orientation: { x: 'auto', y: 'auto'},
1152     secondStep: 15,
1153     snapToStep: false,
1154     showSeconds: false,
1155     showInputs: true,
1156     showMeridian: true,
1157     template: 'dropdown',
1158     appendWidgetTo: 'body',
1159     showWidgetOnAddonClick: true,
1160     maxHours: 24,
1161     explicitMode: false
1162   };
1163
1164   $.fn.timepicker.Constructor = Timepicker;
1165
1166   $(document).on(
1167     'focus.timepicker.data-api click.timepicker.data-api',
1168     '[data-provide="timepicker"]',
1169     function(e){
1170       var $this = $(this);
1171       if ($this.data('timepicker')) {
1172         return;
1173       }
1174       e.preventDefault();
1175       // component click requires us to explicitly show it
1176       $this.timepicker();
1177     }
1178   );
1179
1180 })(jQuery, window, document);