<!DOCTYPE html>
<html ng-app="plunker">
<head>
<script data-require="moment.js@2.1.0" data-semver="2.1.0" src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.1.0/moment.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
<script src="position.js"></script>
<script src="timepicker.js"></script>
<script src="timepicker-tpl.js"></script>
<script src="psTimePicker.js"></script>
<script src="example.js"></script>
</head>
<body>
<div ng-controller="mainController">
<p class="input-group">
<input ps-input-time sy-timepicker-popup="HH:mm:ss" class="form-control" ng-model="date" show-meridian="false" is-open="opened" />
<span class="input-group-btn">
<button class="btn btn-default" ng-click="open($event)">
<i class="glyphicon glyphicon-time"></i>
</button>
</span>
</p>
<pre class="alert alert-info">Time is: {{date | date:'HH:mm:ss' }}</pre>
</div>
<p>https://github.com/randallmeeker/ps-input-time</p>
</body>
</html>
angular.module('plunker', [
'sy.bootstrap.timepicker',
'template/syTimepicker/timepicker.html',
'template/syTimepicker/popup.html',
'ps.inputTime'
])
.controller('mainController', function($scope) {
$scope.date = new Date();
$scope.open = function($event) {
$event.preventDefault();
$event.stopPropagation();
$scope.opened = true;
};
});
angular.module('sy.bootstrap.timepicker',
['ui.bootstrap.position'])
.constant('syTimepickerConfig', {
hourStep: 1,
minuteStep: 1,
secondStep: 1,
showMeridian: true,
showSeconds: true,
meridians: null,
readonlyInput: false,
mousewheel: true
})
.controller('syTimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'syTimepickerConfig',
function($scope, $attrs, $parse, $log, $locale, syTimepickerConfig) {
var selected = new Date(),
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : syTimepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS;
$scope.showSeconds = getValue($attrs.showSeconds, syTimepickerConfig.showSeconds);
function getValue(value, defaultValue) {
return angular.isDefined(value) ? $scope.$parent.$eval(value) : defaultValue;
}
this.init = function( ngModelCtrl_, inputs ) {
ngModelCtrl = ngModelCtrl_;
ngModelCtrl.$render = this.render;
var hoursInputEl = inputs.eq(0),
minutesInputEl = inputs.eq(1),
secondsInputEl = inputs.eq(2);
var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : syTimepickerConfig.mousewheel;
if ( mousewheel ) {
this.setupMousewheelEvents( hoursInputEl, minutesInputEl, secondsInputEl );
}
$scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? scope.$parent.$eval($attrs.readonlyInput) : syTimepickerConfig.readonlyInput;
this.setupInputEvents( hoursInputEl, minutesInputEl, secondsInputEl );
};
var hourStep = syTimepickerConfig.hourStep;
if ($attrs.hourStep) {
$scope.$parent.$watch($parse($attrs.hourStep), function(value) {
hourStep = parseInt(value, 10);
});
}
var minuteStep = syTimepickerConfig.minuteStep;
if ($attrs.minuteStep) {
$scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
minuteStep = parseInt(value, 10);
});
}
var secondStep = syTimepickerConfig.secondStep;
if ($attrs.secondStep) {
$scope.$parent.$watch($parse($attrs.secondStep), function(value) {
secondStep = parseInt(value, 10);
});
}
// 12H / 24H mode
$scope.showMeridian = syTimepickerConfig.showMeridian;
if ($attrs.showMeridian) {
$scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
$scope.showMeridian = !!value;
if ( ngModelCtrl.$error.time ) {
// Evaluate from template
var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
if (angular.isDefined( hours ) && angular.isDefined( minutes )) {
selected.setHours( hours );
refresh();
}
} else {
updateTemplate();
}
});
}
// Get $scope.hours in 24H mode if valid
function getHoursFromTemplate ( ) {
var hours = parseInt( $scope.hours, 10 );
var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
if ( !valid ) {
return undefined;
}
if ( $scope.showMeridian ) {
if ( hours === 12 ) {
hours = 0;
}
if ( $scope.meridian === meridians[1] ) {
hours = hours + 12;
}
}
return hours;
}
function getMinutesFromTemplate() {
var minutes = parseInt($scope.minutes, 10);
return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined;
}
function getSecondsFromTemplate() {
var seconds = parseInt($scope.seconds, 10);
return ( seconds >= 0 && seconds < 60 ) ? seconds : undefined;
}
function pad( value ) {
return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value;
}
// Respond on mousewheel spin
this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl , secondsInputEl ) {
var isScrollingUp = function(e) {
if (e.originalEvent) {
e = e.originalEvent;
}
//pick correct delta variable depending on event
var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
return (e.detail || delta > 0);
};
hoursInputEl.bind('mousewheel wheel', function(e) {
$scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() );
e.preventDefault();
});
minutesInputEl.bind('mousewheel wheel', function(e) {
$scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() );
e.preventDefault();
});
secondsInputEl.bind('mousewheel wheel', function(e) {
$scope.$apply( (isScrollingUp(e)) ? $scope.incrementSeconds() : $scope.decrementSeconds() );
e.preventDefault();
});
};
this.setupInputEvents = function( hoursInputEl, minutesInputEl, secondsInputEl ) {
if ( $scope.readonlyInput ) {
$scope.updateHours = angular.noop;
$scope.updateMinutes = angular.noop;
$scope.updateSeconds = angular.noop;
return;
}
var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) {
ngModelCtrl.$setViewValue( null );
ngModelCtrl.$setValidity('time', false);
if (angular.isDefined(invalidHours)) {
$scope.invalidHours = invalidHours;
}
if (angular.isDefined(invalidMinutes)) {
$scope.invalidMinutes = invalidMinutes;
}
if (angular.isDefined(invalidSeconds)) {
$scope.invalidSeconds = invalidSeconds;
}
};
$scope.updateHours = function() {
var hours = getHoursFromTemplate();
if ( angular.isDefined(hours) ) {
selected.setHours( hours );
refresh( 'h' );
} else {
invalidate(true);
}
};
hoursInputEl.bind('blur', function(e) {
if ( !$scope.validHours && $scope.hours < 10) {
$scope.$apply( function() {
$scope.hours = pad( $scope.hours );
});
}
});
$scope.updateMinutes = function() {
var minutes = getMinutesFromTemplate();
if ( angular.isDefined(minutes) ) {
selected.setMinutes( minutes );
refresh( 'm' );
} else {
invalidate(undefined, true);
}
};
minutesInputEl.bind('blur', function(e) {
if ( !$scope.invalidMinutes && $scope.minutes < 10 ) {
$scope.$apply( function() {
$scope.minutes = pad( $scope.minutes );
});
}
});
$scope.updateSeconds = function() {
var seconds = getSecondsFromTemplate();
if ( angular.isDefined(seconds) ) {
selected.setSeconds( seconds );
refresh( 's' );
} else {
invalidate(undefined, true);
}
};
secondsInputEl.bind('blur', function(e) {
if ( !$scope.invalidSeconds && $scope.seconds < 10 ) {
$scope.$apply( function() {
$scope.seconds = pad( $scope.seconds );
});
}
});
};
this.render = function() {
var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null;
if ( isNaN(date) ) {
ngModelCtrl.$setValidity('time', false);
$log.error('syTimepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
} else {
if ( date ) {
selected = date;
}
makeValid();
updateTemplate();
}
};
// Call internally when we know that model is valid.
function refresh( keyboardChange ) {
makeValid();
ngModelCtrl.$setViewValue( new Date(selected) );
updateTemplate( keyboardChange );
}
function makeValid() {
ngModelCtrl.$setValidity('time', true);
$scope.invalidHours = false;
$scope.invalidMinutes = false;
$scope.invalidSeconds = false;
}
function updateTemplate( keyboardChange ) {
var hours = selected.getHours(), minutes = selected.getMinutes(), seconds = selected.getSeconds();
if ( $scope.showMeridian ) {
hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system
}
$scope.hours = keyboardChange === 'h' ? hours : pad(hours);
$scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes);
$scope.seconds = keyboardChange === 's' ? seconds : pad(seconds);
$scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
}
function addMinutes( minutes ) {
var dt = new Date( selected.getTime() + minutes * 60000 );
selected.setHours( dt.getHours(), dt.getMinutes() );
refresh();
}
function addSeconds( seconds ) {
var dt = new Date( selected.getTime() + seconds * 1000 );
selected.setHours( dt.getHours(), dt.getMinutes(), dt.getSeconds());
refresh();
}
$scope.incrementHours = function() {
addMinutes( hourStep * 60);
};
$scope.decrementHours = function() {
addMinutes( - hourStep * 60);
};
$scope.incrementMinutes = function() {
addMinutes( minuteStep);
};
$scope.decrementMinutes = function() {
addMinutes( - minuteStep);
};
$scope.incrementSeconds = function() {
addSeconds( secondStep );
};
$scope.decrementSeconds = function() {
addSeconds( - secondStep );
};
$scope.toggleMeridian = function() {
addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) );
};
}])
.directive('syTimepicker', function () {
return {
restrict: 'EA',
require: ['syTimepicker', '?^ngModel'],
controller:'syTimepickerController',
replace: true,
scope: {},
templateUrl: 'template/syTimepicker/timepicker.html',
link: function(sscope, element, attrs, ctrls) {
var syTimepickerCtrl = ctrls[0], ngModel = ctrls[1];
if ( ngModel ) {
syTimepickerCtrl.init( ngModel, element.find('input') );
}
}
};
})
.constant('syTimepickerPopupConfig', {
timeFormat: 'HH:mm:ss',
appendToBody: false
})
.directive('syTimepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'syTimepickerPopupConfig', 'syTimepickerConfig',
function ($compile, $parse, $document, $position, dateFilter, syTimepickerPopupConfig, syTimepickerConfig) {
return {
restrict: 'EA',
require: 'ngModel',
priority: 1,
link: function(originalScope, element, attrs, ngModel) {
var scope = originalScope.$new(), // create a child scope so we are not polluting original one
timeFormat,
appendToBody = angular.isDefined(attrs.syTimepickerAppendToBody) ? originalScope.$eval(attrs.syTimepickerAppendToBody) : syTimepickerPopupConfig.appendToBody;
attrs.$observe('syTimepickerPopup', function(value) {
timeFormat = value || syTimepickerPopupConfig.timeFormat;
ngModel.$render();
});
originalScope.$on('$destroy', function() {
$popup.remove();
scope.$destroy();
});
var getIsOpen, setIsOpen;
if ( attrs.isOpen ) {
getIsOpen = $parse(attrs.isOpen);
setIsOpen = getIsOpen.assign;
originalScope.$watch(getIsOpen, function updateOpen(value) {
scope.isOpen = !! value;
});
}
scope.isOpen = getIsOpen ? getIsOpen(originalScope) : false; // Initial state
function setOpen( value ) {
if (setIsOpen) {
setIsOpen(originalScope, !!value);
} else {
scope.isOpen = !!value;
}
}
var documentClickBind = function(event) {
if (scope.isOpen && event.target !== element[0]) {
scope.$apply(function() {
setOpen(false);
});
}
};
var elementFocusBind = function() {
scope.$apply(function() {
setOpen( true );
});
};
// popup element used to display calendar
var popupEl = angular.element('<div sy-timepicker-popup-wrap><div sy-timepicker></div></div>');
popupEl.attr({
'ng-model': 'date',
'ng-change': 'dateSelection()'
});
var syTimepickerEl = angular.element(popupEl.children()[0]),
syTimepickerOptions = {};
if (attrs.syTimepickerOptions) {
syTimepickerOptions = originalScope.$eval(attrs.syTimepickerOptions);
syTimepickerEl.attr(angular.extend({}, syTimepickerOptions));
}
function parseTime(viewValue) {
if (!viewValue) {
ngModel.$setValidity('time', true);
return null;
} else if (angular.isDate(viewValue)) {
ngModel.$setValidity('time', true);
return viewValue;
} else if (angular.isString(viewValue)) {
var date = new moment('1970-01-01 ' + viewValue, 'YYY-MM-DD ' + timeFormat);
if (!date.isValid()) {
ngModel.$setValidity('time', false);
return undefined;
} else {
ngModel.$setValidity('time', true);
return date.toDate();
}
} else {
ngModel.$setValidity('time', false);
return undefined;
}
}
ngModel.$parsers.unshift(parseTime);
// Inner change
scope.dateSelection = function(dt) {
if (angular.isDefined(dt)) {
scope.date = dt;
}
ngModel.$setViewValue(scope.date);
ngModel.$render();
};
element.bind('input change keyup', function() {
scope.$apply(function() {
scope.date = ngModel.$modelValue;
});
});
// Outter change
ngModel.$render = function() {
var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, timeFormat) : '';
element.val(date);
scope.date = ngModel.$modelValue;
};
function addWatchableAttribute(attribute, scopeProperty, syTimepickerAttribute) {
if (attribute) {
originalScope.$watch($parse(attribute), function(value){
scope[scopeProperty] = value;
});
syTimepickerEl.attr(syTimepickerAttribute || scopeProperty, scopeProperty);
}
}
if (attrs.showMeridian) {
syTimepickerEl.attr('show-meridian', attrs.showMeridian);
}
if (attrs.showSeconds) {
syTimepickerEl.attr('show-seconds', attrs.showSeconds);
}
function updatePosition() {
scope.position = appendToBody ? $position.offset(element) : $position.position(element);
scope.position.top = scope.position.top + element.prop('offsetHeight');
}
var documentBindingInitialized = false, elementFocusInitialized = false;
scope.$watch('isOpen', function(value) {
if (value) {
updatePosition();
$document.bind('click', documentClickBind);
if(elementFocusInitialized) {
element.unbind('focus', elementFocusBind);
}
element[0].focus();
documentBindingInitialized = true;
} else {
if(documentBindingInitialized) {
$document.unbind('click', documentClickBind);
}
element.bind('focus', elementFocusBind);
elementFocusInitialized = true;
}
if ( setIsOpen ) {
setIsOpen(originalScope, value);
}
});
var $popup = $compile(popupEl)(scope);
if ( appendToBody ) {
$document.find('body').append($popup);
} else {
element.after($popup);
}
}
};
}])
.directive('syTimepickerPopupWrap', function() {
return {
restrict:'EA',
replace: true,
transclude: true,
templateUrl: 'template/syTimepicker/popup.html',
link:function (scope, element, attrs) {
element.bind('click', function(event) {
event.preventDefault();
event.stopPropagation();
});
}
};
});
angular.module("template/syTimepicker/timepicker.html", []).run(["$templateCache",
function($templateCache) {
$templateCache.put("template/syTimepicker/timepicker.html",
"<table>\n" +
" <tbody>\n" +
" <tr class=\"text-center\">\n" +
" <td><a ng-click=\"incrementHours()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" +
" <td> </td>\n" +
" <td><a ng-click=\"incrementMinutes()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" +
" <td ng-show=\"showSeconds\"> </td>\n" +
" <td ng-show=\"showSeconds\"><a ng-click=\"incrementSeconds()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" +
" <td ng-show=\"showMeridian\"></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td style=\"width:50px;\" class=\"form-group\" ng-class=\"{'has-error': invalidHours}\">\n" +
" <input type=\"text\" ng-model=\"hours\" ng-change=\"updateHours()\" class=\"form-control text-center\" ng-mousewheel=\"incrementHours()\" ng-readonly=\"readonlyInput\" maxlength=\"2\">\n" +
" </td>\n" +
" <td>:</td>\n" +
" <td style=\"width:50px;\" class=\"form-group\" ng-class=\"{'has-error': invalidMinutes}\">\n" +
" <input type=\"text\" ng-model=\"minutes\" ng-change=\"updateMinutes()\" class=\"form-control text-center\" ng-readonly=\"readonlyInput\" maxlength=\"2\">\n" +
" </td>\n" +
" <td ng-show=\"showSeconds\">:</td>\n" +
" <td ng-show=\"showSeconds\" style=\"width:50px;\" class=\"form-group\" ng-class=\"{'has-error': invalidSeconds}\" ng-show=\"showSeconds\">\n" +
" <input type=\"text\" ng-model=\"seconds\" ng-change=\"updateSeconds()\" class=\"form-control text-center\" ng-readonly=\"readonlyInput\" maxlength=\"2\">\n" +
" </td>\n" +
" <td ng-show=\"showMeridian\"><button type=\"button\" class=\"btn btn-default text-center\" ng-click=\"toggleMeridian()\">{{meridian}}</button></td>\n" +
" </tr>\n" +
" <tr class=\"text-center\">\n" +
" <td><a ng-click=\"decrementHours()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" +
" <td> </td>\n" +
" <td><a ng-click=\"decrementMinutes()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" +
" <td ng-show=\"showSeconds\"> </td>\n" +
" <td ng-show=\"showSeconds\"><a ng-click=\"decrementSeconds()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" +
" <td ng-show=\"showMeridian\"></td>\n" +
" </tr>\n" +
" </tbody>\n" +
"</table>\n" +
"");
}
]);
angular.module("template/syTimepicker/popup.html", []).run(["$templateCache",
function($templateCache) {
$templateCache.put("template/syTimepicker/popup.html",
"<ul class=\"dropdown-menu\" ng-style=\"{display: (isOpen && 'block') || 'none', top: position.top+'px', left: position.left+'px'}\" style=\"min-width:0px;\">\n" +
" <li ng-transclude></li>\n" +
"</ul>\n" +
"");
}
]);
angular.module('ui.bootstrap.position', [])
/**
* A set of utility methods that can be use to retrieve position of DOM elements.
* It is meant to be used where we need to absolute-position DOM elements in
* relation to other, existing elements (this is the case for tooltips, popovers,
* typeahead suggestions etc.).
*/
.factory('$position', ['$document', '$window', function ($document, $window) {
function getStyle(el, cssprop) {
if (el.currentStyle) { //IE
return el.currentStyle[cssprop];
} else if ($window.getComputedStyle) {
return $window.getComputedStyle(el)[cssprop];
}
// finally try and get inline style
return el.style[cssprop];
}
/**
* Checks if a given element is statically positioned
* @param element - raw DOM element
*/
function isStaticPositioned(element) {
return (getStyle(element, 'position') || 'static' ) === 'static';
}
/**
* returns the closest, non-statically positioned parentOffset of a given element
* @param element
*/
var parentOffsetEl = function (element) {
var docDomEl = $document[0];
var offsetParent = element.offsetParent || docDomEl;
while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
offsetParent = offsetParent.offsetParent;
}
return offsetParent || docDomEl;
};
return {
/**
* Provides read-only equivalent of jQuery's position function:
* http://api.jquery.com/position/
*/
position: function (element) {
var elBCR = this.offset(element);
var offsetParentBCR = { top: 0, left: 0 };
var offsetParentEl = parentOffsetEl(element[0]);
if (offsetParentEl != $document[0]) {
offsetParentBCR = this.offset(angular.element(offsetParentEl));
offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
}
var boundingClientRect = element[0].getBoundingClientRect();
return {
width: boundingClientRect.width || element.prop('offsetWidth'),
height: boundingClientRect.height || element.prop('offsetHeight'),
top: elBCR.top - offsetParentBCR.top,
left: elBCR.left - offsetParentBCR.left
};
},
/**
* Provides read-only equivalent of jQuery's offset function:
* http://api.jquery.com/offset/
*/
offset: function (element) {
var boundingClientRect = element[0].getBoundingClientRect();
return {
width: boundingClientRect.width || element.prop('offsetWidth'),
height: boundingClientRect.height || element.prop('offsetHeight'),
top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
};
},
/**
* Provides coordinates for the targetEl in relation to hostEl
*/
positionElements: function (hostEl, targetEl, positionStr, appendToBody) {
var positionStrParts = positionStr.split('-');
var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
var hostElPos,
targetElWidth,
targetElHeight,
targetElPos;
hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
targetElWidth = targetEl.prop('offsetWidth');
targetElHeight = targetEl.prop('offsetHeight');
var shiftWidth = {
center: function () {
return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
},
left: function () {
return hostElPos.left;
},
right: function () {
return hostElPos.left + hostElPos.width;
}
};
var shiftHeight = {
center: function () {
return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
},
top: function () {
return hostElPos.top;
},
bottom: function () {
return hostElPos.top + hostElPos.height;
}
};
switch (pos0) {
case 'right':
targetElPos = {
top: shiftHeight[pos1](),
left: shiftWidth[pos0]()
};
break;
case 'left':
targetElPos = {
top: shiftHeight[pos1](),
left: hostElPos.left - targetElWidth
};
break;
case 'bottom':
targetElPos = {
top: shiftHeight[pos0](),
left: shiftWidth[pos1]()
};
break;
default:
targetElPos = {
top: hostElPos.top - targetElHeight,
left: shiftWidth[pos1]()
};
break;
}
return targetElPos;
}
};
}]);
angular.module('ps.inputTime', [])
.value('psInputTimeConfig', {
minuteStep : 5,
minDate : null,
maxDate : null,
fixedDay: true,
format: 'hh:mma'
})
.directive("psInputTime", ['$filter', 'psInputTimeConfig', '$parse', function($filter, psInputTimeConfig, $parse) {
var temp12hr = '((0?[0-9])|(1[0-2]))(:|\s)([0-5][0-9])[ap]m',
temp24hr = '([01]?[0-9]|2[0-3])[:;][0-5][0-9]',
temp24noColon = '(2[0-3]|[01]?[0-9])([0-5][0-9])';
var customFloor = function(value, roundTo) {
return Math.floor(value / psInputConfig.minuteStep) * psInputConfig.minuteStep;
};
var timeTest12hr = new RegExp('^' + temp12hr + '$', ['i']),
timeTest24hr = new RegExp('^' + temp24hr + '$', ['i']),
timeTest24noColon = new RegExp('^' + temp24noColon + '$', ['i']);
return {
restrict: "A",
require: '?^ngModel',
scope: {},
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
ngModel.$render = function () {
};
var minuteStep = getValue(attrs.minuteStep, psInputTimeConfig.minuteStep),
fixedDay = getValue(attrs.fixedDay, psInputTimeConfig.fixedDay),
timeFormat = attrs.format || psInputTimeConfig.format,
maxDate = null,
minDate = null;
function getValue(value, defaultValue) {
return angular.isDefined(value) ? scope.$parent.$eval(value) : defaultValue;
}
if(attrs.min || attrs.max){
fixedDay = false;
}
function checkMinMaxValid(){
if(minDate !== null && ngModel.$modelValue < minDate){
ngModel.$setValidity('time-min', false);
}else if (minDate !== null) ngModel.$setValidity('time-min', true);
if(maxDate !== null && ngModel.$modelValue > maxDate){
ngModel.$setValidity('time-max', false);
} else if (maxDate !== null) ngModel.$setValidity('time-max', true);
}
if (attrs.max) {
scope.$parent.$watch($parse(attrs.max), function(value) {
maxDate = value ? new Date(value) : null;
checkMinMaxValid();
});
}
if (attrs.min) {
scope.$parent.$watch($parse(attrs.min), function(value) {
minDate = value ? new Date(value) : null;
checkMinMaxValid();
});
}
var reservedKey = false;
element.on('keydown', function(e) {
reservedKey = false;
switch (e.keyCode) {
case 37:
// left button hit
if(verifyFormat()){
tabBackward(e);
reservedKey = true;
}
break;
case 38:
// up button hit
if(verifyFormat()) {
addTime();
reservedKey = true;
}
break;
case 39:
// right button hit
if(verifyFormat()){
tabForward(e);
reservedKey = true;
}
break;
case 40:
// down button hit
if(verifyFormat()) {
subtractTime();
reservedKey = true;
}
break;
case 9:
// TAB
if(verifyFormat()){
if(e.shiftKey){
if(getSelectionPoint() != 'hour') {
reservedKey = true;
tabBackward(e);
}
} else{
if(getSelectionPoint() != 'meridian') {
reservedKey = true;
tabForward(e);
}
}
}
break;
default:
// e.preventDefault();
break;
}
if(reservedKey){
e.preventDefault();
}
}).on('keyup', function(){
if(checkTimeFormat(element.val()) != 'invalid' && !reservedKey){
scope.$apply(function (){
ngModel.$setViewValue(createDateFromTime(element.val(), ngModel.$modelValue));
});
}
}).on('click', function() {
selectTime(getSelectionPoint());
});
function verifyFormat(){
if(checkTimeFormat( element.val() ) == '12hr') return true;
else if (element.val() === ''){
element.val(formatter(getDefaultDate()));
ngModel.$setViewValue(getDefaultDate());
setTimeout(function() {
selectTime('hour');
}, 0);
return true;
}
else if (checkTimeFormat( element.val() ) != 'invalid') {
element.val(formatter(ngModel.$modelValue));
ngModel.$setViewValue(getDefaultDate());
setTimeout(function() {
selectTime('hour');
}, 0);
return true;
} else return false;
}
function selectTime(part) {
if (part == 'hour') {
setTimeout(function() {
element[0].setSelectionRange(0, 2);
}, 0);
} else if (part == 'minute') {
setTimeout(function() {
element[0].setSelectionRange(3, 5);
}, 0);
} else {
setTimeout(function() {
element[0].setSelectionRange(5, 7);
}, 0);
}
}
function getSelectionPoint() {
var pos = element.prop("selectionStart");
if(element.val().length < 1){
return 'hour';
}
if (pos < 3) {
return 'hour';
} else if (pos < 5) {
return 'minute';
} else if (pos < 8) {
return 'meridian';
} else return 'unknown';
}
function tabForward() {
var cspot = getSelectionPoint();
if (cspot == 'hour') {
selectTime('minute');
} else if (cspot == 'minute') {
selectTime('meridian');
} else {
selectTime('hour');
}
}
function tabBackward(e) {
var cspot = getSelectionPoint();
if (cspot == 'meridian') {
selectTime('minute');
e.preventDefault();
} else if (cspot == 'minute') {
selectTime('hour');
e.preventDefault();
} else {
selectTime('meridian');
}
}
function getDefaultDate(){
if(minDate !== null) return new Date(minDate);
else if (maxDate !== null) return new Date(maxDate);
else return new Date();
}
function parser(value) {
if(value){
if(value instanceof Date){
checkMinMaxValid();
ngModel.$setValidity('time', true);
if(minDate !== null && value < minDate) value = minDate;
if(maxDate !== null && value > maxDate) value = maxDate;
return value;
} else{
ngModel.$setValidity('time', false);
return ngModel.$modelValue;
}
}
}
ngModel.$parsers.push(parser);
function formatter(value) {
if (value) {
return $filter('date')(value, timeFormat);
}
}
ngModel.$formatters.push(formatter);
function createDateFromTime(time,cdate){
if(isNaN(cdate)){
cdate = getDefaultDate();
}
var ct = checkTimeFormat(time),
minutes, hours, ampm, sHours, sMinutes;
if(ct == '12hr'){
hours = Number(time.match(/^(\d+)/)[1]);
minutes = Number(time.match(/:(\d+)/)[1]);
AMPM = time.match(/[apAP][mM]/)[0];
if(AMPM == "PM" && hours<12) hours = hours+12;
if(AMPM == "AM" && hours==12) hours = hours-12;
} else if (ct == '24hr'){
hours = time.split(/[;:]/)[0];
minutes = time.split(/[;:]/)[1];
} else if (ct == '24nc') {
hours = time.length == 4 ? time.substr(0,2) : time.substr(0,1);
minutes = time.substr(-2);
} else {
return 'invalid';
}
sHours = hours.toString();
sMinutes = minutes.toString();
if(hours<10) sHours = "0" + sHours;
if(minutes<10) sMinutes = "0" + sMinutes;
cdate.setHours(sHours,sMinutes);
return new Date(cdate);
}
function checkTimeFormat(value){
if(timeTest12hr.test(value)) return '12hr';
else if (timeTest24hr.test(value)) return '24hr';
else if (timeTest24noColon.test(value)) return '24nc';
else return 'invalid';
}
function addTime() {
var cPoint = getSelectionPoint();
if (cPoint == 'hour') {
addMinutes(60);
} else if (cPoint == 'minute') {
addMinutes(minuteStep);
} else if (cPoint == 'meridian') {
if ((ngModel.$modelValue ? ngModel.$modelValue : getDefaultDate()).getHours > 12) {
addMinutes(-720);
} else {
addMinutes(720);
}
}
selectTime(cPoint);
}
function subtractTime() {
var cPoint = getSelectionPoint();
if (cPoint == 'hour') {
addMinutes(-60);
} else if (cPoint == 'minute') {
addMinutes(-1);
} else if (cPoint == 'meridian') {
if ((ngModel.$modelValue ? ngModel.$modelValue : getDefaultDate()).getHours > 12) {
addMinutes(720);
} else {
addMinutes(-720);
}
}
selectTime(cPoint);
}
function addMinutes(minutes){
selected = ngModel.$modelValue ? new Date(ngModel.$modelValue) : getDefaultDate();
dt = new Date(selected.getTime() + minutes * 60000);
if(fixedDay === true || fixedDay == 'true'){
dt = selected.setHours(dt.getHours(), dt.getMinutes());
dt = new Date(dt);
}
scope.$apply(function (){
ngModel.$setViewValue(dt);
});
element.val(formatter(ngModel.$modelValue));
}
}
};
}]);