diff --git a/.gitignore b/.gitignore index b512c09..323128d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -node_modules \ No newline at end of file +node_modules +bower_components +.idea +*.iml diff --git a/README.md b/README.md index 5ff911a..deb9add 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,17 @@ if (something) { // Manual operations (after clockpicker is initialized). $('#demo-input').clockpicker('show') // Or hide, remove ... .clockpicker('toggleView', 'minutes'); + + // Get the time of single clockpicker + var dateObject = $('#demo-input').clockpicker('getTime'); + console.log(dateObject); + + // Get the time of a clockpicker list + $('.clockpicker').clockpicker('getTime', function(dateObject) { + // The clockpicker element + console.log(this); + console.log(dateObject); + }); } ``` @@ -69,14 +80,18 @@ if (something) { | Name | Default | Description | | ---- | ------- | ----------- | -| default | '' | default time, 'now' or '13:14' e.g. | -| placement | 'bottom' | popover placement | +| default | '' | default time, 'now', Date object or '13:14' e.g. | +| placement | 'bottom' | popover placement. Supported values: 'top', 'bottom', 'left', 'right', 'top-adaptive', 'bottom-adaptive'. | | align | 'left' | popover arrow align | | donetext | '完成' | done button text | | autoclose | false | auto close when minute is selected | | twelvehour | false | enables twelve hour mode with AM & PM buttons | | vibrate | true | vibrate the device when dragging clock hand | -| fromnow | 0 | set default time to * milliseconds from now (using with default = 'now') | +| fromnow | 0 | set default time to * milliseconds from now (using with default = 'now' or default = Date) | +| hourstep | 1 | allow to multi increment the hour | +| minutestep | 1 | allow to multi increment the minute | +| ampmSubmit | false | allow submit with AM and PM buttons instead of the minute selection/picker | +| addonOnly | false | disables the focus and click triggers on the input field (to allow delegation to native pickers) | | init | | callback function triggered after the colorpicker has been initiated | | beforeShow | | callback function triggered before popup is shown | | afterShow | | callback function triggered after popup is shown | @@ -95,6 +110,7 @@ if (something) { | hide | | hide the clockpicker | | remove | | remove the clockpicker (and event listeners) | | toggleView | 'hours' or 'minutes' | toggle to hours or minutes view | +| getTime | Optional callback (Can be used for list of elements) | Returns Date object. (Will call the callback if callback is given) | ## What's included @@ -135,6 +151,13 @@ gulp ## Change log +0.1.0 + +* Able to set incremental step for hours and minutes +* Added getTime +* Able to set the default value via Date object +* Some AM & PM bug fixes + 0.0.7 * Enables twelve hour mode with AM & PM buttons. diff --git a/am_pm_submit.html b/am_pm_submit.html new file mode 100644 index 0000000..1adabcb --- /dev/null +++ b/am_pm_submit.html @@ -0,0 +1,38 @@ + + + + + + +ClockPicker + + + + + + + + + + + + + + + + + + diff --git a/dist/bootstrap-clockpicker.js b/dist/bootstrap-clockpicker.js index e930b4f..51c9530 100644 --- a/dist/bootstrap-clockpicker.js +++ b/dist/bootstrap-clockpicker.js @@ -95,6 +95,7 @@ amPmBlock = popover.find('.clockpicker-am-pm-block'), isInput = element.prop('tagName') === 'INPUT', input = isInput ? element : element.find('input'), + isHTML5 = input.prop('type') === 'time', addon = element.find('.input-group-addon'), self = this, timer; @@ -102,10 +103,13 @@ this.id = uniqueId('cp'); this.element = element; this.options = options; + this.options.hourstep = this.parseStep(this.options.hourstep, 12); + this.options.minutestep = this.parseStep(this.options.minutestep, 60); this.isAppended = false; this.isShown = false; this.currentView = 'hours'; this.isInput = isInput; + this.isHTML5 = isHTML5; this.input = input; this.addon = addon; this.popover = popover; @@ -116,38 +120,25 @@ this.spanHours = popover.find('.clockpicker-span-hours'); this.spanMinutes = popover.find('.clockpicker-span-minutes'); this.spanAmPm = popover.find('.clockpicker-span-am-pm'); - this.amOrPm = "PM"; - + this.amOrPm = ""; + this.currentPlacementClass = options.placement; + this.raiseCallback = function() { + raiseCallback.apply(self, arguments); + }; + // Setup for for 12 hour clock if option is selected if (options.twelvehour) { - - var amPmButtonsTemplate = ['
', - '', - '', - '
'].join(''); - - var amPmButtons = $(amPmButtonsTemplate); - //amPmButtons.appendTo(plate); - - ////Not working b/c they are not shown when this runs - //$('clockpicker-am-button') - // .on("click", function() { - // self.amOrPm = "AM"; - // $('.clockpicker-span-am-pm').empty().append('AM'); - // }); - // - //$('clockpicker-pm-button') - // .on("click", function() { - // self.amOrPm = "PM"; - // $('.clockpicker-span-am-pm').empty().append('PM'); - // }); - + $('') .on("click", function() { self.amOrPm = "AM"; $('.clockpicker-span-am-pm').empty().append('AM'); + + if (options.ampmSubmit) { + setTimeout(function(){ + self.done(); + }, duration / 2); + } }).appendTo(this.amPmBlock); @@ -155,6 +146,12 @@ .on("click", function() { self.amOrPm = 'PM'; $('.clockpicker-span-am-pm').empty().append('PM'); + + if (options.ampmSubmit) { + setTimeout(function(){ + self.done(); + }, duration / 2); + } }).appendTo(this.amPmBlock); } @@ -167,7 +164,7 @@ } // Placement and arrow align - make sure they make sense. - if ((options.placement === 'top' || options.placement === 'bottom') && (options.align === 'top' || options.align === 'bottom')) options.align = 'left'; + if (/^(top|bottom)/.test(options.placement) && (options.align === 'top' || options.align === 'bottom')) options.align = 'left'; if ((options.placement === 'left' || options.placement === 'right') && (options.align === 'left' || options.align === 'right')) options.align = 'top'; popover.addClass(options.placement); @@ -177,7 +174,9 @@ this.spanMinutes.click($.proxy(this.toggleView, this, 'minutes')); // Show or toggle - input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this)); + if (!options.addonOnly) { + input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this)); + } addon.on('click.clockpicker', $.proxy(this.toggle, this)); // Build ticks @@ -186,7 +185,7 @@ // Hours view if (options.twelvehour) { - for (i = 1; i < 13; i += 1) { + for (i = 0; i < 12; i += options.hourstep) { tick = tickTpl.clone(); radian = i / 6 * Math.PI; radius = outerRadius; @@ -195,12 +194,12 @@ left: dialRadius + Math.sin(radian) * radius - tickRadius, top: dialRadius - Math.cos(radian) * radius - tickRadius }); - tick.html(i === 0 ? '00' : i); + tick.html(i === 0 ? 12 : i); hoursView.append(tick); tick.on(mousedownEvent, mousedown); } } else { - for (i = 0; i < 24; i += 1) { + for (i = 0; i < 24; i += options.hourstep) { tick = tickTpl.clone(); radian = i / 6 * Math.PI; var inner = i > 0 && i < 13; @@ -219,7 +218,8 @@ } // Minutes view - for (i = 0; i < 60; i += 5) { + var incrementValue = Math.max(options.minutestep, 5); + for (i = 0; i < 60; i += incrementValue) { tick = tickTpl.clone(); radian = i / 30 * Math.PI; tick.css({ @@ -267,7 +267,7 @@ } // Clock - self.setHand(dx, dy, ! space, true); + self.setHand(dx, dy, true); // Mousemove on document $doc.off(mousemoveEvent).on(mousemoveEvent, function(e){ @@ -280,7 +280,7 @@ return; } moved = true; - self.setHand(x, y, false, true); + self.setHand(x, y, true); }); // Mouseup on document @@ -297,10 +297,12 @@ self.toggleView('minutes', duration / 2); } else { if (options.autoclose) { - self.minutesView.addClass('clockpicker-dial-out'); - setTimeout(function(){ - self.done(); - }, duration / 2); + if (!options.ampmSubmit) { + self.minutesView.addClass('clockpicker-dial-out'); + setTimeout(function(){ + self.done(); + }, duration / 2); + } } } plate.prepend(canvas); @@ -352,25 +354,75 @@ this.canvas = canvas; } - raiseCallback(this.options.init); + this.raiseCallback(this.options.init, 'init'); } - function raiseCallback(callbackFunction) { - if (callbackFunction && typeof callbackFunction === "function") { - callbackFunction(); + function raiseCallback(callbackFunction, triggerName) { + if (callbackFunction && typeof callbackFunction === "function" && this.element) { + var time = this.getTime() || null; + callbackFunction.call(this.element, time); + } + if (triggerName) { + this.element.trigger('clockpicker.' + triggerName || 'NoName'); + } + } + + /** + * Find most suitable vertical placement, doing our best to ensure it is inside of the viewport. + * + * First try to place the element according with preferredPlacement, then try the opposite + * placement and as a last resort, popover will be placed on the very top of the viewport. + * + * @param {jQuery} element + * @param {jQuery} popover + * @param preferredPlacement Preferred placement, if there is enough room for it. + * @returns {string} One of: 'top', 'bottom' or 'viewport-top'. + */ + function resolveAdaptiveVerticalPlacement(element, popover, preferredPlacement) { + var popoverHeight = popover.outerHeight(), + elementHeight = element.outerHeight(), + elementTopOffset = element.offset().top, + elementBottomOffset = element.offset().top + elementHeight, + minVisibleY = elementTopOffset - element[0].getBoundingClientRect().top, + maxVisibleY = minVisibleY + document.documentElement.clientHeight, + isEnoughRoomAbove = (elementTopOffset - popoverHeight) >= minVisibleY, + isEnoughRoomBelow = (elementBottomOffset + popoverHeight) <= maxVisibleY; + + if (preferredPlacement === 'top') { + if (isEnoughRoomAbove) { + return 'top'; + } else if (isEnoughRoomBelow) { + return 'bottom'; + } + } else { + if (isEnoughRoomBelow) { + return 'bottom'; + } else if (isEnoughRoomAbove) { + return 'top'; + } } + + return 'viewport-top'; + } + + ClockPicker.prototype.parseStep = function(givenStepSize, wholeSize) { + return wholeSize % givenStepSize === 0 ? givenStepSize : 1; } // Default options ClockPicker.DEFAULTS = { - 'default': '', // default time, 'now' or '13:14' e.g. - fromnow: 0, // set default time to * milliseconds from now (using with default = 'now') - placement: 'bottom', // clock popover placement - align: 'left', // popover arrow align - donetext: '完成', // done button text - autoclose: false, // auto close when minute is selected - twelvehour: false, // change to 12 hour AM/PM clock from 24 hour - vibrate: true // vibrate the device when dragging clock hand + 'default': '', // default time, 'now' or '13:14' e.g. + fromnow: 0, // set default time to * milliseconds from now (using with default = 'now') + placement: 'bottom', // clock popover placement + align: 'left', // popover arrow align + donetext: '完成', // done button text + autoclose: false, // auto close when minute is selected + twelvehour: false, // change to 12 hour AM/PM clock from 24 hour + vibrate: true, // vibrate the device when dragging clock hand + hourstep: 1, // allow to multi increment the hour + minutestep: 1, // allow to multi increment the minute + ampmSubmit: false, // allow submit with AM and PM buttons instead of the minute selection/picker + addonOnly: false // only open on clicking on the input-addon }; // Show or hide popover @@ -378,7 +430,19 @@ this[this.isShown ? 'hide' : 'show'](); }; - // Set popover position + // Set new placement class for popover and remove the old one, if any. + ClockPicker.prototype.updatePlacementClass = function(newClass) { + if (this.currentPlacementClass) { + this.popover.removeClass(this.currentPlacementClass); + } + if (newClass) { + this.popover.addClass(newClass); + } + + this.currentPlacementClass = newClass; + }; + + // Set popover position and update placement class, if needed ClockPicker.prototype.locate = function(){ var element = this.element, popover = this.popover, @@ -390,6 +454,15 @@ styles = {}, self = this; + if (placement === 'top-adaptive' || placement === 'bottom-adaptive') { + var preferredPlacement = placement.substr(0, placement.indexOf('-')); + // Adaptive placement should be resolved into one of the "static" placement + // options, that is best suitable for the current window scroll position. + placement = resolveAdaptiveVerticalPlacement(element, popover, preferredPlacement); + + this.updatePlacementClass(placement !== 'viewport-top' ? placement : ''); + } + popover.show(); // Place the popover @@ -406,6 +479,9 @@ case 'left': styles.left = offset.left - popover.outerWidth(); break; + case 'viewport-top': + styles.top = offset.top - element[0].getBoundingClientRect().top; + break; } // Align the popover arrow @@ -427,6 +503,33 @@ popover.css(styles); }; + // The input can be changed by the user + // So before we can use this.hours/this.minutes we must update it + ClockPicker.prototype.parseInputValue = function(){ + var value = this.input.prop('value') || this.options['default'] || ''; + + if (value === 'now') { + value = new Date(+ new Date() + this.options.fromnow); + } + if (value instanceof Date) { + value = value.getHours() + ':' + value.getMinutes(); + } + + value = value.split(':'); + + // Minutes can have AM/PM that needs to be removed + this.hours = + value[0] || 0; + this.minutes = + (value[1] + '').replace(/\D/g, '') || 0; + + this.hours = Math.round(this.hours / this.options.hourstep) * this.options.hourstep; + this.minutes = Math.round(this.minutes / this.options.minutestep) * this.options.minutestep; + + if (this.options.twelvehour) { + var period = (value[1] + '').replace(/\d+/g, '').toLowerCase(); + this.amOrPm = this.hours > 12 || period === 'pm' ? 'PM' : 'AM'; + } + }; + // Show popover ClockPicker.prototype.show = function(e){ // Not show again @@ -434,7 +537,7 @@ return; } - raiseCallback(this.options.beforeShow); + this.raiseCallback(this.options.beforeShow, 'beforeShow'); var self = this; @@ -452,20 +555,16 @@ this.isAppended = true; } - - // Get the time - var value = ((this.input.prop('value') || this.options['default'] || '') + '').split(':'); - if (value[0] === 'now') { - var now = new Date(+ new Date() + this.options.fromnow); - value = [ - now.getHours(), - now.getMinutes() - ]; - } - this.hours = + value[0] || 0; - this.minutes = + value[1] || 0; + + // Get the time from the input field + this.parseInputValue(); + this.spanHours.html(leadingZero(this.hours)); this.spanMinutes.html(leadingZero(this.minutes)); + + if (this.options.twelvehour) { + this.spanAmPm.empty().append(this.amOrPm); + } // Toggle to hours view this.toggleView('hours'); @@ -492,12 +591,12 @@ } }); - raiseCallback(this.options.afterShow); + this.raiseCallback(this.options.afterShow, 'afterShow'); }; // Hide popover ClockPicker.prototype.hide = function(){ - raiseCallback(this.options.beforeHide); + this.raiseCallback(this.options.beforeHide, 'beforeHide'); this.isShown = false; @@ -507,14 +606,14 @@ this.popover.hide(); - raiseCallback(this.options.afterHide); + this.raiseCallback(this.options.afterHide, 'afterHide'); }; // Toggle to hours or minutes view ClockPicker.prototype.toggleView = function(view, delay){ var raiseAfterHourSelect = false; if (view === 'minutes' && $(this.hoursView).css("visibility") === "visible") { - raiseCallback(this.options.beforeHourSelect); + this.raiseCallback(this.options.beforeHourSelect, 'beforeHourSelect'); raiseAfterHourSelect = true; } var isHours = view === 'hours', @@ -540,7 +639,7 @@ }, duration); if (raiseAfterHourSelect) { - raiseCallback(this.options.afterHourSelect); + this.raiseCallback(this.options.afterHourSelect, 'afterHourSelect'); } }; @@ -567,19 +666,26 @@ }; // Set clock hand to (x, y) - ClockPicker.prototype.setHand = function(x, y, roundBy5, dragging){ + ClockPicker.prototype.setHand = function(x, y, dragging){ var radian = Math.atan2(x, - y), isHours = this.currentView === 'hours', - unit = Math.PI / (isHours || roundBy5 ? 6 : 30), z = Math.sqrt(x * x + y * y), options = this.options, inner = isHours && z < (outerRadius + innerRadius) / 2, radius = inner ? innerRadius : outerRadius, + unit, value; - - if (options.twelvehour) { - radius = outerRadius; - } + + // Calculate the unit + if (isHours) { + unit = options.hourstep / 6 * Math.PI + } else { + unit = options.minutestep / 30 * Math.PI + } + + if (options.twelvehour) { + radius = outerRadius; + } // Radian should in range [0, 2PI] if (radian < 0) { @@ -593,35 +699,25 @@ radian = value * unit; // Correct the hours or minutes - if (options.twelvehour) { - if (isHours) { - if (value === 0) { - value = 12; - } - } else { - if (roundBy5) { - value *= 5; - } - if (value === 60) { - value = 0; - } + if (isHours) { + value *= options.hourstep; + + if (! options.twelvehour && (!inner)==(value>0)) { + value += 12; + } + if (options.twelvehour && value === 0) { + value = 12; + } + if (value === 24) { + value = 0; } } else { - if (isHours) { - if (value === 12) { - value = 0; - } - value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12; - } else { - if (roundBy5) { - value *= 5; - } - if (value === 60) { - value = 0; - } + value *= options.minutestep; + if (value === 60) { + value = 0; } } - + // Once hours or minutes changed, vibrate the device if (this[this.currentView] !== value) { if (vibrate && this.options.vibrate) { @@ -670,13 +766,43 @@ this.fg.setAttribute('cy', cy); }; + // Allow user to get time time as Date object + ClockPicker.prototype.getTime = function(callback) { + this.parseInputValue(); + + var hours = this.hours; + if (this.options.twelvehour && hours < 12 && this.amOrPm === 'PM') { + hours += 12; + } + + var selectedTime = new Date(); + selectedTime.setMinutes(this.minutes) + selectedTime.setHours(hours); + selectedTime.setSeconds(0); + + return callback && callback.apply(this.element, selectedTime) || selectedTime; + } + // Hours and minutes are selected ClockPicker.prototype.done = function() { - raiseCallback(this.options.beforeDone); + this.raiseCallback(this.options.beforeDone, 'beforeDone'); this.hide(); var last = this.input.prop('value'), - value = leadingZero(this.hours) + ':' + leadingZero(this.minutes); - if (this.options.twelvehour) { + outHours = this.hours, + value = ':' + leadingZero(this.minutes); + + if (this.isHTML5 && this.options.twelvehour) { + if (this.hours < 12 && this.amOrPm === 'PM') { + outHours += 12; + } + if (this.hours === 12 && this.amOrPm === 'AM') { + outHours = 0; + } + } + + value = leadingZero(outHours) + value; + + if (!this.isHTML5 && this.options.twelvehour) { value = value + this.amOrPm; } @@ -692,7 +818,7 @@ this.input.trigger('blur'); } - raiseCallback(this.options.afterDone); + this.raiseCallback(this.options.afterDone, 'afterDone'); }; // Remove clockpicker from input @@ -712,18 +838,31 @@ // Extends $.fn.clockpicker $.fn.clockpicker = function(option){ var args = Array.prototype.slice.call(arguments, 1); - return this.each(function(){ + + function handleClockPickerRequest() { var $this = $(this), data = $this.data('clockpicker'); if (! data) { var options = $.extend({}, ClockPicker.DEFAULTS, $this.data(), typeof option == 'object' && option); $this.data('clockpicker', new ClockPicker($this, options)); } else { - // Manual operatsions. show, hide, remove, e.g. + // Manual operations. show, hide, remove, getTime, e.g. if (typeof data[option] === 'function') { - data[option].apply(data, args); + return data[option].apply(data, args); } } - }); + } + + // If we explicitly do a call on a single element then we can return the value (if needed) + // This allows us, for example, to return the value of getTime + if (this.length == 1) { + var returnValue = handleClockPickerRequest.apply(this[0]); + + // If we do not have any return value then return the object itself so you can chain + return returnValue !== undefined ? returnValue : this; + } + + // If we do have a list then we do not care about return values + return this.each(handleClockPickerRequest); }; }()); diff --git a/dist/bootstrap-clockpicker.min.js b/dist/bootstrap-clockpicker.min.js index c8006a3..acac791 100644 --- a/dist/bootstrap-clockpicker.min.js +++ b/dist/bootstrap-clockpicker.min.js @@ -3,4 +3,4 @@ * Copyright 2014 Wang Shenwei. * Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE) */ -!function(){function t(t){return document.createElementNS(p,t)}function i(t){return(10>t?"0":"")+t}function e(t){var i=++m+"";return t?t+i:i}function s(s,r){function p(t,i){var e=u.offset(),s=/^touch/.test(t.type),o=e.left+b,n=e.top+b,p=(s?t.originalEvent.touches[0]:t).pageX-o,h=(s?t.originalEvent.touches[0]:t).pageY-n,k=Math.sqrt(p*p+h*h),v=!1;if(!i||!(g-y>k||k>g+y)){t.preventDefault();var m=setTimeout(function(){c.addClass("clockpicker-moving")},200);l&&u.append(x.canvas),x.setHand(p,h,!i,!0),a.off(d).on(d,function(t){t.preventDefault();var i=/^touch/.test(t.type),e=(i?t.originalEvent.touches[0]:t).pageX-o,s=(i?t.originalEvent.touches[0]:t).pageY-n;(v||e!==p||s!==h)&&(v=!0,x.setHand(e,s,!1,!0))}),a.off(f).on(f,function(t){a.off(f),t.preventDefault();var e=/^touch/.test(t.type),s=(e?t.originalEvent.changedTouches[0]:t).pageX-o,l=(e?t.originalEvent.changedTouches[0]:t).pageY-n;(i||v)&&s===p&&l===h&&x.setHand(s,l),"hours"===x.currentView?x.toggleView("minutes",A/2):r.autoclose&&(x.minutesView.addClass("clockpicker-dial-out"),setTimeout(function(){x.done()},A/2)),u.prepend(j),clearTimeout(m),c.removeClass("clockpicker-moving"),a.off(d)})}}var h=n(V),u=h.find(".clockpicker-plate"),v=h.find(".clockpicker-hours"),m=h.find(".clockpicker-minutes"),T=h.find(".clockpicker-am-pm-block"),C="INPUT"===s.prop("tagName"),H=C?s:s.find("input"),P=s.find(".input-group-addon"),x=this;if(this.id=e("cp"),this.element=s,this.options=r,this.isAppended=!1,this.isShown=!1,this.currentView="hours",this.isInput=C,this.input=H,this.addon=P,this.popover=h,this.plate=u,this.hoursView=v,this.minutesView=m,this.amPmBlock=T,this.spanHours=h.find(".clockpicker-span-hours"),this.spanMinutes=h.find(".clockpicker-span-minutes"),this.spanAmPm=h.find(".clockpicker-span-am-pm"),this.amOrPm="PM",r.twelvehour){{var S=['
','",'","
"].join("");n(S)}n('').on("click",function(){x.amOrPm="AM",n(".clockpicker-span-am-pm").empty().append("AM")}).appendTo(this.amPmBlock),n('').on("click",function(){x.amOrPm="PM",n(".clockpicker-span-am-pm").empty().append("PM")}).appendTo(this.amPmBlock)}r.autoclose||n('").click(n.proxy(this.done,this)).appendTo(h),"top"!==r.placement&&"bottom"!==r.placement||"top"!==r.align&&"bottom"!==r.align||(r.align="left"),"left"!==r.placement&&"right"!==r.placement||"left"!==r.align&&"right"!==r.align||(r.align="top"),h.addClass(r.placement),h.addClass("clockpicker-align-"+r.align),this.spanHours.click(n.proxy(this.toggleView,this,"hours")),this.spanMinutes.click(n.proxy(this.toggleView,this,"minutes")),H.on("focus.clockpicker click.clockpicker",n.proxy(this.show,this)),P.on("click.clockpicker",n.proxy(this.toggle,this));var E,D,I,B,z=n('
');if(r.twelvehour)for(E=1;13>E;E+=1)D=z.clone(),I=E/6*Math.PI,B=g,D.css("font-size","120%"),D.css({left:b+Math.sin(I)*B-y,top:b-Math.cos(I)*B-y}),D.html(0===E?"00":E),v.append(D),D.on(k,p);else for(E=0;24>E;E+=1){D=z.clone(),I=E/6*Math.PI;var O=E>0&&13>E;B=O?w:g,D.css({left:b+Math.sin(I)*B-y,top:b-Math.cos(I)*B-y}),O&&D.css("font-size","120%"),D.html(0===E?"00":E),v.append(D),D.on(k,p)}for(E=0;60>E;E+=5)D=z.clone(),I=E/30*Math.PI,D.css({left:b+Math.sin(I)*g-y,top:b-Math.cos(I)*g-y}),D.css("font-size","120%"),D.html(i(E)),m.append(D),D.on(k,p);if(u.on(k,function(t){0===n(t.target).closest(".clockpicker-tick").length&&p(t,!0)}),l){var j=h.find(".clockpicker-canvas"),L=t("svg");L.setAttribute("class","clockpicker-svg"),L.setAttribute("width",M),L.setAttribute("height",M);var U=t("g");U.setAttribute("transform","translate("+b+","+b+")");var W=t("circle");W.setAttribute("class","clockpicker-canvas-bearing"),W.setAttribute("cx",0),W.setAttribute("cy",0),W.setAttribute("r",2);var N=t("line");N.setAttribute("x1",0),N.setAttribute("y1",0);var X=t("circle");X.setAttribute("class","clockpicker-canvas-bg"),X.setAttribute("r",y);var Y=t("circle");Y.setAttribute("class","clockpicker-canvas-fg"),Y.setAttribute("r",3.5),U.appendChild(N),U.appendChild(X),U.appendChild(Y),U.appendChild(W),L.appendChild(U),j.append(L),this.hand=N,this.bg=X,this.fg=Y,this.bearing=W,this.g=U,this.canvas=j}o(this.options.init)}function o(t){t&&"function"==typeof t&&t()}var c,n=window.jQuery,r=n(window),a=n(document),p="http://www.w3.org/2000/svg",l="SVGAngle"in window&&function(){var t,i=document.createElement("div");return i.innerHTML="",t=(i.firstChild&&i.firstChild.namespaceURI)==p,i.innerHTML="",t}(),h=function(){var t=document.createElement("div").style;return"transition"in t||"WebkitTransition"in t||"MozTransition"in t||"msTransition"in t||"OTransition"in t}(),u="ontouchstart"in window,k="mousedown"+(u?" touchstart":""),d="mousemove.clockpicker"+(u?" touchmove.clockpicker":""),f="mouseup.clockpicker"+(u?" touchend.clockpicker":""),v=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,m=0,b=100,g=80,w=54,y=13,M=2*b,A=h?350:1,V=['
','
','
',''," : ",'','',"
",'
','
','
','
','
',"
",'',"","
","
"].join("");s.DEFAULTS={"default":"",fromnow:0,placement:"bottom",align:"left",donetext:"完成",autoclose:!1,twelvehour:!1,vibrate:!0},s.prototype.toggle=function(){this[this.isShown?"hide":"show"]()},s.prototype.locate=function(){var t=this.element,i=this.popover,e=t.offset(),s=t.outerWidth(),o=t.outerHeight(),c=this.options.placement,n=this.options.align,r={};switch(i.show(),c){case"bottom":r.top=e.top+o;break;case"right":r.left=e.left+s;break;case"top":r.top=e.top-i.outerHeight();break;case"left":r.left=e.left-i.outerWidth()}switch(n){case"left":r.left=e.left;break;case"right":r.left=e.left+s-i.outerWidth();break;case"top":r.top=e.top;break;case"bottom":r.top=e.top+o-i.outerHeight()}i.css(r)},s.prototype.show=function(){if(!this.isShown){o(this.options.beforeShow);var t=this;this.isAppended||(c=n(document.body).append(this.popover),r.on("resize.clockpicker"+this.id,function(){t.isShown&&t.locate()}),this.isAppended=!0);var e=((this.input.prop("value")||this.options["default"]||"")+"").split(":");if("now"===e[0]){var s=new Date(+new Date+this.options.fromnow);e=[s.getHours(),s.getMinutes()]}this.hours=+e[0]||0,this.minutes=+e[1]||0,this.spanHours.html(i(this.hours)),this.spanMinutes.html(i(this.minutes)),this.toggleView("hours"),this.locate(),this.isShown=!0,a.on("click.clockpicker."+this.id+" focusin.clockpicker."+this.id,function(i){var e=n(i.target);0===e.closest(t.popover).length&&0===e.closest(t.addon).length&&0===e.closest(t.input).length&&t.hide()}),a.on("keyup.clockpicker."+this.id,function(i){27===i.keyCode&&t.hide()}),o(this.options.afterShow)}},s.prototype.hide=function(){o(this.options.beforeHide),this.isShown=!1,a.off("click.clockpicker."+this.id+" focusin.clockpicker."+this.id),a.off("keyup.clockpicker."+this.id),this.popover.hide(),o(this.options.afterHide)},s.prototype.toggleView=function(t,i){var e=!1;"minutes"===t&&"visible"===n(this.hoursView).css("visibility")&&(o(this.options.beforeHourSelect),e=!0);var s="hours"===t,c=s?this.hoursView:this.minutesView,r=s?this.minutesView:this.hoursView;this.currentView=t,this.spanHours.toggleClass("text-primary",s),this.spanMinutes.toggleClass("text-primary",!s),r.addClass("clockpicker-dial-out"),c.css("visibility","visible").removeClass("clockpicker-dial-out"),this.resetClock(i),clearTimeout(this.toggleViewTimer),this.toggleViewTimer=setTimeout(function(){r.css("visibility","hidden")},A),e&&o(this.options.afterHourSelect)},s.prototype.resetClock=function(t){var i=this.currentView,e=this[i],s="hours"===i,o=Math.PI/(s?6:30),c=e*o,n=s&&e>0&&13>e?w:g,r=Math.sin(c)*n,a=-Math.cos(c)*n,p=this;l&&t?(p.canvas.addClass("clockpicker-canvas-out"),setTimeout(function(){p.canvas.removeClass("clockpicker-canvas-out"),p.setHand(r,a)},t)):this.setHand(r,a)},s.prototype.setHand=function(t,e,s,o){var c,r=Math.atan2(t,-e),a="hours"===this.currentView,p=Math.PI/(a||s?6:30),h=Math.sqrt(t*t+e*e),u=this.options,k=a&&(g+w)/2>h,d=k?w:g;if(u.twelvehour&&(d=g),0>r&&(r=2*Math.PI+r),c=Math.round(r/p),r=c*p,u.twelvehour?a?0===c&&(c=12):(s&&(c*=5),60===c&&(c=0)):a?(12===c&&(c=0),c=k?0===c?12:c:0===c?0:c+12):(s&&(c*=5),60===c&&(c=0)),this[this.currentView]!==c&&v&&this.options.vibrate&&(this.vibrateTimer||(navigator[v](10),this.vibrateTimer=setTimeout(n.proxy(function(){this.vibrateTimer=null},this),100))),this[this.currentView]=c,this[a?"spanHours":"spanMinutes"].html(i(c)),!l)return void this[a?"hoursView":"minutesView"].find(".clockpicker-tick").each(function(){var t=n(this);t.toggleClass("active",c===+t.html())});o||!a&&c%5?(this.g.insertBefore(this.hand,this.bearing),this.g.insertBefore(this.bg,this.fg),this.bg.setAttribute("class","clockpicker-canvas-bg clockpicker-canvas-bg-trans")):(this.g.insertBefore(this.hand,this.bg),this.g.insertBefore(this.fg,this.bg),this.bg.setAttribute("class","clockpicker-canvas-bg"));var f=Math.sin(r)*d,m=-Math.cos(r)*d;this.hand.setAttribute("x2",f),this.hand.setAttribute("y2",m),this.bg.setAttribute("cx",f),this.bg.setAttribute("cy",m),this.fg.setAttribute("cx",f),this.fg.setAttribute("cy",m)},s.prototype.done=function(){o(this.options.beforeDone),this.hide();var t=this.input.prop("value"),e=i(this.hours)+":"+i(this.minutes);this.options.twelvehour&&(e+=this.amOrPm),this.input.prop("value",e),e!==t&&(this.input.triggerHandler("change"),this.isInput||this.element.trigger("change")),this.options.autoclose&&this.input.trigger("blur"),o(this.options.afterDone)},s.prototype.remove=function(){this.element.removeData("clockpicker"),this.input.off("focus.clockpicker click.clockpicker"),this.addon.off("click.clockpicker"),this.isShown&&this.hide(),this.isAppended&&(r.off("resize.clockpicker"+this.id),this.popover.remove())},n.fn.clockpicker=function(t){var i=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=n(this),o=e.data("clockpicker");if(o)"function"==typeof o[t]&&o[t].apply(o,i);else{var c=n.extend({},s.DEFAULTS,e.data(),"object"==typeof t&&t);e.data("clockpicker",new s(e,c))}})}}(); \ No newline at end of file +!function(){function t(t){return document.createElementNS(h,t)}function e(t){return(10>t?"0":"")+t}function i(t){var e=++g+"";return t?t+e:e}function s(s,n){function a(t,e){var i=u.offset(),s=/^touch/.test(t.type),o=i.left+b,c=i.top+b,a=(s?t.originalEvent.touches[0]:t).pageX-o,h=(s?t.originalEvent.touches[0]:t).pageY-c,d=Math.sqrt(a*a+h*h),m=!1;if(!e||!(w-M>d||d>w+M)){t.preventDefault();var v=setTimeout(function(){r.addClass("clockpicker-moving")},200);l&&u.append(x.canvas),x.setHand(a,h,!0),p.off(k).on(k,function(t){t.preventDefault();var e=/^touch/.test(t.type),i=(e?t.originalEvent.touches[0]:t).pageX-o,s=(e?t.originalEvent.touches[0]:t).pageY-c;(m||i!==a||s!==h)&&(m=!0,x.setHand(i,s,!0))}),p.off(f).on(f,function(t){p.off(f),t.preventDefault();var i=/^touch/.test(t.type),s=(i?t.originalEvent.changedTouches[0]:t).pageX-o,l=(i?t.originalEvent.changedTouches[0]:t).pageY-c;(e||m)&&s===a&&l===h&&x.setHand(s,l),"hours"===x.currentView?x.toggleView("minutes",A/2):n.autoclose&&(n.ampmSubmit||(x.minutesView.addClass("clockpicker-dial-out"),setTimeout(function(){x.done()},A/2))),u.prepend(N),clearTimeout(v),r.removeClass("clockpicker-moving"),p.off(k)})}}var h=c(P),u=h.find(".clockpicker-plate"),d=h.find(".clockpicker-hours"),v=h.find(".clockpicker-minutes"),g=h.find(".clockpicker-am-pm-block"),H="INPUT"===s.prop("tagName"),T=H?s:s.find("input"),V="time"===T.prop("type"),S=s.find(".input-group-addon"),x=this;this.id=i("cp"),this.element=s,this.options=n,this.options.hourstep=this.parseStep(this.options.hourstep,12),this.options.minutestep=this.parseStep(this.options.minutestep,60),this.isAppended=!1,this.isShown=!1,this.currentView="hours",this.isInput=H,this.isHTML5=V,this.input=T,this.addon=S,this.popover=h,this.plate=u,this.hoursView=d,this.minutesView=v,this.amPmBlock=g,this.spanHours=h.find(".clockpicker-span-hours"),this.spanMinutes=h.find(".clockpicker-span-minutes"),this.spanAmPm=h.find(".clockpicker-span-am-pm"),this.amOrPm="",this.currentPlacementClass=n.placement,this.raiseCallback=function(){o.apply(x,arguments)},n.twelvehour&&(c('').on("click",function(){x.amOrPm="AM",c(".clockpicker-span-am-pm").empty().append("AM"),n.ampmSubmit&&setTimeout(function(){x.done()},A/2)}).appendTo(this.amPmBlock),c('').on("click",function(){x.amOrPm="PM",c(".clockpicker-span-am-pm").empty().append("PM"),n.ampmSubmit&&setTimeout(function(){x.done()},A/2)}).appendTo(this.amPmBlock)),n.autoclose||c('").click(c.proxy(this.done,this)).appendTo(h),!/^(top|bottom)/.test(n.placement)||"top"!==n.align&&"bottom"!==n.align||(n.align="left"),"left"!==n.placement&&"right"!==n.placement||"left"!==n.align&&"right"!==n.align||(n.align="top"),h.addClass(n.placement),h.addClass("clockpicker-align-"+n.align),this.spanHours.click(c.proxy(this.toggleView,this,"hours")),this.spanMinutes.click(c.proxy(this.toggleView,this,"minutes")),n.addonOnly||T.on("focus.clockpicker click.clockpicker",c.proxy(this.show,this)),S.on("click.clockpicker",c.proxy(this.toggle,this));var D,I,O,E,B=c('
');if(n.twelvehour)for(D=0;12>D;D+=n.hourstep)I=B.clone(),O=D/6*Math.PI,E=w,I.css("font-size","120%"),I.css({left:b+Math.sin(O)*E-M,top:b-Math.cos(O)*E-M}),I.html(0===D?12:D),d.append(I),I.on(m,a);else for(D=0;24>D;D+=n.hourstep){I=B.clone(),O=D/6*Math.PI;var L=D>0&&13>D;E=L?y:w,I.css({left:b+Math.sin(O)*E-M,top:b-Math.cos(O)*E-M}),L&&I.css("font-size","120%"),I.html(0===D?"00":D),d.append(I),I.on(m,a)}var z=Math.max(n.minutestep,5);for(D=0;60>D;D+=z)I=B.clone(),O=D/30*Math.PI,I.css({left:b+Math.sin(O)*w-M,top:b-Math.cos(O)*w-M}),I.css("font-size","120%"),I.html(e(D)),v.append(I),I.on(m,a);if(u.on(m,function(t){0===c(t.target).closest(".clockpicker-tick").length&&a(t,!0)}),l){var N=h.find(".clockpicker-canvas"),U=t("svg");U.setAttribute("class","clockpicker-svg"),U.setAttribute("width",C),U.setAttribute("height",C);var W=t("g");W.setAttribute("transform","translate("+b+","+b+")");var j=t("circle");j.setAttribute("class","clockpicker-canvas-bearing"),j.setAttribute("cx",0),j.setAttribute("cy",0),j.setAttribute("r",2);var R=t("line");R.setAttribute("x1",0),R.setAttribute("y1",0);var X=t("circle");X.setAttribute("class","clockpicker-canvas-bg"),X.setAttribute("r",M);var Y=t("circle");Y.setAttribute("class","clockpicker-canvas-fg"),Y.setAttribute("r",3.5),W.appendChild(R),W.appendChild(X),W.appendChild(Y),W.appendChild(j),U.appendChild(W),N.append(U),this.hand=R,this.bg=X,this.fg=Y,this.bearing=j,this.g=W,this.canvas=N}this.raiseCallback(this.options.init,"init")}function o(t,e){if(t&&"function"==typeof t&&this.element){var i=this.getTime()||null;t.call(this.element,i)}e&&this.element.trigger("clockpicker."+e||"NoName")}function n(t,e,i){var s=e.outerHeight(),o=t.outerHeight(),n=t.offset().top,r=t.offset().top+o,c=n-t[0].getBoundingClientRect().top,a=c+document.documentElement.clientHeight,p=n-s>=c,h=a>=r+s;if("top"===i){if(p)return"top";if(h)return"bottom"}else{if(h)return"bottom";if(p)return"top"}return"viewport-top"}var r,c=window.jQuery,a=c(window),p=c(document),h="http://www.w3.org/2000/svg",l="SVGAngle"in window&&function(){var t,e=document.createElement("div");return e.innerHTML="",t=(e.firstChild&&e.firstChild.namespaceURI)==h,e.innerHTML="",t}(),u=function(){var t=document.createElement("div").style;return"transition"in t||"WebkitTransition"in t||"MozTransition"in t||"msTransition"in t||"OTransition"in t}(),d="ontouchstart"in window,m="mousedown"+(d?" touchstart":""),k="mousemove.clockpicker"+(d?" touchmove.clockpicker":""),f="mouseup.clockpicker"+(d?" touchend.clockpicker":""),v=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,g=0,b=100,w=80,y=54,M=13,C=2*b,A=u?350:1,P=['
','
','
',''," : ",'','',"
",'
','
','
','
','
',"
",'',"","
","
"].join("");s.prototype.parseStep=function(t,e){return e%t===0?t:1},s.DEFAULTS={"default":"",fromnow:0,placement:"bottom",align:"left",donetext:"完成",autoclose:!1,twelvehour:!1,vibrate:!0,hourstep:1,minutestep:1,ampmSubmit:!1,addonOnly:!1},s.prototype.toggle=function(){this[this.isShown?"hide":"show"]()},s.prototype.updatePlacementClass=function(t){this.currentPlacementClass&&this.popover.removeClass(this.currentPlacementClass),t&&this.popover.addClass(t),this.currentPlacementClass=t},s.prototype.locate=function(){var t=this.element,e=this.popover,i=t.offset(),s=t.outerWidth(),o=t.outerHeight(),r=this.options.placement,c=this.options.align,a={};if("top-adaptive"===r||"bottom-adaptive"===r){var p=r.substr(0,r.indexOf("-"));r=n(t,e,p),this.updatePlacementClass("viewport-top"!==r?r:"")}switch(e.show(),r){case"bottom":a.top=i.top+o;break;case"right":a.left=i.left+s;break;case"top":a.top=i.top-e.outerHeight();break;case"left":a.left=i.left-e.outerWidth();break;case"viewport-top":a.top=i.top-t[0].getBoundingClientRect().top}switch(c){case"left":a.left=i.left;break;case"right":a.left=i.left+s-e.outerWidth();break;case"top":a.top=i.top;break;case"bottom":a.top=i.top+o-e.outerHeight()}e.css(a)},s.prototype.parseInputValue=function(){var t=this.input.prop("value")||this.options["default"]||"";if("now"===t&&(t=new Date(+new Date+this.options.fromnow)),t instanceof Date&&(t=t.getHours()+":"+t.getMinutes()),t=t.split(":"),this.hours=+t[0]||0,this.minutes=+(t[1]+"").replace(/\D/g,"")||0,this.hours=Math.round(this.hours/this.options.hourstep)*this.options.hourstep,this.minutes=Math.round(this.minutes/this.options.minutestep)*this.options.minutestep,this.options.twelvehour){var e=(t[1]+"").replace(/\d+/g,"").toLowerCase();this.amOrPm=this.hours>12||"pm"===e?"PM":"AM"}},s.prototype.show=function(){if(!this.isShown){this.raiseCallback(this.options.beforeShow,"beforeShow");var t=this;this.isAppended||(r=c(document.body).append(this.popover),a.on("resize.clockpicker"+this.id,function(){t.isShown&&t.locate()}),this.isAppended=!0),this.parseInputValue(),this.spanHours.html(e(this.hours)),this.spanMinutes.html(e(this.minutes)),this.options.twelvehour&&this.spanAmPm.empty().append(this.amOrPm),this.toggleView("hours"),this.locate(),this.isShown=!0,p.on("click.clockpicker."+this.id+" focusin.clockpicker."+this.id,function(e){var i=c(e.target);0===i.closest(t.popover).length&&0===i.closest(t.addon).length&&0===i.closest(t.input).length&&t.hide()}),p.on("keyup.clockpicker."+this.id,function(e){27===e.keyCode&&t.hide()}),this.raiseCallback(this.options.afterShow,"afterShow")}},s.prototype.hide=function(){this.raiseCallback(this.options.beforeHide,"beforeHide"),this.isShown=!1,p.off("click.clockpicker."+this.id+" focusin.clockpicker."+this.id),p.off("keyup.clockpicker."+this.id),this.popover.hide(),this.raiseCallback(this.options.afterHide,"afterHide")},s.prototype.toggleView=function(t,e){var i=!1;"minutes"===t&&"visible"===c(this.hoursView).css("visibility")&&(this.raiseCallback(this.options.beforeHourSelect,"beforeHourSelect"),i=!0);var s="hours"===t,o=s?this.hoursView:this.minutesView,n=s?this.minutesView:this.hoursView;this.currentView=t,this.spanHours.toggleClass("text-primary",s),this.spanMinutes.toggleClass("text-primary",!s),n.addClass("clockpicker-dial-out"),o.css("visibility","visible").removeClass("clockpicker-dial-out"),this.resetClock(e),clearTimeout(this.toggleViewTimer),this.toggleViewTimer=setTimeout(function(){n.css("visibility","hidden")},A),i&&this.raiseCallback(this.options.afterHourSelect,"afterHourSelect")},s.prototype.resetClock=function(t){var e=this.currentView,i=this[e],s="hours"===e,o=Math.PI/(s?6:30),n=i*o,r=s&&i>0&&13>i?y:w,c=Math.sin(n)*r,a=-Math.cos(n)*r,p=this;l&&t?(p.canvas.addClass("clockpicker-canvas-out"),setTimeout(function(){p.canvas.removeClass("clockpicker-canvas-out"),p.setHand(c,a)},t)):this.setHand(c,a)},s.prototype.setHand=function(t,i,s){var o,n,r=Math.atan2(t,-i),a="hours"===this.currentView,p=Math.sqrt(t*t+i*i),h=this.options,u=a&&(w+y)/2>p,d=u?y:w;if(o=a?h.hourstep/6*Math.PI:h.minutestep/30*Math.PI,h.twelvehour&&(d=w),0>r&&(r=2*Math.PI+r),n=Math.round(r/o),r=n*o,a?(n*=h.hourstep,h.twelvehour||!u!=n>0||(n+=12),h.twelvehour&&0===n&&(n=12),24===n&&(n=0)):(n*=h.minutestep,60===n&&(n=0)),this[this.currentView]!==n&&v&&this.options.vibrate&&(this.vibrateTimer||(navigator[v](10),this.vibrateTimer=setTimeout(c.proxy(function(){this.vibrateTimer=null},this),100))),this[this.currentView]=n,this[a?"spanHours":"spanMinutes"].html(e(n)),!l)return this[a?"hoursView":"minutesView"].find(".clockpicker-tick").each(function(){var t=c(this);t.toggleClass("active",n===+t.html())}),void 0;s||!a&&n%5?(this.g.insertBefore(this.hand,this.bearing),this.g.insertBefore(this.bg,this.fg),this.bg.setAttribute("class","clockpicker-canvas-bg clockpicker-canvas-bg-trans")):(this.g.insertBefore(this.hand,this.bg),this.g.insertBefore(this.fg,this.bg),this.bg.setAttribute("class","clockpicker-canvas-bg"));var m=Math.sin(r)*d,k=-Math.cos(r)*d;this.hand.setAttribute("x2",m),this.hand.setAttribute("y2",k),this.bg.setAttribute("cx",m),this.bg.setAttribute("cy",k),this.fg.setAttribute("cx",m),this.fg.setAttribute("cy",k)},s.prototype.getTime=function(t){this.parseInputValue();var e=this.hours;this.options.twelvehour&&12>e&&"PM"===this.amOrPm&&(e+=12);var i=new Date;return i.setMinutes(this.minutes),i.setHours(e),i.setSeconds(0),t&&t.apply(this.element,i)||i},s.prototype.done=function(){this.raiseCallback(this.options.beforeDone,"beforeDone"),this.hide();var t=this.input.prop("value"),i=this.hours,s=":"+e(this.minutes);this.isHTML5&&this.options.twelvehour&&(this.hours<12&&"PM"===this.amOrPm&&(i+=12),12===this.hours&&"AM"===this.amOrPm&&(i=0)),s=e(i)+s,!this.isHTML5&&this.options.twelvehour&&(s+=this.amOrPm),this.input.prop("value",s),s!==t&&(this.input.triggerHandler("change"),this.isInput||this.element.trigger("change")),this.options.autoclose&&this.input.trigger("blur"),this.raiseCallback(this.options.afterDone,"afterDone")},s.prototype.remove=function(){this.element.removeData("clockpicker"),this.input.off("focus.clockpicker click.clockpicker"),this.addon.off("click.clockpicker"),this.isShown&&this.hide(),this.isAppended&&(a.off("resize.clockpicker"+this.id),this.popover.remove())},c.fn.clockpicker=function(t){function e(){var e=c(this),o=e.data("clockpicker");if(o){if("function"==typeof o[t])return o[t].apply(o,i)}else{var n=c.extend({},s.DEFAULTS,e.data(),"object"==typeof t&&t);e.data("clockpicker",new s(e,n))}}var i=Array.prototype.slice.call(arguments,1);if(1==this.length){var o=e.apply(this[0]);return void 0!==o?o:this}return this.each(e)}}(); \ No newline at end of file diff --git a/dist/jquery-clockpicker.js b/dist/jquery-clockpicker.js index e930b4f..51c9530 100644 --- a/dist/jquery-clockpicker.js +++ b/dist/jquery-clockpicker.js @@ -95,6 +95,7 @@ amPmBlock = popover.find('.clockpicker-am-pm-block'), isInput = element.prop('tagName') === 'INPUT', input = isInput ? element : element.find('input'), + isHTML5 = input.prop('type') === 'time', addon = element.find('.input-group-addon'), self = this, timer; @@ -102,10 +103,13 @@ this.id = uniqueId('cp'); this.element = element; this.options = options; + this.options.hourstep = this.parseStep(this.options.hourstep, 12); + this.options.minutestep = this.parseStep(this.options.minutestep, 60); this.isAppended = false; this.isShown = false; this.currentView = 'hours'; this.isInput = isInput; + this.isHTML5 = isHTML5; this.input = input; this.addon = addon; this.popover = popover; @@ -116,38 +120,25 @@ this.spanHours = popover.find('.clockpicker-span-hours'); this.spanMinutes = popover.find('.clockpicker-span-minutes'); this.spanAmPm = popover.find('.clockpicker-span-am-pm'); - this.amOrPm = "PM"; - + this.amOrPm = ""; + this.currentPlacementClass = options.placement; + this.raiseCallback = function() { + raiseCallback.apply(self, arguments); + }; + // Setup for for 12 hour clock if option is selected if (options.twelvehour) { - - var amPmButtonsTemplate = ['
', - '', - '', - '
'].join(''); - - var amPmButtons = $(amPmButtonsTemplate); - //amPmButtons.appendTo(plate); - - ////Not working b/c they are not shown when this runs - //$('clockpicker-am-button') - // .on("click", function() { - // self.amOrPm = "AM"; - // $('.clockpicker-span-am-pm').empty().append('AM'); - // }); - // - //$('clockpicker-pm-button') - // .on("click", function() { - // self.amOrPm = "PM"; - // $('.clockpicker-span-am-pm').empty().append('PM'); - // }); - + $('') .on("click", function() { self.amOrPm = "AM"; $('.clockpicker-span-am-pm').empty().append('AM'); + + if (options.ampmSubmit) { + setTimeout(function(){ + self.done(); + }, duration / 2); + } }).appendTo(this.amPmBlock); @@ -155,6 +146,12 @@ .on("click", function() { self.amOrPm = 'PM'; $('.clockpicker-span-am-pm').empty().append('PM'); + + if (options.ampmSubmit) { + setTimeout(function(){ + self.done(); + }, duration / 2); + } }).appendTo(this.amPmBlock); } @@ -167,7 +164,7 @@ } // Placement and arrow align - make sure they make sense. - if ((options.placement === 'top' || options.placement === 'bottom') && (options.align === 'top' || options.align === 'bottom')) options.align = 'left'; + if (/^(top|bottom)/.test(options.placement) && (options.align === 'top' || options.align === 'bottom')) options.align = 'left'; if ((options.placement === 'left' || options.placement === 'right') && (options.align === 'left' || options.align === 'right')) options.align = 'top'; popover.addClass(options.placement); @@ -177,7 +174,9 @@ this.spanMinutes.click($.proxy(this.toggleView, this, 'minutes')); // Show or toggle - input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this)); + if (!options.addonOnly) { + input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this)); + } addon.on('click.clockpicker', $.proxy(this.toggle, this)); // Build ticks @@ -186,7 +185,7 @@ // Hours view if (options.twelvehour) { - for (i = 1; i < 13; i += 1) { + for (i = 0; i < 12; i += options.hourstep) { tick = tickTpl.clone(); radian = i / 6 * Math.PI; radius = outerRadius; @@ -195,12 +194,12 @@ left: dialRadius + Math.sin(radian) * radius - tickRadius, top: dialRadius - Math.cos(radian) * radius - tickRadius }); - tick.html(i === 0 ? '00' : i); + tick.html(i === 0 ? 12 : i); hoursView.append(tick); tick.on(mousedownEvent, mousedown); } } else { - for (i = 0; i < 24; i += 1) { + for (i = 0; i < 24; i += options.hourstep) { tick = tickTpl.clone(); radian = i / 6 * Math.PI; var inner = i > 0 && i < 13; @@ -219,7 +218,8 @@ } // Minutes view - for (i = 0; i < 60; i += 5) { + var incrementValue = Math.max(options.minutestep, 5); + for (i = 0; i < 60; i += incrementValue) { tick = tickTpl.clone(); radian = i / 30 * Math.PI; tick.css({ @@ -267,7 +267,7 @@ } // Clock - self.setHand(dx, dy, ! space, true); + self.setHand(dx, dy, true); // Mousemove on document $doc.off(mousemoveEvent).on(mousemoveEvent, function(e){ @@ -280,7 +280,7 @@ return; } moved = true; - self.setHand(x, y, false, true); + self.setHand(x, y, true); }); // Mouseup on document @@ -297,10 +297,12 @@ self.toggleView('minutes', duration / 2); } else { if (options.autoclose) { - self.minutesView.addClass('clockpicker-dial-out'); - setTimeout(function(){ - self.done(); - }, duration / 2); + if (!options.ampmSubmit) { + self.minutesView.addClass('clockpicker-dial-out'); + setTimeout(function(){ + self.done(); + }, duration / 2); + } } } plate.prepend(canvas); @@ -352,25 +354,75 @@ this.canvas = canvas; } - raiseCallback(this.options.init); + this.raiseCallback(this.options.init, 'init'); } - function raiseCallback(callbackFunction) { - if (callbackFunction && typeof callbackFunction === "function") { - callbackFunction(); + function raiseCallback(callbackFunction, triggerName) { + if (callbackFunction && typeof callbackFunction === "function" && this.element) { + var time = this.getTime() || null; + callbackFunction.call(this.element, time); + } + if (triggerName) { + this.element.trigger('clockpicker.' + triggerName || 'NoName'); + } + } + + /** + * Find most suitable vertical placement, doing our best to ensure it is inside of the viewport. + * + * First try to place the element according with preferredPlacement, then try the opposite + * placement and as a last resort, popover will be placed on the very top of the viewport. + * + * @param {jQuery} element + * @param {jQuery} popover + * @param preferredPlacement Preferred placement, if there is enough room for it. + * @returns {string} One of: 'top', 'bottom' or 'viewport-top'. + */ + function resolveAdaptiveVerticalPlacement(element, popover, preferredPlacement) { + var popoverHeight = popover.outerHeight(), + elementHeight = element.outerHeight(), + elementTopOffset = element.offset().top, + elementBottomOffset = element.offset().top + elementHeight, + minVisibleY = elementTopOffset - element[0].getBoundingClientRect().top, + maxVisibleY = minVisibleY + document.documentElement.clientHeight, + isEnoughRoomAbove = (elementTopOffset - popoverHeight) >= minVisibleY, + isEnoughRoomBelow = (elementBottomOffset + popoverHeight) <= maxVisibleY; + + if (preferredPlacement === 'top') { + if (isEnoughRoomAbove) { + return 'top'; + } else if (isEnoughRoomBelow) { + return 'bottom'; + } + } else { + if (isEnoughRoomBelow) { + return 'bottom'; + } else if (isEnoughRoomAbove) { + return 'top'; + } } + + return 'viewport-top'; + } + + ClockPicker.prototype.parseStep = function(givenStepSize, wholeSize) { + return wholeSize % givenStepSize === 0 ? givenStepSize : 1; } // Default options ClockPicker.DEFAULTS = { - 'default': '', // default time, 'now' or '13:14' e.g. - fromnow: 0, // set default time to * milliseconds from now (using with default = 'now') - placement: 'bottom', // clock popover placement - align: 'left', // popover arrow align - donetext: '完成', // done button text - autoclose: false, // auto close when minute is selected - twelvehour: false, // change to 12 hour AM/PM clock from 24 hour - vibrate: true // vibrate the device when dragging clock hand + 'default': '', // default time, 'now' or '13:14' e.g. + fromnow: 0, // set default time to * milliseconds from now (using with default = 'now') + placement: 'bottom', // clock popover placement + align: 'left', // popover arrow align + donetext: '完成', // done button text + autoclose: false, // auto close when minute is selected + twelvehour: false, // change to 12 hour AM/PM clock from 24 hour + vibrate: true, // vibrate the device when dragging clock hand + hourstep: 1, // allow to multi increment the hour + minutestep: 1, // allow to multi increment the minute + ampmSubmit: false, // allow submit with AM and PM buttons instead of the minute selection/picker + addonOnly: false // only open on clicking on the input-addon }; // Show or hide popover @@ -378,7 +430,19 @@ this[this.isShown ? 'hide' : 'show'](); }; - // Set popover position + // Set new placement class for popover and remove the old one, if any. + ClockPicker.prototype.updatePlacementClass = function(newClass) { + if (this.currentPlacementClass) { + this.popover.removeClass(this.currentPlacementClass); + } + if (newClass) { + this.popover.addClass(newClass); + } + + this.currentPlacementClass = newClass; + }; + + // Set popover position and update placement class, if needed ClockPicker.prototype.locate = function(){ var element = this.element, popover = this.popover, @@ -390,6 +454,15 @@ styles = {}, self = this; + if (placement === 'top-adaptive' || placement === 'bottom-adaptive') { + var preferredPlacement = placement.substr(0, placement.indexOf('-')); + // Adaptive placement should be resolved into one of the "static" placement + // options, that is best suitable for the current window scroll position. + placement = resolveAdaptiveVerticalPlacement(element, popover, preferredPlacement); + + this.updatePlacementClass(placement !== 'viewport-top' ? placement : ''); + } + popover.show(); // Place the popover @@ -406,6 +479,9 @@ case 'left': styles.left = offset.left - popover.outerWidth(); break; + case 'viewport-top': + styles.top = offset.top - element[0].getBoundingClientRect().top; + break; } // Align the popover arrow @@ -427,6 +503,33 @@ popover.css(styles); }; + // The input can be changed by the user + // So before we can use this.hours/this.minutes we must update it + ClockPicker.prototype.parseInputValue = function(){ + var value = this.input.prop('value') || this.options['default'] || ''; + + if (value === 'now') { + value = new Date(+ new Date() + this.options.fromnow); + } + if (value instanceof Date) { + value = value.getHours() + ':' + value.getMinutes(); + } + + value = value.split(':'); + + // Minutes can have AM/PM that needs to be removed + this.hours = + value[0] || 0; + this.minutes = + (value[1] + '').replace(/\D/g, '') || 0; + + this.hours = Math.round(this.hours / this.options.hourstep) * this.options.hourstep; + this.minutes = Math.round(this.minutes / this.options.minutestep) * this.options.minutestep; + + if (this.options.twelvehour) { + var period = (value[1] + '').replace(/\d+/g, '').toLowerCase(); + this.amOrPm = this.hours > 12 || period === 'pm' ? 'PM' : 'AM'; + } + }; + // Show popover ClockPicker.prototype.show = function(e){ // Not show again @@ -434,7 +537,7 @@ return; } - raiseCallback(this.options.beforeShow); + this.raiseCallback(this.options.beforeShow, 'beforeShow'); var self = this; @@ -452,20 +555,16 @@ this.isAppended = true; } - - // Get the time - var value = ((this.input.prop('value') || this.options['default'] || '') + '').split(':'); - if (value[0] === 'now') { - var now = new Date(+ new Date() + this.options.fromnow); - value = [ - now.getHours(), - now.getMinutes() - ]; - } - this.hours = + value[0] || 0; - this.minutes = + value[1] || 0; + + // Get the time from the input field + this.parseInputValue(); + this.spanHours.html(leadingZero(this.hours)); this.spanMinutes.html(leadingZero(this.minutes)); + + if (this.options.twelvehour) { + this.spanAmPm.empty().append(this.amOrPm); + } // Toggle to hours view this.toggleView('hours'); @@ -492,12 +591,12 @@ } }); - raiseCallback(this.options.afterShow); + this.raiseCallback(this.options.afterShow, 'afterShow'); }; // Hide popover ClockPicker.prototype.hide = function(){ - raiseCallback(this.options.beforeHide); + this.raiseCallback(this.options.beforeHide, 'beforeHide'); this.isShown = false; @@ -507,14 +606,14 @@ this.popover.hide(); - raiseCallback(this.options.afterHide); + this.raiseCallback(this.options.afterHide, 'afterHide'); }; // Toggle to hours or minutes view ClockPicker.prototype.toggleView = function(view, delay){ var raiseAfterHourSelect = false; if (view === 'minutes' && $(this.hoursView).css("visibility") === "visible") { - raiseCallback(this.options.beforeHourSelect); + this.raiseCallback(this.options.beforeHourSelect, 'beforeHourSelect'); raiseAfterHourSelect = true; } var isHours = view === 'hours', @@ -540,7 +639,7 @@ }, duration); if (raiseAfterHourSelect) { - raiseCallback(this.options.afterHourSelect); + this.raiseCallback(this.options.afterHourSelect, 'afterHourSelect'); } }; @@ -567,19 +666,26 @@ }; // Set clock hand to (x, y) - ClockPicker.prototype.setHand = function(x, y, roundBy5, dragging){ + ClockPicker.prototype.setHand = function(x, y, dragging){ var radian = Math.atan2(x, - y), isHours = this.currentView === 'hours', - unit = Math.PI / (isHours || roundBy5 ? 6 : 30), z = Math.sqrt(x * x + y * y), options = this.options, inner = isHours && z < (outerRadius + innerRadius) / 2, radius = inner ? innerRadius : outerRadius, + unit, value; - - if (options.twelvehour) { - radius = outerRadius; - } + + // Calculate the unit + if (isHours) { + unit = options.hourstep / 6 * Math.PI + } else { + unit = options.minutestep / 30 * Math.PI + } + + if (options.twelvehour) { + radius = outerRadius; + } // Radian should in range [0, 2PI] if (radian < 0) { @@ -593,35 +699,25 @@ radian = value * unit; // Correct the hours or minutes - if (options.twelvehour) { - if (isHours) { - if (value === 0) { - value = 12; - } - } else { - if (roundBy5) { - value *= 5; - } - if (value === 60) { - value = 0; - } + if (isHours) { + value *= options.hourstep; + + if (! options.twelvehour && (!inner)==(value>0)) { + value += 12; + } + if (options.twelvehour && value === 0) { + value = 12; + } + if (value === 24) { + value = 0; } } else { - if (isHours) { - if (value === 12) { - value = 0; - } - value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12; - } else { - if (roundBy5) { - value *= 5; - } - if (value === 60) { - value = 0; - } + value *= options.minutestep; + if (value === 60) { + value = 0; } } - + // Once hours or minutes changed, vibrate the device if (this[this.currentView] !== value) { if (vibrate && this.options.vibrate) { @@ -670,13 +766,43 @@ this.fg.setAttribute('cy', cy); }; + // Allow user to get time time as Date object + ClockPicker.prototype.getTime = function(callback) { + this.parseInputValue(); + + var hours = this.hours; + if (this.options.twelvehour && hours < 12 && this.amOrPm === 'PM') { + hours += 12; + } + + var selectedTime = new Date(); + selectedTime.setMinutes(this.minutes) + selectedTime.setHours(hours); + selectedTime.setSeconds(0); + + return callback && callback.apply(this.element, selectedTime) || selectedTime; + } + // Hours and minutes are selected ClockPicker.prototype.done = function() { - raiseCallback(this.options.beforeDone); + this.raiseCallback(this.options.beforeDone, 'beforeDone'); this.hide(); var last = this.input.prop('value'), - value = leadingZero(this.hours) + ':' + leadingZero(this.minutes); - if (this.options.twelvehour) { + outHours = this.hours, + value = ':' + leadingZero(this.minutes); + + if (this.isHTML5 && this.options.twelvehour) { + if (this.hours < 12 && this.amOrPm === 'PM') { + outHours += 12; + } + if (this.hours === 12 && this.amOrPm === 'AM') { + outHours = 0; + } + } + + value = leadingZero(outHours) + value; + + if (!this.isHTML5 && this.options.twelvehour) { value = value + this.amOrPm; } @@ -692,7 +818,7 @@ this.input.trigger('blur'); } - raiseCallback(this.options.afterDone); + this.raiseCallback(this.options.afterDone, 'afterDone'); }; // Remove clockpicker from input @@ -712,18 +838,31 @@ // Extends $.fn.clockpicker $.fn.clockpicker = function(option){ var args = Array.prototype.slice.call(arguments, 1); - return this.each(function(){ + + function handleClockPickerRequest() { var $this = $(this), data = $this.data('clockpicker'); if (! data) { var options = $.extend({}, ClockPicker.DEFAULTS, $this.data(), typeof option == 'object' && option); $this.data('clockpicker', new ClockPicker($this, options)); } else { - // Manual operatsions. show, hide, remove, e.g. + // Manual operations. show, hide, remove, getTime, e.g. if (typeof data[option] === 'function') { - data[option].apply(data, args); + return data[option].apply(data, args); } } - }); + } + + // If we explicitly do a call on a single element then we can return the value (if needed) + // This allows us, for example, to return the value of getTime + if (this.length == 1) { + var returnValue = handleClockPickerRequest.apply(this[0]); + + // If we do not have any return value then return the object itself so you can chain + return returnValue !== undefined ? returnValue : this; + } + + // If we do have a list then we do not care about return values + return this.each(handleClockPickerRequest); }; }()); diff --git a/dist/jquery-clockpicker.min.js b/dist/jquery-clockpicker.min.js index c8006a3..acac791 100644 --- a/dist/jquery-clockpicker.min.js +++ b/dist/jquery-clockpicker.min.js @@ -3,4 +3,4 @@ * Copyright 2014 Wang Shenwei. * Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE) */ -!function(){function t(t){return document.createElementNS(p,t)}function i(t){return(10>t?"0":"")+t}function e(t){var i=++m+"";return t?t+i:i}function s(s,r){function p(t,i){var e=u.offset(),s=/^touch/.test(t.type),o=e.left+b,n=e.top+b,p=(s?t.originalEvent.touches[0]:t).pageX-o,h=(s?t.originalEvent.touches[0]:t).pageY-n,k=Math.sqrt(p*p+h*h),v=!1;if(!i||!(g-y>k||k>g+y)){t.preventDefault();var m=setTimeout(function(){c.addClass("clockpicker-moving")},200);l&&u.append(x.canvas),x.setHand(p,h,!i,!0),a.off(d).on(d,function(t){t.preventDefault();var i=/^touch/.test(t.type),e=(i?t.originalEvent.touches[0]:t).pageX-o,s=(i?t.originalEvent.touches[0]:t).pageY-n;(v||e!==p||s!==h)&&(v=!0,x.setHand(e,s,!1,!0))}),a.off(f).on(f,function(t){a.off(f),t.preventDefault();var e=/^touch/.test(t.type),s=(e?t.originalEvent.changedTouches[0]:t).pageX-o,l=(e?t.originalEvent.changedTouches[0]:t).pageY-n;(i||v)&&s===p&&l===h&&x.setHand(s,l),"hours"===x.currentView?x.toggleView("minutes",A/2):r.autoclose&&(x.minutesView.addClass("clockpicker-dial-out"),setTimeout(function(){x.done()},A/2)),u.prepend(j),clearTimeout(m),c.removeClass("clockpicker-moving"),a.off(d)})}}var h=n(V),u=h.find(".clockpicker-plate"),v=h.find(".clockpicker-hours"),m=h.find(".clockpicker-minutes"),T=h.find(".clockpicker-am-pm-block"),C="INPUT"===s.prop("tagName"),H=C?s:s.find("input"),P=s.find(".input-group-addon"),x=this;if(this.id=e("cp"),this.element=s,this.options=r,this.isAppended=!1,this.isShown=!1,this.currentView="hours",this.isInput=C,this.input=H,this.addon=P,this.popover=h,this.plate=u,this.hoursView=v,this.minutesView=m,this.amPmBlock=T,this.spanHours=h.find(".clockpicker-span-hours"),this.spanMinutes=h.find(".clockpicker-span-minutes"),this.spanAmPm=h.find(".clockpicker-span-am-pm"),this.amOrPm="PM",r.twelvehour){{var S=['
','",'","
"].join("");n(S)}n('').on("click",function(){x.amOrPm="AM",n(".clockpicker-span-am-pm").empty().append("AM")}).appendTo(this.amPmBlock),n('').on("click",function(){x.amOrPm="PM",n(".clockpicker-span-am-pm").empty().append("PM")}).appendTo(this.amPmBlock)}r.autoclose||n('").click(n.proxy(this.done,this)).appendTo(h),"top"!==r.placement&&"bottom"!==r.placement||"top"!==r.align&&"bottom"!==r.align||(r.align="left"),"left"!==r.placement&&"right"!==r.placement||"left"!==r.align&&"right"!==r.align||(r.align="top"),h.addClass(r.placement),h.addClass("clockpicker-align-"+r.align),this.spanHours.click(n.proxy(this.toggleView,this,"hours")),this.spanMinutes.click(n.proxy(this.toggleView,this,"minutes")),H.on("focus.clockpicker click.clockpicker",n.proxy(this.show,this)),P.on("click.clockpicker",n.proxy(this.toggle,this));var E,D,I,B,z=n('
');if(r.twelvehour)for(E=1;13>E;E+=1)D=z.clone(),I=E/6*Math.PI,B=g,D.css("font-size","120%"),D.css({left:b+Math.sin(I)*B-y,top:b-Math.cos(I)*B-y}),D.html(0===E?"00":E),v.append(D),D.on(k,p);else for(E=0;24>E;E+=1){D=z.clone(),I=E/6*Math.PI;var O=E>0&&13>E;B=O?w:g,D.css({left:b+Math.sin(I)*B-y,top:b-Math.cos(I)*B-y}),O&&D.css("font-size","120%"),D.html(0===E?"00":E),v.append(D),D.on(k,p)}for(E=0;60>E;E+=5)D=z.clone(),I=E/30*Math.PI,D.css({left:b+Math.sin(I)*g-y,top:b-Math.cos(I)*g-y}),D.css("font-size","120%"),D.html(i(E)),m.append(D),D.on(k,p);if(u.on(k,function(t){0===n(t.target).closest(".clockpicker-tick").length&&p(t,!0)}),l){var j=h.find(".clockpicker-canvas"),L=t("svg");L.setAttribute("class","clockpicker-svg"),L.setAttribute("width",M),L.setAttribute("height",M);var U=t("g");U.setAttribute("transform","translate("+b+","+b+")");var W=t("circle");W.setAttribute("class","clockpicker-canvas-bearing"),W.setAttribute("cx",0),W.setAttribute("cy",0),W.setAttribute("r",2);var N=t("line");N.setAttribute("x1",0),N.setAttribute("y1",0);var X=t("circle");X.setAttribute("class","clockpicker-canvas-bg"),X.setAttribute("r",y);var Y=t("circle");Y.setAttribute("class","clockpicker-canvas-fg"),Y.setAttribute("r",3.5),U.appendChild(N),U.appendChild(X),U.appendChild(Y),U.appendChild(W),L.appendChild(U),j.append(L),this.hand=N,this.bg=X,this.fg=Y,this.bearing=W,this.g=U,this.canvas=j}o(this.options.init)}function o(t){t&&"function"==typeof t&&t()}var c,n=window.jQuery,r=n(window),a=n(document),p="http://www.w3.org/2000/svg",l="SVGAngle"in window&&function(){var t,i=document.createElement("div");return i.innerHTML="",t=(i.firstChild&&i.firstChild.namespaceURI)==p,i.innerHTML="",t}(),h=function(){var t=document.createElement("div").style;return"transition"in t||"WebkitTransition"in t||"MozTransition"in t||"msTransition"in t||"OTransition"in t}(),u="ontouchstart"in window,k="mousedown"+(u?" touchstart":""),d="mousemove.clockpicker"+(u?" touchmove.clockpicker":""),f="mouseup.clockpicker"+(u?" touchend.clockpicker":""),v=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,m=0,b=100,g=80,w=54,y=13,M=2*b,A=h?350:1,V=['
','
','
',''," : ",'','',"
",'
','
','
','
','
',"
",'',"","
","
"].join("");s.DEFAULTS={"default":"",fromnow:0,placement:"bottom",align:"left",donetext:"完成",autoclose:!1,twelvehour:!1,vibrate:!0},s.prototype.toggle=function(){this[this.isShown?"hide":"show"]()},s.prototype.locate=function(){var t=this.element,i=this.popover,e=t.offset(),s=t.outerWidth(),o=t.outerHeight(),c=this.options.placement,n=this.options.align,r={};switch(i.show(),c){case"bottom":r.top=e.top+o;break;case"right":r.left=e.left+s;break;case"top":r.top=e.top-i.outerHeight();break;case"left":r.left=e.left-i.outerWidth()}switch(n){case"left":r.left=e.left;break;case"right":r.left=e.left+s-i.outerWidth();break;case"top":r.top=e.top;break;case"bottom":r.top=e.top+o-i.outerHeight()}i.css(r)},s.prototype.show=function(){if(!this.isShown){o(this.options.beforeShow);var t=this;this.isAppended||(c=n(document.body).append(this.popover),r.on("resize.clockpicker"+this.id,function(){t.isShown&&t.locate()}),this.isAppended=!0);var e=((this.input.prop("value")||this.options["default"]||"")+"").split(":");if("now"===e[0]){var s=new Date(+new Date+this.options.fromnow);e=[s.getHours(),s.getMinutes()]}this.hours=+e[0]||0,this.minutes=+e[1]||0,this.spanHours.html(i(this.hours)),this.spanMinutes.html(i(this.minutes)),this.toggleView("hours"),this.locate(),this.isShown=!0,a.on("click.clockpicker."+this.id+" focusin.clockpicker."+this.id,function(i){var e=n(i.target);0===e.closest(t.popover).length&&0===e.closest(t.addon).length&&0===e.closest(t.input).length&&t.hide()}),a.on("keyup.clockpicker."+this.id,function(i){27===i.keyCode&&t.hide()}),o(this.options.afterShow)}},s.prototype.hide=function(){o(this.options.beforeHide),this.isShown=!1,a.off("click.clockpicker."+this.id+" focusin.clockpicker."+this.id),a.off("keyup.clockpicker."+this.id),this.popover.hide(),o(this.options.afterHide)},s.prototype.toggleView=function(t,i){var e=!1;"minutes"===t&&"visible"===n(this.hoursView).css("visibility")&&(o(this.options.beforeHourSelect),e=!0);var s="hours"===t,c=s?this.hoursView:this.minutesView,r=s?this.minutesView:this.hoursView;this.currentView=t,this.spanHours.toggleClass("text-primary",s),this.spanMinutes.toggleClass("text-primary",!s),r.addClass("clockpicker-dial-out"),c.css("visibility","visible").removeClass("clockpicker-dial-out"),this.resetClock(i),clearTimeout(this.toggleViewTimer),this.toggleViewTimer=setTimeout(function(){r.css("visibility","hidden")},A),e&&o(this.options.afterHourSelect)},s.prototype.resetClock=function(t){var i=this.currentView,e=this[i],s="hours"===i,o=Math.PI/(s?6:30),c=e*o,n=s&&e>0&&13>e?w:g,r=Math.sin(c)*n,a=-Math.cos(c)*n,p=this;l&&t?(p.canvas.addClass("clockpicker-canvas-out"),setTimeout(function(){p.canvas.removeClass("clockpicker-canvas-out"),p.setHand(r,a)},t)):this.setHand(r,a)},s.prototype.setHand=function(t,e,s,o){var c,r=Math.atan2(t,-e),a="hours"===this.currentView,p=Math.PI/(a||s?6:30),h=Math.sqrt(t*t+e*e),u=this.options,k=a&&(g+w)/2>h,d=k?w:g;if(u.twelvehour&&(d=g),0>r&&(r=2*Math.PI+r),c=Math.round(r/p),r=c*p,u.twelvehour?a?0===c&&(c=12):(s&&(c*=5),60===c&&(c=0)):a?(12===c&&(c=0),c=k?0===c?12:c:0===c?0:c+12):(s&&(c*=5),60===c&&(c=0)),this[this.currentView]!==c&&v&&this.options.vibrate&&(this.vibrateTimer||(navigator[v](10),this.vibrateTimer=setTimeout(n.proxy(function(){this.vibrateTimer=null},this),100))),this[this.currentView]=c,this[a?"spanHours":"spanMinutes"].html(i(c)),!l)return void this[a?"hoursView":"minutesView"].find(".clockpicker-tick").each(function(){var t=n(this);t.toggleClass("active",c===+t.html())});o||!a&&c%5?(this.g.insertBefore(this.hand,this.bearing),this.g.insertBefore(this.bg,this.fg),this.bg.setAttribute("class","clockpicker-canvas-bg clockpicker-canvas-bg-trans")):(this.g.insertBefore(this.hand,this.bg),this.g.insertBefore(this.fg,this.bg),this.bg.setAttribute("class","clockpicker-canvas-bg"));var f=Math.sin(r)*d,m=-Math.cos(r)*d;this.hand.setAttribute("x2",f),this.hand.setAttribute("y2",m),this.bg.setAttribute("cx",f),this.bg.setAttribute("cy",m),this.fg.setAttribute("cx",f),this.fg.setAttribute("cy",m)},s.prototype.done=function(){o(this.options.beforeDone),this.hide();var t=this.input.prop("value"),e=i(this.hours)+":"+i(this.minutes);this.options.twelvehour&&(e+=this.amOrPm),this.input.prop("value",e),e!==t&&(this.input.triggerHandler("change"),this.isInput||this.element.trigger("change")),this.options.autoclose&&this.input.trigger("blur"),o(this.options.afterDone)},s.prototype.remove=function(){this.element.removeData("clockpicker"),this.input.off("focus.clockpicker click.clockpicker"),this.addon.off("click.clockpicker"),this.isShown&&this.hide(),this.isAppended&&(r.off("resize.clockpicker"+this.id),this.popover.remove())},n.fn.clockpicker=function(t){var i=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=n(this),o=e.data("clockpicker");if(o)"function"==typeof o[t]&&o[t].apply(o,i);else{var c=n.extend({},s.DEFAULTS,e.data(),"object"==typeof t&&t);e.data("clockpicker",new s(e,c))}})}}(); \ No newline at end of file +!function(){function t(t){return document.createElementNS(h,t)}function e(t){return(10>t?"0":"")+t}function i(t){var e=++g+"";return t?t+e:e}function s(s,n){function a(t,e){var i=u.offset(),s=/^touch/.test(t.type),o=i.left+b,c=i.top+b,a=(s?t.originalEvent.touches[0]:t).pageX-o,h=(s?t.originalEvent.touches[0]:t).pageY-c,d=Math.sqrt(a*a+h*h),m=!1;if(!e||!(w-M>d||d>w+M)){t.preventDefault();var v=setTimeout(function(){r.addClass("clockpicker-moving")},200);l&&u.append(x.canvas),x.setHand(a,h,!0),p.off(k).on(k,function(t){t.preventDefault();var e=/^touch/.test(t.type),i=(e?t.originalEvent.touches[0]:t).pageX-o,s=(e?t.originalEvent.touches[0]:t).pageY-c;(m||i!==a||s!==h)&&(m=!0,x.setHand(i,s,!0))}),p.off(f).on(f,function(t){p.off(f),t.preventDefault();var i=/^touch/.test(t.type),s=(i?t.originalEvent.changedTouches[0]:t).pageX-o,l=(i?t.originalEvent.changedTouches[0]:t).pageY-c;(e||m)&&s===a&&l===h&&x.setHand(s,l),"hours"===x.currentView?x.toggleView("minutes",A/2):n.autoclose&&(n.ampmSubmit||(x.minutesView.addClass("clockpicker-dial-out"),setTimeout(function(){x.done()},A/2))),u.prepend(N),clearTimeout(v),r.removeClass("clockpicker-moving"),p.off(k)})}}var h=c(P),u=h.find(".clockpicker-plate"),d=h.find(".clockpicker-hours"),v=h.find(".clockpicker-minutes"),g=h.find(".clockpicker-am-pm-block"),H="INPUT"===s.prop("tagName"),T=H?s:s.find("input"),V="time"===T.prop("type"),S=s.find(".input-group-addon"),x=this;this.id=i("cp"),this.element=s,this.options=n,this.options.hourstep=this.parseStep(this.options.hourstep,12),this.options.minutestep=this.parseStep(this.options.minutestep,60),this.isAppended=!1,this.isShown=!1,this.currentView="hours",this.isInput=H,this.isHTML5=V,this.input=T,this.addon=S,this.popover=h,this.plate=u,this.hoursView=d,this.minutesView=v,this.amPmBlock=g,this.spanHours=h.find(".clockpicker-span-hours"),this.spanMinutes=h.find(".clockpicker-span-minutes"),this.spanAmPm=h.find(".clockpicker-span-am-pm"),this.amOrPm="",this.currentPlacementClass=n.placement,this.raiseCallback=function(){o.apply(x,arguments)},n.twelvehour&&(c('').on("click",function(){x.amOrPm="AM",c(".clockpicker-span-am-pm").empty().append("AM"),n.ampmSubmit&&setTimeout(function(){x.done()},A/2)}).appendTo(this.amPmBlock),c('').on("click",function(){x.amOrPm="PM",c(".clockpicker-span-am-pm").empty().append("PM"),n.ampmSubmit&&setTimeout(function(){x.done()},A/2)}).appendTo(this.amPmBlock)),n.autoclose||c('").click(c.proxy(this.done,this)).appendTo(h),!/^(top|bottom)/.test(n.placement)||"top"!==n.align&&"bottom"!==n.align||(n.align="left"),"left"!==n.placement&&"right"!==n.placement||"left"!==n.align&&"right"!==n.align||(n.align="top"),h.addClass(n.placement),h.addClass("clockpicker-align-"+n.align),this.spanHours.click(c.proxy(this.toggleView,this,"hours")),this.spanMinutes.click(c.proxy(this.toggleView,this,"minutes")),n.addonOnly||T.on("focus.clockpicker click.clockpicker",c.proxy(this.show,this)),S.on("click.clockpicker",c.proxy(this.toggle,this));var D,I,O,E,B=c('
');if(n.twelvehour)for(D=0;12>D;D+=n.hourstep)I=B.clone(),O=D/6*Math.PI,E=w,I.css("font-size","120%"),I.css({left:b+Math.sin(O)*E-M,top:b-Math.cos(O)*E-M}),I.html(0===D?12:D),d.append(I),I.on(m,a);else for(D=0;24>D;D+=n.hourstep){I=B.clone(),O=D/6*Math.PI;var L=D>0&&13>D;E=L?y:w,I.css({left:b+Math.sin(O)*E-M,top:b-Math.cos(O)*E-M}),L&&I.css("font-size","120%"),I.html(0===D?"00":D),d.append(I),I.on(m,a)}var z=Math.max(n.minutestep,5);for(D=0;60>D;D+=z)I=B.clone(),O=D/30*Math.PI,I.css({left:b+Math.sin(O)*w-M,top:b-Math.cos(O)*w-M}),I.css("font-size","120%"),I.html(e(D)),v.append(I),I.on(m,a);if(u.on(m,function(t){0===c(t.target).closest(".clockpicker-tick").length&&a(t,!0)}),l){var N=h.find(".clockpicker-canvas"),U=t("svg");U.setAttribute("class","clockpicker-svg"),U.setAttribute("width",C),U.setAttribute("height",C);var W=t("g");W.setAttribute("transform","translate("+b+","+b+")");var j=t("circle");j.setAttribute("class","clockpicker-canvas-bearing"),j.setAttribute("cx",0),j.setAttribute("cy",0),j.setAttribute("r",2);var R=t("line");R.setAttribute("x1",0),R.setAttribute("y1",0);var X=t("circle");X.setAttribute("class","clockpicker-canvas-bg"),X.setAttribute("r",M);var Y=t("circle");Y.setAttribute("class","clockpicker-canvas-fg"),Y.setAttribute("r",3.5),W.appendChild(R),W.appendChild(X),W.appendChild(Y),W.appendChild(j),U.appendChild(W),N.append(U),this.hand=R,this.bg=X,this.fg=Y,this.bearing=j,this.g=W,this.canvas=N}this.raiseCallback(this.options.init,"init")}function o(t,e){if(t&&"function"==typeof t&&this.element){var i=this.getTime()||null;t.call(this.element,i)}e&&this.element.trigger("clockpicker."+e||"NoName")}function n(t,e,i){var s=e.outerHeight(),o=t.outerHeight(),n=t.offset().top,r=t.offset().top+o,c=n-t[0].getBoundingClientRect().top,a=c+document.documentElement.clientHeight,p=n-s>=c,h=a>=r+s;if("top"===i){if(p)return"top";if(h)return"bottom"}else{if(h)return"bottom";if(p)return"top"}return"viewport-top"}var r,c=window.jQuery,a=c(window),p=c(document),h="http://www.w3.org/2000/svg",l="SVGAngle"in window&&function(){var t,e=document.createElement("div");return e.innerHTML="",t=(e.firstChild&&e.firstChild.namespaceURI)==h,e.innerHTML="",t}(),u=function(){var t=document.createElement("div").style;return"transition"in t||"WebkitTransition"in t||"MozTransition"in t||"msTransition"in t||"OTransition"in t}(),d="ontouchstart"in window,m="mousedown"+(d?" touchstart":""),k="mousemove.clockpicker"+(d?" touchmove.clockpicker":""),f="mouseup.clockpicker"+(d?" touchend.clockpicker":""),v=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,g=0,b=100,w=80,y=54,M=13,C=2*b,A=u?350:1,P=['
','
','
',''," : ",'','',"
",'
','
','
','
','
',"
",'',"","
","
"].join("");s.prototype.parseStep=function(t,e){return e%t===0?t:1},s.DEFAULTS={"default":"",fromnow:0,placement:"bottom",align:"left",donetext:"完成",autoclose:!1,twelvehour:!1,vibrate:!0,hourstep:1,minutestep:1,ampmSubmit:!1,addonOnly:!1},s.prototype.toggle=function(){this[this.isShown?"hide":"show"]()},s.prototype.updatePlacementClass=function(t){this.currentPlacementClass&&this.popover.removeClass(this.currentPlacementClass),t&&this.popover.addClass(t),this.currentPlacementClass=t},s.prototype.locate=function(){var t=this.element,e=this.popover,i=t.offset(),s=t.outerWidth(),o=t.outerHeight(),r=this.options.placement,c=this.options.align,a={};if("top-adaptive"===r||"bottom-adaptive"===r){var p=r.substr(0,r.indexOf("-"));r=n(t,e,p),this.updatePlacementClass("viewport-top"!==r?r:"")}switch(e.show(),r){case"bottom":a.top=i.top+o;break;case"right":a.left=i.left+s;break;case"top":a.top=i.top-e.outerHeight();break;case"left":a.left=i.left-e.outerWidth();break;case"viewport-top":a.top=i.top-t[0].getBoundingClientRect().top}switch(c){case"left":a.left=i.left;break;case"right":a.left=i.left+s-e.outerWidth();break;case"top":a.top=i.top;break;case"bottom":a.top=i.top+o-e.outerHeight()}e.css(a)},s.prototype.parseInputValue=function(){var t=this.input.prop("value")||this.options["default"]||"";if("now"===t&&(t=new Date(+new Date+this.options.fromnow)),t instanceof Date&&(t=t.getHours()+":"+t.getMinutes()),t=t.split(":"),this.hours=+t[0]||0,this.minutes=+(t[1]+"").replace(/\D/g,"")||0,this.hours=Math.round(this.hours/this.options.hourstep)*this.options.hourstep,this.minutes=Math.round(this.minutes/this.options.minutestep)*this.options.minutestep,this.options.twelvehour){var e=(t[1]+"").replace(/\d+/g,"").toLowerCase();this.amOrPm=this.hours>12||"pm"===e?"PM":"AM"}},s.prototype.show=function(){if(!this.isShown){this.raiseCallback(this.options.beforeShow,"beforeShow");var t=this;this.isAppended||(r=c(document.body).append(this.popover),a.on("resize.clockpicker"+this.id,function(){t.isShown&&t.locate()}),this.isAppended=!0),this.parseInputValue(),this.spanHours.html(e(this.hours)),this.spanMinutes.html(e(this.minutes)),this.options.twelvehour&&this.spanAmPm.empty().append(this.amOrPm),this.toggleView("hours"),this.locate(),this.isShown=!0,p.on("click.clockpicker."+this.id+" focusin.clockpicker."+this.id,function(e){var i=c(e.target);0===i.closest(t.popover).length&&0===i.closest(t.addon).length&&0===i.closest(t.input).length&&t.hide()}),p.on("keyup.clockpicker."+this.id,function(e){27===e.keyCode&&t.hide()}),this.raiseCallback(this.options.afterShow,"afterShow")}},s.prototype.hide=function(){this.raiseCallback(this.options.beforeHide,"beforeHide"),this.isShown=!1,p.off("click.clockpicker."+this.id+" focusin.clockpicker."+this.id),p.off("keyup.clockpicker."+this.id),this.popover.hide(),this.raiseCallback(this.options.afterHide,"afterHide")},s.prototype.toggleView=function(t,e){var i=!1;"minutes"===t&&"visible"===c(this.hoursView).css("visibility")&&(this.raiseCallback(this.options.beforeHourSelect,"beforeHourSelect"),i=!0);var s="hours"===t,o=s?this.hoursView:this.minutesView,n=s?this.minutesView:this.hoursView;this.currentView=t,this.spanHours.toggleClass("text-primary",s),this.spanMinutes.toggleClass("text-primary",!s),n.addClass("clockpicker-dial-out"),o.css("visibility","visible").removeClass("clockpicker-dial-out"),this.resetClock(e),clearTimeout(this.toggleViewTimer),this.toggleViewTimer=setTimeout(function(){n.css("visibility","hidden")},A),i&&this.raiseCallback(this.options.afterHourSelect,"afterHourSelect")},s.prototype.resetClock=function(t){var e=this.currentView,i=this[e],s="hours"===e,o=Math.PI/(s?6:30),n=i*o,r=s&&i>0&&13>i?y:w,c=Math.sin(n)*r,a=-Math.cos(n)*r,p=this;l&&t?(p.canvas.addClass("clockpicker-canvas-out"),setTimeout(function(){p.canvas.removeClass("clockpicker-canvas-out"),p.setHand(c,a)},t)):this.setHand(c,a)},s.prototype.setHand=function(t,i,s){var o,n,r=Math.atan2(t,-i),a="hours"===this.currentView,p=Math.sqrt(t*t+i*i),h=this.options,u=a&&(w+y)/2>p,d=u?y:w;if(o=a?h.hourstep/6*Math.PI:h.minutestep/30*Math.PI,h.twelvehour&&(d=w),0>r&&(r=2*Math.PI+r),n=Math.round(r/o),r=n*o,a?(n*=h.hourstep,h.twelvehour||!u!=n>0||(n+=12),h.twelvehour&&0===n&&(n=12),24===n&&(n=0)):(n*=h.minutestep,60===n&&(n=0)),this[this.currentView]!==n&&v&&this.options.vibrate&&(this.vibrateTimer||(navigator[v](10),this.vibrateTimer=setTimeout(c.proxy(function(){this.vibrateTimer=null},this),100))),this[this.currentView]=n,this[a?"spanHours":"spanMinutes"].html(e(n)),!l)return this[a?"hoursView":"minutesView"].find(".clockpicker-tick").each(function(){var t=c(this);t.toggleClass("active",n===+t.html())}),void 0;s||!a&&n%5?(this.g.insertBefore(this.hand,this.bearing),this.g.insertBefore(this.bg,this.fg),this.bg.setAttribute("class","clockpicker-canvas-bg clockpicker-canvas-bg-trans")):(this.g.insertBefore(this.hand,this.bg),this.g.insertBefore(this.fg,this.bg),this.bg.setAttribute("class","clockpicker-canvas-bg"));var m=Math.sin(r)*d,k=-Math.cos(r)*d;this.hand.setAttribute("x2",m),this.hand.setAttribute("y2",k),this.bg.setAttribute("cx",m),this.bg.setAttribute("cy",k),this.fg.setAttribute("cx",m),this.fg.setAttribute("cy",k)},s.prototype.getTime=function(t){this.parseInputValue();var e=this.hours;this.options.twelvehour&&12>e&&"PM"===this.amOrPm&&(e+=12);var i=new Date;return i.setMinutes(this.minutes),i.setHours(e),i.setSeconds(0),t&&t.apply(this.element,i)||i},s.prototype.done=function(){this.raiseCallback(this.options.beforeDone,"beforeDone"),this.hide();var t=this.input.prop("value"),i=this.hours,s=":"+e(this.minutes);this.isHTML5&&this.options.twelvehour&&(this.hours<12&&"PM"===this.amOrPm&&(i+=12),12===this.hours&&"AM"===this.amOrPm&&(i=0)),s=e(i)+s,!this.isHTML5&&this.options.twelvehour&&(s+=this.amOrPm),this.input.prop("value",s),s!==t&&(this.input.triggerHandler("change"),this.isInput||this.element.trigger("change")),this.options.autoclose&&this.input.trigger("blur"),this.raiseCallback(this.options.afterDone,"afterDone")},s.prototype.remove=function(){this.element.removeData("clockpicker"),this.input.off("focus.clockpicker click.clockpicker"),this.addon.off("click.clockpicker"),this.isShown&&this.hide(),this.isAppended&&(a.off("resize.clockpicker"+this.id),this.popover.remove())},c.fn.clockpicker=function(t){function e(){var e=c(this),o=e.data("clockpicker");if(o){if("function"==typeof o[t])return o[t].apply(o,i)}else{var n=c.extend({},s.DEFAULTS,e.data(),"object"==typeof t&&t);e.data("clockpicker",new s(e,n))}}var i=Array.prototype.slice.call(arguments,1);if(1==this.length){var o=e.apply(this[0]);return void 0!==o?o:this}return this.each(e)}}(); \ No newline at end of file diff --git a/jquery.html b/jquery.html index 8a03ad8..80a5709 100644 --- a/jquery.html +++ b/jquery.html @@ -381,7 +381,8 @@

License

placement: 'bottom', align: 'right', autoclose: true, - 'default': '20:48' + 'default': '20:48', + minutestep: 5 }); $('.clockpicker-with-callbacks').clockpicker({ donetext: 'Done', diff --git a/src/clockpicker.js b/src/clockpicker.js index 7f208aa..dcf666c 100644 --- a/src/clockpicker.js +++ b/src/clockpicker.js @@ -95,6 +95,7 @@ amPmBlock = popover.find('.clockpicker-am-pm-block'), isInput = element.prop('tagName') === 'INPUT', input = isInput ? element : element.find('input'), + isHTML5 = input.prop('type') === 'time', addon = element.find('.input-group-addon'), self = this, timer; @@ -102,10 +103,13 @@ this.id = uniqueId('cp'); this.element = element; this.options = options; + this.options.hourstep = this.parseStep(this.options.hourstep, 12); + this.options.minutestep = this.parseStep(this.options.minutestep, 60); this.isAppended = false; this.isShown = false; this.currentView = 'hours'; this.isInput = isInput; + this.isHTML5 = isHTML5; this.input = input; this.addon = addon; this.popover = popover; @@ -116,49 +120,42 @@ this.spanHours = popover.find('.clockpicker-span-hours'); this.spanMinutes = popover.find('.clockpicker-span-minutes'); this.spanAmPm = popover.find('.clockpicker-span-am-pm'); - this.amOrPm = "PM"; - + this.amOrPm = ""; + this.currentPlacementClass = options.placement; + this.raiseCallback = function() { + raiseCallback.apply(self, arguments); + }; + // Setup for for 12 hour clock if option is selected if (options.twelvehour) { - - var amPmButtonsTemplate = ['
', - '', - '', - '
'].join(''); - - var amPmButtons = $(amPmButtonsTemplate); - //amPmButtons.appendTo(plate); - - ////Not working b/c they are not shown when this runs - //$('clockpicker-am-button') - // .on("click", function() { - // self.amOrPm = "AM"; - // $('.clockpicker-span-am-pm').empty().append('AM'); - // }); - // - //$('clockpicker-pm-button') - // .on("click", function() { - // self.amOrPm = "PM"; - // $('.clockpicker-span-am-pm').empty().append('PM'); - // }); - + $('') .on("click", function() { self.amOrPm = "AM"; $('.clockpicker-span-am-pm').empty().append('AM'); + + if (options.ampmSubmit) { + setTimeout(function(){ + self.done(); + }, duration / 2); + } }).appendTo(this.amPmBlock); - - + + $('') .on("click", function() { self.amOrPm = 'PM'; $('.clockpicker-span-am-pm').empty().append('PM'); + + if (options.ampmSubmit) { + setTimeout(function(){ + self.done(); + }, duration / 2); + } }).appendTo(this.amPmBlock); - + } - + if (! options.autoclose) { // If autoclose is not setted, append a button $('') @@ -167,7 +164,7 @@ } // Placement and arrow align - make sure they make sense. - if ((options.placement === 'top' || options.placement === 'bottom') && (options.align === 'top' || options.align === 'bottom')) options.align = 'left'; + if (/^(top|bottom)/.test(options.placement) && (options.align === 'top' || options.align === 'bottom')) options.align = 'left'; if ((options.placement === 'left' || options.placement === 'right') && (options.align === 'left' || options.align === 'right')) options.align = 'top'; popover.addClass(options.placement); @@ -177,7 +174,9 @@ this.spanMinutes.click($.proxy(this.toggleView, this, 'minutes')); // Show or toggle - input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this)); + if (!options.addonOnly) { + input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this)); + } addon.on('click.clockpicker', $.proxy(this.toggle, this)); // Build ticks @@ -186,7 +185,7 @@ // Hours view if (options.twelvehour) { - for (i = 1; i < 13; i += 1) { + for (i = 0; i < 12; i += options.hourstep) { tick = tickTpl.clone(); radian = i / 6 * Math.PI; radius = outerRadius; @@ -195,12 +194,12 @@ left: dialRadius + Math.sin(radian) * radius - tickRadius, top: dialRadius - Math.cos(radian) * radius - tickRadius }); - tick.html(i === 0 ? '00' : i); + tick.html(i === 0 ? 12 : i); hoursView.append(tick); tick.on(mousedownEvent, mousedown); } } else { - for (i = 0; i < 24; i += 1) { + for (i = 0; i < 24; i += options.hourstep) { tick = tickTpl.clone(); radian = i / 6 * Math.PI; var inner = i > 0 && i < 13; @@ -219,7 +218,8 @@ } // Minutes view - for (i = 0; i < 60; i += 5) { + var incrementValue = Math.max(options.minutestep, 5); + for (i = 0; i < 60; i += incrementValue) { tick = tickTpl.clone(); radian = i / 30 * Math.PI; tick.css({ @@ -267,7 +267,7 @@ } // Clock - self.setHand(dx, dy, ! space, true); + self.setHand(dx, dy, true); // Mousemove on document $doc.off(mousemoveEvent).on(mousemoveEvent, function(e){ @@ -280,7 +280,7 @@ return; } moved = true; - self.setHand(x, y, false, true); + self.setHand(x, y, true); }); // Mouseup on document @@ -297,10 +297,12 @@ self.toggleView('minutes', duration / 2); } else { if (options.autoclose) { - self.minutesView.addClass('clockpicker-dial-out'); - setTimeout(function(){ - self.done(); - }, duration / 2); + if (!options.ampmSubmit) { + self.minutesView.addClass('clockpicker-dial-out'); + setTimeout(function(){ + self.done(); + }, duration / 2); + } } } plate.prepend(canvas); @@ -352,25 +354,75 @@ this.canvas = canvas; } - raiseCallback(this.options.init); + this.raiseCallback(this.options.init, 'init'); + } + + function raiseCallback(callbackFunction, triggerName) { + if (callbackFunction && typeof callbackFunction === "function" && this.element) { + var time = this.getTime() || null; + callbackFunction.call(this.element, time); + } + if (triggerName) { + this.element.trigger('clockpicker.' + triggerName || 'NoName'); + } } - function raiseCallback(callbackFunction) { - if (callbackFunction && typeof callbackFunction === "function") { - callbackFunction(); + /** + * Find most suitable vertical placement, doing our best to ensure it is inside of the viewport. + * + * First try to place the element according with preferredPlacement, then try the opposite + * placement and as a last resort, popover will be placed on the very top of the viewport. + * + * @param {jQuery} element + * @param {jQuery} popover + * @param preferredPlacement Preferred placement, if there is enough room for it. + * @returns {string} One of: 'top', 'bottom' or 'viewport-top'. + */ + function resolveAdaptiveVerticalPlacement(element, popover, preferredPlacement) { + var popoverHeight = popover.outerHeight(), + elementHeight = element.outerHeight(), + elementTopOffset = element.offset().top, + elementBottomOffset = element.offset().top + elementHeight, + minVisibleY = elementTopOffset - element[0].getBoundingClientRect().top, + maxVisibleY = minVisibleY + document.documentElement.clientHeight, + isEnoughRoomAbove = (elementTopOffset - popoverHeight) >= minVisibleY, + isEnoughRoomBelow = (elementBottomOffset + popoverHeight) <= maxVisibleY; + + if (preferredPlacement === 'top') { + if (isEnoughRoomAbove) { + return 'top'; + } else if (isEnoughRoomBelow) { + return 'bottom'; + } + } else { + if (isEnoughRoomBelow) { + return 'bottom'; + } else if (isEnoughRoomAbove) { + return 'top'; + } } + + return 'viewport-top'; + } + + ClockPicker.prototype.parseStep = function(givenStepSize, wholeSize) { + return wholeSize % givenStepSize === 0 ? givenStepSize : 1; } // Default options ClockPicker.DEFAULTS = { - 'default': '', // default time, 'now' or '13:14' e.g. - fromnow: 0, // set default time to * milliseconds from now (using with default = 'now') - placement: 'bottom', // clock popover placement - align: 'left', // popover arrow align - donetext: '完成', // done button text - autoclose: false, // auto close when minute is selected - twelvehour: false, // change to 12 hour AM/PM clock from 24 hour - vibrate: true // vibrate the device when dragging clock hand + 'default': '', // default time, 'now' or '13:14' e.g. + fromnow: 0, // set default time to * milliseconds from now (using with default = 'now') + placement: 'bottom', // clock popover placement + align: 'left', // popover arrow align + donetext: '完成', // done button text + autoclose: false, // auto close when minute is selected + twelvehour: false, // change to 12 hour AM/PM clock from 24 hour + vibrate: true, // vibrate the device when dragging clock hand + hourstep: 1, // allow to multi increment the hour + minutestep: 1, // allow to multi increment the minute + ampmSubmit: false, // allow submit with AM and PM buttons instead of the minute selection/picker + addonOnly: false // only open on clicking on the input-addon }; // Show or hide popover @@ -378,7 +430,19 @@ this[this.isShown ? 'hide' : 'show'](); }; - // Set popover position + // Set new placement class for popover and remove the old one, if any. + ClockPicker.prototype.updatePlacementClass = function(newClass) { + if (this.currentPlacementClass) { + this.popover.removeClass(this.currentPlacementClass); + } + if (newClass) { + this.popover.addClass(newClass); + } + + this.currentPlacementClass = newClass; + }; + + // Set popover position and update placement class, if needed ClockPicker.prototype.locate = function(){ var element = this.element, popover = this.popover, @@ -390,6 +454,15 @@ styles = {}, self = this; + if (placement === 'top-adaptive' || placement === 'bottom-adaptive') { + var preferredPlacement = placement.substr(0, placement.indexOf('-')); + // Adaptive placement should be resolved into one of the "static" placement + // options, that is best suitable for the current window scroll position. + placement = resolveAdaptiveVerticalPlacement(element, popover, preferredPlacement); + + this.updatePlacementClass(placement !== 'viewport-top' ? placement : ''); + } + popover.show(); // Place the popover @@ -406,6 +479,9 @@ case 'left': styles.left = offset.left - popover.outerWidth(); break; + case 'viewport-top': + styles.top = offset.top - element[0].getBoundingClientRect().top; + break; } // Align the popover arrow @@ -427,6 +503,33 @@ popover.css(styles); }; + // The input can be changed by the user + // So before we can use this.hours/this.minutes we must update it + ClockPicker.prototype.parseInputValue = function(){ + var value = this.input.prop('value') || this.options['default'] || ''; + + if (value === 'now') { + value = new Date(+ new Date() + this.options.fromnow); + } + if (value instanceof Date) { + value = value.getHours() + ':' + value.getMinutes(); + } + + value = value.split(':'); + + // Minutes can have AM/PM that needs to be removed + this.hours = + value[0] || 0; + this.minutes = + (value[1] + '').replace(/\D/g, '') || 0; + + this.hours = Math.round(this.hours / this.options.hourstep) * this.options.hourstep; + this.minutes = Math.round(this.minutes / this.options.minutestep) * this.options.minutestep; + + if (this.options.twelvehour) { + var period = (value[1] + '').replace(/\d+/g, '').toLowerCase(); + this.amOrPm = this.hours > 12 || period === 'pm' ? 'PM' : 'AM'; + } + }; + // Show popover ClockPicker.prototype.show = function(e){ // Not show again @@ -434,7 +537,7 @@ return; } - raiseCallback(this.options.beforeShow); + this.raiseCallback(this.options.beforeShow, 'beforeShow'); var self = this; @@ -453,20 +556,16 @@ this.isAppended = true; } - // Get the time - var value = ((this.input.prop('value') || this.options['default'] || '') + '').split(':'); - if (value[0] === 'now') { - var now = new Date(+ new Date() + this.options.fromnow); - value = [ - now.getHours(), - now.getMinutes() - ]; - } - this.hours = + value[0] || 0; - this.minutes = + value[1] || 0; + // Get the time from the input field + this.parseInputValue(); + this.spanHours.html(leadingZero(this.hours)); this.spanMinutes.html(leadingZero(this.minutes)); + if (this.options.twelvehour) { + this.spanAmPm.empty().append(this.amOrPm); + } + // Toggle to hours view this.toggleView('hours'); @@ -492,12 +591,12 @@ } }); - raiseCallback(this.options.afterShow); + this.raiseCallback(this.options.afterShow, 'afterShow'); }; // Hide popover ClockPicker.prototype.hide = function(){ - raiseCallback(this.options.beforeHide); + this.raiseCallback(this.options.beforeHide, 'beforeHide'); this.isShown = false; @@ -507,14 +606,14 @@ this.popover.hide(); - raiseCallback(this.options.afterHide); + this.raiseCallback(this.options.afterHide, 'afterHide'); }; // Toggle to hours or minutes view ClockPicker.prototype.toggleView = function(view, delay){ var raiseAfterHourSelect = false; if (view === 'minutes' && $(this.hoursView).css("visibility") === "visible") { - raiseCallback(this.options.beforeHourSelect); + this.raiseCallback(this.options.beforeHourSelect, 'beforeHourSelect'); raiseAfterHourSelect = true; } var isHours = view === 'hours', @@ -540,7 +639,7 @@ }, duration); if (raiseAfterHourSelect) { - raiseCallback(this.options.afterHourSelect); + this.raiseCallback(this.options.afterHourSelect, 'afterHourSelect'); } }; @@ -567,19 +666,26 @@ }; // Set clock hand to (x, y) - ClockPicker.prototype.setHand = function(x, y, roundBy5, dragging){ + ClockPicker.prototype.setHand = function(x, y, dragging){ var radian = Math.atan2(x, - y), isHours = this.currentView === 'hours', - unit = Math.PI / (isHours || roundBy5 ? 6 : 30), z = Math.sqrt(x * x + y * y), options = this.options, inner = isHours && z < (outerRadius + innerRadius) / 2, radius = inner ? innerRadius : outerRadius, + unit, value; - - if (options.twelvehour) { - radius = outerRadius; - } + + // Calculate the unit + if (isHours) { + unit = options.hourstep / 6 * Math.PI + } else { + unit = options.minutestep / 30 * Math.PI + } + + if (options.twelvehour) { + radius = outerRadius; + } // Radian should in range [0, 2PI] if (radian < 0) { @@ -593,35 +699,25 @@ radian = value * unit; // Correct the hours or minutes - if (options.twelvehour) { - if (isHours) { - if (value === 0) { - value = 12; - } - } else { - if (roundBy5) { - value *= 5; - } - if (value === 60) { - value = 0; - } + if (isHours) { + value *= options.hourstep; + + if (! options.twelvehour && (!inner)==(value>0)) { + value += 12; + } + if (options.twelvehour && value === 0) { + value = 12; + } + if (value === 24) { + value = 0; } } else { - if (isHours) { - if (value === 12) { - value = 0; - } - value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12; - } else { - if (roundBy5) { - value *= 5; - } - if (value === 60) { - value = 0; - } + value *= options.minutestep; + if (value === 60) { + value = 0; } } - + // Once hours or minutes changed, vibrate the device if (this[this.currentView] !== value) { if (vibrate && this.options.vibrate) { @@ -670,19 +766,49 @@ this.fg.setAttribute('cy', cy); }; + // Allow user to get time time as Date object + ClockPicker.prototype.getTime = function(callback) { + this.parseInputValue(); + + var hours = this.hours; + if (this.options.twelvehour && hours < 12 && this.amOrPm === 'PM') { + hours += 12; + } + + var selectedTime = new Date(); + selectedTime.setMinutes(this.minutes) + selectedTime.setHours(hours); + selectedTime.setSeconds(0); + + return callback && callback.apply(this.element, selectedTime) || selectedTime; + } + // Hours and minutes are selected ClockPicker.prototype.done = function() { - raiseCallback(this.options.beforeDone); + this.raiseCallback(this.options.beforeDone, 'beforeDone'); this.hide(); var last = this.input.prop('value'), - value = leadingZero(this.hours) + ':' + leadingZero(this.minutes); - if (this.options.twelvehour) { + outHours = this.hours, + value = ':' + leadingZero(this.minutes); + + if (this.isHTML5 && this.options.twelvehour) { + if (this.hours < 12 && this.amOrPm === 'PM') { + outHours += 12; + } + if (this.hours === 12 && this.amOrPm === 'AM') { + outHours = 0; + } + } + + value = leadingZero(outHours) + value; + + if (!this.isHTML5 && this.options.twelvehour) { value = value + this.amOrPm; } - + this.input.prop('value', value); if (value !== last) { - this.input.triggerHandler('change'); + this.input.trigger('change'); if (! this.isInput) { this.element.trigger('change'); } @@ -692,7 +818,7 @@ this.input.trigger('blur'); } - raiseCallback(this.options.afterDone); + this.raiseCallback(this.options.afterDone, 'afterDone'); }; // Remove clockpicker from input @@ -712,18 +838,31 @@ // Extends $.fn.clockpicker $.fn.clockpicker = function(option){ var args = Array.prototype.slice.call(arguments, 1); - return this.each(function(){ + + function handleClockPickerRequest() { var $this = $(this), data = $this.data('clockpicker'); if (! data) { var options = $.extend({}, ClockPicker.DEFAULTS, $this.data(), typeof option == 'object' && option); $this.data('clockpicker', new ClockPicker($this, options)); } else { - // Manual operatsions. show, hide, remove, e.g. + // Manual operations. show, hide, remove, getTime, e.g. if (typeof data[option] === 'function') { - data[option].apply(data, args); + return data[option].apply(data, args); } } - }); + } + + // If we explicitly do a call on a single element then we can return the value (if needed) + // This allows us, for example, to return the value of getTime + if (this.length == 1) { + var returnValue = handleClockPickerRequest.apply(this[0]); + + // If we do not have any return value then return the object itself so you can chain + return returnValue !== undefined ? returnValue : this; + } + + // If we do have a list then we do not care about return values + return this.each(handleClockPickerRequest); }; }());