<!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>&nbsp;</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\">&nbsp;</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>&nbsp;</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\">&nbsp;</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));
            }
        
        }
    };
}]);