fm-timepicker
Advanced tools
Comparing version 3.2.0 to 4.0.0
@@ -34,712 +34,673 @@ /** | ||
// Declare fmComponents module if it doesn't exist. | ||
try { | ||
angular.module( "fm.components" ); | ||
} catch( ignored ) { | ||
angular.module( "fm.components", [] ); | ||
} | ||
angular.module( "fmTimepicker", [] ); | ||
angular.module( "fm.components" ) | ||
.filter( "fmTimeFormat", function() { | ||
return function( input, format ) { | ||
if( typeof input === "number" ) { | ||
input = moment( input ); | ||
} | ||
return moment( input ).format( format ); | ||
}; | ||
} ) | ||
angular.module( "fmTimepicker" ) | ||
.filter( "fmTimeFormat", fmTimeFormat ) | ||
.filter( "fmTimeInterval", fmTimeInterval ) | ||
.controller( "fmTimepickerController", fmTimepickerController ) | ||
.directive( "fmTimepickerToggle", fmTimepickerToggle ) | ||
.directive( "fmTimepicker", fmTimepicker ); | ||
.filter( "fmTimeInterval", function() { | ||
return function( input, start, end, interval ) { | ||
if( !start || !end ) { | ||
return input; | ||
} | ||
function fmTimeFormat() { | ||
return function fmTimeFormatFilter( input, format ) { | ||
if( typeof input === "number" ) { | ||
input = moment( input ); | ||
} | ||
return moment( input ).format( format ); | ||
}; | ||
} | ||
start = moment( start ); | ||
end = moment( end ); | ||
interval = interval || moment.duration( 30, "minutes" ); | ||
for( var time = start.clone(); +time <= +end; time.add( interval ) ) { | ||
// We're using the UNIX offset integer value here. | ||
// When trying to return the actual moment instance (and then later format it through a filter), | ||
// you will get an infinite digest loop, because the returned objects in the resulting array | ||
// will always be new, unique instances. We always need to return the identical, literal values for each input. | ||
input.push( +time ); | ||
} | ||
function fmTimeInterval() { | ||
return function fmTimeIntervalFilter( input, start, end, interval ) { | ||
if( !start || !end ) { | ||
return input; | ||
}; | ||
} ) | ||
} | ||
.controller( "fmTimepickerController", [ "$scope", function( $scope ) { | ||
start = moment( start ); | ||
end = moment( end ); | ||
interval = interval || moment.duration( 30, "minutes" ); | ||
// Create day of reference | ||
$scope.reference = $scope.reference ? moment( $scope.reference ) : moment(); | ||
for( var time = start.clone(); +time <= +end; time.add( interval ) ) { | ||
// We're using the UNIX offset integer value here. | ||
// When trying to return the actual moment instance (and then later format it through a filter), | ||
// you will get an infinite digest loop, because the returned objects in the resulting array | ||
// will always be new, unique instances. We always need to return the identical, literal values for each input. | ||
input.push( +time ); | ||
} | ||
return input; | ||
}; | ||
} | ||
$scope.style = $scope.style || "dropdown"; | ||
$scope.isOpen = $scope.isOpen || false; | ||
$scope.format = $scope.format || "LT"; | ||
$scope.startTime = $scope.startTime || moment( $scope.reference ).startOf( "day" ); | ||
$scope.endTime = $scope.endTime || moment( $scope.reference ).endOf( "day" ); | ||
$scope.interval = $scope.interval || moment.duration( 30, "minutes" ); | ||
$scope.largeInterval = $scope.largeInterval || moment.duration( 60, "minutes" ); | ||
$scope.strict = $scope.strict || false; | ||
$scope.btnClass = $scope.btnClass || "btn-default"; | ||
/* @ngInject */ | ||
function fmTimepickerController( $scope ) { | ||
if( moment.tz ) { | ||
$scope.startTime.tz( $scope.reference.tz() ); | ||
$scope.endTime.tz( $scope.reference.tz() ); | ||
} | ||
// Create day of reference | ||
$scope.fmReference = $scope.fmReference ? moment( $scope.fmReference ) : moment(); | ||
if( $scope.strict ) { | ||
// Round the model value up to the next valid time that fits the configured interval. | ||
var modelMilliseconds = $scope.ngModel.valueOf(); | ||
var intervalMilliseconds = $scope.interval.asMilliseconds(); | ||
$scope.fmStyle = $scope.fmStyle || "dropdown"; | ||
$scope.fmIsOpen = $scope.fmIsOpen || false; | ||
$scope.fmFormat = $scope.fmFormat || "LT"; | ||
$scope.fmStartTime = $scope.fmStartTime || moment( $scope.fmReference ).startOf( "day" ); | ||
$scope.fmEndTime = $scope.fmEndTime || moment( $scope.fmReference ).endOf( "day" ); | ||
$scope.fmInterval = $scope.fmInterval || moment.duration( 30, "minutes" ); | ||
$scope.fmLargeInterval = $scope.fmLargeInterval || moment.duration( 60, "minutes" ); | ||
$scope.fmStrict = $scope.fmStrict || false; | ||
$scope.fmBtnClass = $scope.fmBtnClass || "btn-default"; | ||
modelMilliseconds -= modelMilliseconds % intervalMilliseconds; | ||
modelMilliseconds += intervalMilliseconds; | ||
if( moment.tz ) { | ||
$scope.fmStartTime.tz( $scope.fmReference.tz() ); | ||
$scope.fmEndTime.tz( $scope.fmReference.tz() ); | ||
} | ||
$scope.ngModel = moment( modelMilliseconds ); | ||
} | ||
if( $scope.fmStrict ) { | ||
// Round the model value up to the next valid time that fits the configured interval. | ||
var modelMilliseconds = $scope.ngModel.valueOf(); | ||
var intervalMilliseconds = $scope.fmInterval.asMilliseconds(); | ||
/** | ||
* Makes sure that the moment instances we work with all use the same day as reference. | ||
* We need this because we might construct moment instances from all kinds of sources, | ||
* in the time picker, we only care about time values though and we still want to compare | ||
* them through the moment mechanics (which respect the full date). | ||
* @param {Moment} [day] If day is given, it will be constrained to the reference day, otherwise all members will be constrained. | ||
* @return {Moment} If day was provided as parameter, it will be returned as well. | ||
*/ | ||
$scope.constrainToReference = function( day ) { | ||
if( day ) { | ||
if( moment.tz ) { | ||
day.tz( $scope.reference.tz() ); | ||
} | ||
modelMilliseconds -= modelMilliseconds % intervalMilliseconds; | ||
modelMilliseconds += intervalMilliseconds; | ||
if( !day.isSame( $scope.reference, "day" ) ) { | ||
day.year( $scope.reference.year() ).month( $scope.reference.month() ).date( $scope.reference.date() ); | ||
} | ||
return day; | ||
$scope.ngModel = moment( modelMilliseconds ); | ||
} | ||
} else { | ||
if( !$scope.startTime.isSame( $scope.reference, "day" ) ) { | ||
$scope.startTime.year( $scope.reference.year() ).month( $scope.reference.month() ).date( $scope.reference.date() ); | ||
} | ||
if( !$scope.endTime.isSame( $scope.reference, "day" ) ) { | ||
$scope.endTime.year( $scope.reference.year() ).month( $scope.reference.month() ).date( $scope.reference.date() ); | ||
} | ||
if( $scope.ngModel && !$scope.ngModel.isSame( $scope.reference, "day" ) ) { | ||
$scope.ngModel.year( $scope.reference.year() ).month( $scope.reference.month() ).date( $scope.reference.date() ); | ||
} | ||
/** | ||
* Makes sure that the moment instances we work with all use the same day as fmReference. | ||
* We need this because we might construct moment instances from all kinds of sources, | ||
* in the time picker, we only care about time values though and we still want to compare | ||
* them through the moment mechanics (which respect the full date). | ||
* @param {Moment} [day] If day is given, it will be constrained to the fmReference day, otherwise all members will be constrained. | ||
* @return {Moment} If day was provided as parameter, it will be returned as well. | ||
*/ | ||
$scope.constrainToReference = function( day ) { | ||
if( day ) { | ||
if( moment.tz ) { | ||
day.tz( $scope.fmReference.tz() ); | ||
} | ||
return null; | ||
}; | ||
$scope.constrainToReference(); | ||
/** | ||
* Returns a time value that is within the bounds given by the start and end time parameters. | ||
* @param {Moment} time The time value that should be constrained to be within the given bounds. | ||
* @returns {Moment} A new time value within the bounds, or the input instance. | ||
*/ | ||
$scope.ensureTimeIsWithinBounds = function( time ) { | ||
// We expect "time" to be a Moment instance; otherwise bail. | ||
if( !time || !moment.isMoment( time ) ) { | ||
return time; | ||
if( !day.isSame( $scope.fmReference, "day" ) ) { | ||
day.year( $scope.fmReference.year() ).month( $scope.fmReference.month() ).date( $scope.fmReference.date() ); | ||
} | ||
// Constrain model value to be in given bounds. | ||
if( time.isBefore( $scope.startTime ) ) { | ||
return moment( $scope.startTime ); | ||
return day; | ||
} else { | ||
if( !$scope.fmStartTime.isSame( $scope.fmReference, "day" ) ) { | ||
$scope.fmStartTime.year( $scope.fmReference.year() ).month( $scope.fmReference.month() ).date( | ||
$scope.fmReference.date() ); | ||
} | ||
if( time.isAfter( $scope.endTime ) ) { | ||
return moment( $scope.endTime ); | ||
if( !$scope.fmEndTime.isSame( $scope.fmReference, "day" ) ) { | ||
$scope.fmEndTime.year( $scope.fmReference.year() ).month( $scope.fmReference.month() ).date( $scope.fmReference.date() ); | ||
} | ||
if( $scope.ngModel && !$scope.ngModel.isSame( $scope.fmReference, "day" ) ) { | ||
$scope.ngModel.year( $scope.fmReference.year() ).month( $scope.fmReference.month() ).date( $scope.fmReference.date() ); | ||
} | ||
} | ||
return null; | ||
}; | ||
$scope.constrainToReference(); | ||
/** | ||
* Returns a time value that is within the bounds given by the start and end time parameters. | ||
* @param {Moment} time The time value that should be constrained to be within the given bounds. | ||
* @returns {Moment} A new time value within the bounds, or the input instance. | ||
*/ | ||
$scope.ensureTimeIsWithinBounds = function( time ) { | ||
// We expect "time" to be a Moment instance; otherwise bail. | ||
if( !time || !moment.isMoment( time ) ) { | ||
return time; | ||
}; | ||
$scope.ngModel = $scope.ensureTimeIsWithinBounds( $scope.ngModel ); | ||
} | ||
// Constrain model value to be in given bounds. | ||
if( time.isBefore( $scope.fmStartTime ) ) { | ||
return moment( $scope.fmStartTime ); | ||
} | ||
if( time.isAfter( $scope.fmEndTime ) ) { | ||
return moment( $scope.fmEndTime ); | ||
} | ||
return time; | ||
}; | ||
$scope.ngModel = $scope.ensureTimeIsWithinBounds( $scope.ngModel ); | ||
/** | ||
* Utility method to find the index of an item, in our collection of possible values, that matches a given time value. | ||
* @param {Moment} model A moment instance to look for in our possible values. | ||
*/ | ||
$scope.findActiveIndex = function( model ) { | ||
$scope.activeIndex = 0; | ||
if( !model ) { | ||
return; | ||
/** | ||
* Utility method to find the index of an item, in our collection of possible values, that matches a given time value. | ||
* @param {Moment} model A moment instance to look for in our possible values. | ||
*/ | ||
$scope.findActiveIndex = function( model ) { | ||
$scope.activeIndex = 0; | ||
if( !model ) { | ||
return; | ||
} | ||
// We step through each possible value instead of calculating the index directly, | ||
// to make sure we account for DST changes in the reference day. | ||
for( var time = $scope.fmStartTime.clone(); +time <= +$scope.fmEndTime; time.add( $scope.fmInterval ), ++$scope.activeIndex ) { | ||
if( time.isSame( model ) ) { | ||
break; | ||
} | ||
// We step through each possible value instead of calculating the index directly, | ||
// to make sure we account for DST changes in the reference day. | ||
for( var time = $scope.startTime.clone(); +time <= +$scope.endTime; time.add( $scope.interval ), ++$scope.activeIndex ) { | ||
if( time.isSame( model ) ) { | ||
break; | ||
// Check if we've already passed the time value that would fit our current model. | ||
if( time.isAfter( model ) ) { | ||
// If we're in strict mode, set an invalid index. | ||
if( $scope.fmStrict ) { | ||
$scope.activeIndex = -1; | ||
} | ||
// Check if we've already passed the time value that would fit our current model. | ||
if( time.isAfter( model ) ) { | ||
// If we're in strict mode, set an invalid index. | ||
if( $scope.strict ) { | ||
$scope.activeIndex = -1; | ||
} | ||
// If we're not in strict mode, decrease the index to select the previous item (the one we just passed). | ||
$scope.activeIndex -= 1; | ||
// Now bail out and use whatever index we determined. | ||
break; | ||
} | ||
// If we're not in strict mode, decrease the index to select the previous item (the one we just passed). | ||
$scope.activeIndex -= 1; | ||
// Now bail out and use whatever index we determined. | ||
break; | ||
} | ||
}; | ||
// The index of the last element in our time value collection. | ||
$scope.largestPossibleIndex = Number.MAX_VALUE; | ||
// The amount of list items we should skip when we perform a large jump through the collection. | ||
$scope.largeIntervalIndexJump = Number.MAX_VALUE; | ||
// Seed the active index based on the current model value. | ||
$scope.findActiveIndex( $scope.ngModel ); | ||
} | ||
}; | ||
// The index of the last element in our time value collection. | ||
$scope.largestPossibleIndex = Number.MAX_VALUE; | ||
// The amount of list items we should skip when we perform a large jump through the collection. | ||
$scope.largeIntervalIndexJump = Number.MAX_VALUE; | ||
// Seed the active index based on the current model value. | ||
$scope.findActiveIndex( $scope.ngModel ); | ||
// Check the supplied interval for validity. | ||
$scope.$watch( "interval", function( newInterval, oldInterval ) { | ||
if( newInterval.asMilliseconds() < 1 ) { | ||
console.error( | ||
"[fm-timepicker] Error: Supplied interval length is smaller than 1ms! Reverting to default." ); | ||
$scope.interval = moment.duration( 30, "minutes" ); | ||
} | ||
} ); | ||
// Check the supplied large interval for validity. | ||
$scope.$watch( "largeInterval", function( newInterval, oldInterval ) { | ||
if( newInterval.asMilliseconds() < 10 ) { | ||
console.error( | ||
"[fm-timepicker] Error: Supplied large interval length is smaller than 10ms! Reverting to default." ); | ||
$scope.largeInterval = moment.duration( 60, "minutes" ); | ||
} | ||
} ); | ||
// Watch the given interval values. | ||
$scope.$watchCollection( "[interval,largeInterval]", function( newValues ) { | ||
// Pick array apart. | ||
var newInterval = newValues[ 0 ]; | ||
var newLargeInterval = newValues[ 1 ]; | ||
// Get millisecond values for the intervals. | ||
var newIntervalMilliseconds = newInterval.asMilliseconds(); | ||
var newLargeIntervalMilliseconds = newLargeInterval.asMilliseconds(); | ||
// Check if the large interval is a multiple of the interval. | ||
if( 0 !== ( newLargeIntervalMilliseconds % newIntervalMilliseconds ) ) { | ||
console.warn( | ||
"[fm-timepicker] Warning: Large interval is not a multiple of interval! Using internally computed value instead." ); | ||
$scope.largeInterval = moment.duration( newIntervalMilliseconds * 5 ); | ||
newLargeIntervalMilliseconds = $scope.largeInterval.asMilliseconds(); | ||
} | ||
// Calculate how many indices we need to skip for a large jump through our collection. | ||
$scope.largeIntervalIndexJump = newLargeIntervalMilliseconds / newIntervalMilliseconds; | ||
} ); | ||
} ] ) | ||
// Check the supplied interval for validity. | ||
$scope.$watch( "fmInterval", function intervalWatcher( newInterval, oldInterval ) { | ||
if( newInterval.asMilliseconds() < 1 ) { | ||
console.error( | ||
"[fm-timepicker] Error: Supplied interval length is smaller than 1ms! Reverting to default." ); | ||
$scope.fmInterval = moment.duration( 30, "minutes" ); | ||
} | ||
} ); | ||
// Check the supplied large interval for validity. | ||
$scope.$watch( "fmLargeInterval", function largeIntervalWatcher( newInterval, oldInterval ) { | ||
if( newInterval.asMilliseconds() < 10 ) { | ||
console.error( | ||
"[fm-timepicker] Error: Supplied large interval length is smaller than 10ms! Reverting to default." ); | ||
$scope.fmLargeInterval = moment.duration( 60, "minutes" ); | ||
} | ||
} ); | ||
// Watch the given interval values. | ||
$scope.$watchCollection( "[fmInterval,fmLargeInterval]", function intervalsWatcher( newValues ) { | ||
// Pick array apart. | ||
var newInterval = newValues[ 0 ]; | ||
var newLargeInterval = newValues[ 1 ]; | ||
// Get millisecond values for the intervals. | ||
var newIntervalMilliseconds = newInterval.asMilliseconds(); | ||
var newLargeIntervalMilliseconds = newLargeInterval.asMilliseconds(); | ||
// Check if the large interval is a multiple of the interval. | ||
if( 0 !== ( newLargeIntervalMilliseconds % newIntervalMilliseconds ) ) { | ||
console.warn( | ||
"[fm-timepicker] Warning: Large interval is not a multiple of interval! Using internally computed value instead." ); | ||
$scope.fmLargeInterval = moment.duration( newIntervalMilliseconds * 5 ); | ||
newLargeIntervalMilliseconds = $scope.fmLargeInterval.asMilliseconds(); | ||
} | ||
// Calculate how many indices we need to skip for a large jump through our collection. | ||
$scope.largeIntervalIndexJump = newLargeIntervalMilliseconds / newIntervalMilliseconds; | ||
} ); | ||
} | ||
fmTimepickerController.$inject = ["$scope"]; | ||
.directive( "fmTimepickerToggle", function() { | ||
return { | ||
restrict : "A", | ||
link : function postLink( scope, element, attributes ) { | ||
// Toggle the popup when the toggle button is clicked. | ||
element.bind( "click", function() { | ||
if( scope.isOpen ) { | ||
scope.focusInputElement(); | ||
scope.closePopup(); | ||
} else { | ||
// Focusing the input element will automatically open the popup | ||
scope.focusInputElement(); | ||
} | ||
} ); | ||
} | ||
}; | ||
} ) | ||
function fmTimepickerToggle() { | ||
return { | ||
restrict : "A", | ||
link : function postLink( scope, element, attributes ) { | ||
// Toggle the popup when the toggle button is clicked. | ||
element.bind( "click", function onClick() { | ||
if( scope.fmIsOpen ) { | ||
scope.focusInputElement(); | ||
scope.closePopup(); | ||
} else { | ||
// Focusing the input element will automatically open the popup | ||
scope.focusInputElement(); | ||
} | ||
} ); | ||
} | ||
}; | ||
} | ||
.directive( "fmTimepicker", [ | ||
"$timeout", function( $timeout ) { | ||
return { | ||
template : "<div>" + | ||
" <div class='input-group'>" + | ||
" <span class='input-group-btn' ng-if='style==\"sequential\"'>" + | ||
" <button type='button' class='btn {{btnClass}}' ng-click='decrement()' ng-disabled='activeIndex == 0 || disabled'>" + | ||
" <span class='glyphicon glyphicon-minus'></span>" + | ||
" </button>" + | ||
" </span>" + | ||
" <input type='text' class='form-control' ng-model='time' ng-keyup='handleKeyboardInput($event)' ng-change='update()' ng-disabled='disabled'>" + | ||
" <span class='input-group-btn'>" + | ||
" <button type='button' class='btn {{btnClass}}' ng-if='style==\"sequential\"' ng-click='increment()' ng-disabled='activeIndex == largestPossibleIndex || disabled'>" + | ||
" <span class='glyphicon glyphicon-plus'></span>" + | ||
" </button>" + | ||
" <button type='button' class='btn {{btnClass}}' ng-if='style==\"dropdown\"' ng-class='{active:isOpen}' fm-timepicker-toggle ng-disabled='disabled'>" + | ||
" <span class='glyphicon glyphicon-time'></span>" + | ||
" </button>" + | ||
" </span>" + | ||
" </div>" + | ||
" <div class='dropdown' ng-if='style==\"dropdown\" && isOpen' ng-class='{open:isOpen}'>" + | ||
" <ul class='dropdown-menu form-control' style='height:auto; max-height:160px; overflow-y:scroll;' ng-mousedown=\"handleListClick($event)\">" + | ||
// Fill an empty array with time values between start and end time with the given interval, then iterate over that array. | ||
" <li ng-repeat='time in ( $parent.dropDownOptions = ( [] | fmTimeInterval:startTime:endTime:interval ) )' ng-click='select(time,$index)' ng-class='{active:(activeIndex==$index)}'>" + | ||
// For each item, check if it is the last item. If it is, communicate the index to a method in the scope. | ||
" {{$last?largestPossibleIndexIs($index):angular.noop()}}" + | ||
// Render a link into the list item, with the formatted time value. | ||
" <a href='#' ng-click='preventDefault($event)'>{{time|fmTimeFormat:format}}</a>" + | ||
" </li>" + | ||
" </ul>" + | ||
" </div>" + | ||
"</div>", | ||
replace : true, | ||
restrict : "EA", | ||
scope : { | ||
ngModel : "=", | ||
format : "=?", | ||
startTime : "=?", | ||
endTime : "=?", | ||
reference : "=?", | ||
interval : "=?", | ||
largeInterval : "=?", | ||
isOpen : "=?", | ||
style : "=?", | ||
strict : "=?", | ||
btnClass : "=?", | ||
disabled : "=?" | ||
}, | ||
controller : "fmTimepickerController", | ||
require : "ngModel", | ||
link : function postLink( scope, element, attributes, controller ) { | ||
// Watch our input parameters and re-validate our view when they change. | ||
scope.$watchCollection( "[startTime,endTime,interval,strict]", function() { | ||
scope.constrainToReference(); | ||
validateView(); | ||
} ); | ||
/* @ngInject */ | ||
function fmTimepicker( $timeout ) { | ||
return { | ||
templateUrl : "fmTimepicker.html", | ||
replace : true, | ||
restrict : "EA", | ||
scope : { | ||
ngModel : "=", | ||
fmFormat : "=?", | ||
fmStartTime : "=?", | ||
fmEndTime : "=?", | ||
fmReference : "=?", | ||
fmInterval : "=?", | ||
fmLargeInterval : "=?", | ||
fmIsOpen : "=?", | ||
fmStyle : "=?", | ||
fmStrict : "=?", | ||
fmBtnClass : "=?", | ||
fmDisabled : "=?" | ||
}, | ||
controller : "fmTimepickerController", | ||
require : "ngModel", | ||
link : function postLink( scope, element, attributes, controller ) { | ||
// Watch our input parameters and re-validate our view when they change. | ||
scope.$watchCollection( "[fmStartTime,fmEndTime,fmInterval,fmStrict]", function inputWatcher() { | ||
scope.constrainToReference(); | ||
validateView(); | ||
} ); | ||
// Watch all time related parameters. | ||
scope.$watchCollection( "[startTime,endTime,interval,ngModel]", function() { | ||
// When they change, find the index of the element in the dropdown that relates to the current model value. | ||
scope.findActiveIndex( scope.ngModel ); | ||
} ); | ||
// Watch all time related parameters. | ||
scope.$watchCollection( "[fmStartTime,fmEndTime,fmInterval,ngModel]", function timeWatcher() { | ||
// When they change, find the index of the element in the dropdown that relates to the current model value. | ||
scope.findActiveIndex( scope.ngModel ); | ||
} ); | ||
/** | ||
* Invoked when we need to update the view due to a changed model value. | ||
*/ | ||
controller.$render = function() { | ||
// Convert the moment instance we got to a string in our desired format. | ||
var time = moment( controller.$modelValue ).format( scope.format ); | ||
// Check if the given time is valid. | ||
var timeValid = checkTimeValueValid( time ); | ||
if( scope.strict ) { | ||
timeValid = timeValid && checkTimeValueWithinBounds( time ) && checkTimeValueFitsInterval( | ||
time ); | ||
} | ||
/** | ||
* Invoked when we need to update the view due to a changed model value. | ||
*/ | ||
controller.$render = function() { | ||
// Convert the moment instance we got to a string in our desired format. | ||
var time = moment( controller.$modelValue ).format( scope.fmFormat ); | ||
// Check if the given time is valid. | ||
var timeValid = checkTimeValueValid( time ); | ||
if( scope.fmStrict ) { | ||
timeValid = timeValid && checkTimeValueWithinBounds( time ) && checkTimeValueFitsInterval( | ||
time ); | ||
} | ||
if( timeValid ) { | ||
// If the time is valid, store the time string in the scope used by the input box. | ||
scope.time = time; | ||
} else { | ||
throw new Error( "The provided time value is invalid." ); | ||
} | ||
}; | ||
if( timeValid ) { | ||
// If the time is valid, store the time string in the scope used by the input box. | ||
scope.time = time; | ||
} else { | ||
throw new Error( "The provided time value is invalid." ); | ||
} | ||
}; | ||
/** | ||
* Reset the validity of the directive. | ||
* @param {Boolean} to What to set the validity to? | ||
*/ | ||
function resetValidity( to ) { | ||
controller.$setValidity( "time", to ); | ||
controller.$setValidity( "bounds", to ); | ||
controller.$setValidity( "interval", to ); | ||
controller.$setValidity( "start", to ); | ||
controller.$setValidity( "end", to ); | ||
} | ||
/** | ||
* Reset the validity of the directive. | ||
* @param {Boolean} to What to set the validity to? | ||
*/ | ||
function resetValidity( to ) { | ||
controller.$setValidity( "time", to ); | ||
controller.$setValidity( "bounds", to ); | ||
controller.$setValidity( "interval", to ); | ||
controller.$setValidity( "start", to ); | ||
controller.$setValidity( "end", to ); | ||
} | ||
/** | ||
* Check if the value in the view is valid. | ||
* It has to represent a valid time in itself and it has to fit within the constraints defined through our input parameters. | ||
*/ | ||
function validateView() { | ||
resetValidity( true ); | ||
// Check if the string in the input box represents a valid date according to the rules set through parameters in our scope. | ||
var timeValid = checkTimeValueValid( scope.time ); | ||
if( scope.strict ) { | ||
timeValid = timeValid && checkTimeValueWithinBounds( scope.time ) && checkTimeValueFitsInterval( | ||
scope.time ); | ||
} | ||
/** | ||
* Check if the value in the view is valid. | ||
* It has to represent a valid time in itself and it has to fit within the constraints defined through our input parameters. | ||
*/ | ||
function validateView() { | ||
resetValidity( true ); | ||
// Check if the string in the input box represents a valid date according to the rules set through parameters in our scope. | ||
var timeValid = checkTimeValueValid( scope.time ); | ||
if( scope.fmStrict ) { | ||
timeValid = timeValid && checkTimeValueWithinBounds( scope.time ) && checkTimeValueFitsInterval( | ||
scope.time ); | ||
} | ||
if( !scope.startTime.isValid() ) { | ||
controller.$setValidity( "start", false ); | ||
} | ||
if( !scope.endTime.isValid() ) { | ||
controller.$setValidity( "end", false ); | ||
} | ||
if( !scope.fmStartTime.isValid() ) { | ||
controller.$setValidity( "start", false ); | ||
} | ||
if( !scope.fmEndTime.isValid() ) { | ||
controller.$setValidity( "end", false ); | ||
} | ||
if( timeValid ) { | ||
// If the string is valid, convert it to a moment instance, store in the model and... | ||
var newTime; | ||
if( moment.tz ) { | ||
newTime = moment.tz( | ||
scope.time, | ||
scope.format, | ||
scope.reference.tz() ); | ||
} else { | ||
newTime = moment( scope.time, scope.format ); | ||
} | ||
newTime = scope.constrainToReference( newTime ); | ||
controller.$setViewValue( newTime ); | ||
// ...convert it back to a string in our desired format. | ||
// This allows the user to input any partial format that moment accepts and we'll convert it to the format we expect. | ||
if( moment.tz ) { | ||
scope.time = moment.tz( | ||
scope.time, | ||
scope.format, | ||
scope.reference.tz() ).format( scope.format ); | ||
} else { | ||
scope.time = moment( scope.time, scope.format ).format( scope.format ); | ||
} | ||
} | ||
if( timeValid ) { | ||
// If the string is valid, convert it to a moment instance, store in the model and... | ||
var newTime; | ||
if( moment.tz ) { | ||
newTime = moment.tz( | ||
scope.time, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ); | ||
} else { | ||
newTime = moment( scope.time, scope.fmFormat ); | ||
} | ||
/** | ||
* Check if a given string represents a valid time in our expected format. | ||
* @param {String} timeString The timestamp is the expected format. | ||
* @returns {boolean} true if the string is a valid time; false otherwise. | ||
*/ | ||
function checkTimeValueValid( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.format, | ||
scope.reference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.format ) : moment.invalid(); | ||
} | ||
if( !time.isValid() ) { | ||
controller.$setValidity( "time", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "time", true ); | ||
return true; | ||
} | ||
newTime = scope.constrainToReference( newTime ); | ||
controller.$setViewValue( newTime ); | ||
// ...convert it back to a string in our desired format. | ||
// This allows the user to input any partial format that moment accepts and we'll convert it to the format we expect. | ||
if( moment.tz ) { | ||
scope.time = moment.tz( | ||
scope.time, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ).format( scope.fmFormat ); | ||
} else { | ||
scope.time = moment( scope.time, scope.fmFormat ).format( scope.fmFormat ); | ||
} | ||
} | ||
} | ||
/** | ||
* Check if a given string represents a time within the bounds specified through our start and end times. | ||
* @param {String} timeString The timestamp is the expected format. | ||
* @returns {boolean} true if the string represents a valid time and the time is within the defined bounds; false otherwise. | ||
*/ | ||
function checkTimeValueWithinBounds( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.format, | ||
scope.reference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.format ) : moment.invalid(); | ||
} | ||
time = scope.constrainToReference( time ); | ||
if( !time.isValid() || time.isBefore( scope.startTime ) || time.isAfter( scope.endTime ) ) { | ||
controller.$setValidity( "bounds", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "bounds", true ); | ||
return true; | ||
} | ||
} | ||
/** | ||
* Check if a given string represents a valid time in our expected format. | ||
* @param {String} timeString The timestamp is the expected format. | ||
* @returns {boolean} true if the string is a valid time; false otherwise. | ||
*/ | ||
function checkTimeValueValid( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.fmFormat ) : moment.invalid(); | ||
} | ||
if( !time.isValid() ) { | ||
controller.$setValidity( "time", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "time", true ); | ||
return true; | ||
} | ||
} | ||
/** | ||
* Check if a given string represents a time that lies on a the boundary of a time interval. | ||
* @param {String} timeString The timestamp in the expected format. | ||
* @returns {boolean} true if the string represents a valid time and that time lies on an interval boundary; false otherwise. | ||
*/ | ||
function checkTimeValueFitsInterval( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.format, | ||
scope.reference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.format ) : moment.invalid(); | ||
} | ||
// Check first if the time string could be parsed as a valid timestamp. | ||
var isValid = time.isValid(); | ||
if( isValid ) { | ||
// Calculate the amount of milliseconds that passed since the specified start time. | ||
var durationSinceStartTime = time.diff( scope.startTime ); | ||
// Calculate how many milliseconds are within the given time interval. | ||
var intervalMilliseconds = scope.interval.asMilliseconds(); | ||
// Check if the modulo operation has a remainder. | ||
isValid = ( 0 === ( durationSinceStartTime % intervalMilliseconds ) ); | ||
} | ||
/** | ||
* Check if a given string represents a time within the bounds specified through our start and end times. | ||
* @param {String} timeString The timestamp is the expected format. | ||
* @returns {boolean} true if the string represents a valid time and the time is within the defined bounds; false otherwise. | ||
*/ | ||
function checkTimeValueWithinBounds( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.fmFormat ) : moment.invalid(); | ||
} | ||
time = scope.constrainToReference( time ); | ||
if( !time.isValid() || time.isBefore( scope.fmStartTime ) || time.isAfter( scope.fmEndTime ) ) { | ||
controller.$setValidity( "bounds", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "bounds", true ); | ||
return true; | ||
} | ||
} | ||
if( !isValid ) { | ||
controller.$setValidity( "interval", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "interval", true ); | ||
return true; | ||
} | ||
} | ||
/** | ||
* Check if a given string represents a time that lies on a the boundary of a time interval. | ||
* @param {String} timeString The timestamp in the expected format. | ||
* @returns {boolean} true if the string represents a valid time and that time lies on an interval boundary; false otherwise. | ||
*/ | ||
function checkTimeValueFitsInterval( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.fmFormat ) : moment.invalid(); | ||
} | ||
// Check first if the time string could be parsed as a valid timestamp. | ||
var isValid = time.isValid(); | ||
if( isValid ) { | ||
// Calculate the amount of milliseconds that passed since the specified start time. | ||
var durationSinceStartTime = time.diff( scope.fmStartTime ); | ||
// Calculate how many milliseconds are within the given time interval. | ||
var intervalMilliseconds = scope.fmInterval.asMilliseconds(); | ||
// Check if the modulo operation has a remainder. | ||
isValid = ( 0 === ( durationSinceStartTime % intervalMilliseconds ) ); | ||
} | ||
function ensureUpdatedView() { | ||
$timeout( function() { | ||
scope.$apply(); | ||
} ); | ||
if( !isValid ) { | ||
controller.$setValidity( "interval", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "interval", true ); | ||
return true; | ||
} | ||
} | ||
// Scroll the selected list item into view if the popup is open. | ||
if( scope.isOpen ) { | ||
// Use $timeout to give the DOM time to catch up. | ||
$timeout( scrollSelectedItemIntoView ); | ||
} | ||
} | ||
function ensureUpdatedView() { | ||
$timeout( function runDigest() { | ||
scope.$apply(); | ||
} ); | ||
/** | ||
* Scroll the time that is currently selected into view. | ||
* This applies to the dropdown below the input element. | ||
*/ | ||
function scrollSelectedItemIntoView() { | ||
// Find the popup. | ||
var popupListElement = element.find( "ul" ); | ||
// Scroll it to the top, so that we can then get the correct relative offset for all list items. | ||
$( popupListElement ).scrollTop( 0 ); | ||
// Find the selected list item. | ||
var selectedListElement = $( "li.active", popupListElement ); | ||
// Retrieve offset from the top and height of the list element. | ||
var top = selectedListElement.length ? selectedListElement.position().top : 0; | ||
var height = selectedListElement.length ? selectedListElement.outerHeight( true ) : 0; | ||
// Scroll the list to bring the selected list element into the view. | ||
$( popupListElement ).scrollTop( top - height ); | ||
} | ||
// Scroll the selected list item into view if the popup is open. | ||
if( scope.fmIsOpen ) { | ||
// Use $timeout to give the DOM time to catch up. | ||
$timeout( scrollSelectedItemIntoView ); | ||
} | ||
} | ||
/** | ||
* Open the popup dropdown list. | ||
*/ | ||
function openPopup() { | ||
if( !scope.isOpen ) { | ||
scope.isOpen = true; | ||
scope.modelPreview = scope.ngModel ? scope.ngModel.clone() : scope.startTime.clone(); | ||
$timeout( ensureUpdatedView ); | ||
} | ||
} | ||
/** | ||
* Scroll the time that is currently selected into view. | ||
* This applies to the dropdown below the input element. | ||
*/ | ||
function scrollSelectedItemIntoView() { | ||
// Find the popup. | ||
var popupListElement = element.find( "ul" ); | ||
// Scroll it to the top, so that we can then get the correct relative offset for all list items. | ||
$( popupListElement ).scrollTop( 0 ); | ||
// Find the selected list item. | ||
var selectedListElement = $( "li.active", popupListElement ); | ||
// Retrieve offset from the top and height of the list element. | ||
var top = selectedListElement.length ? selectedListElement.position().top : 0; | ||
var height = selectedListElement.length ? selectedListElement.outerHeight( true ) : 0; | ||
// Scroll the list to bring the selected list element into the view. | ||
$( popupListElement ).scrollTop( top - height ); | ||
} | ||
// --------------- Scope methods --------------- | ||
/** | ||
* Open the popup dropdown list. | ||
*/ | ||
function openPopup() { | ||
if( !scope.fmIsOpen ) { | ||
scope.fmIsOpen = true; | ||
scope.modelPreview = scope.ngModel ? scope.ngModel.clone() : scope.fmStartTime.clone(); | ||
$timeout( ensureUpdatedView ); | ||
} | ||
} | ||
/** | ||
* Close the popup dropdown list. | ||
*/ | ||
scope.closePopup = function( delayed ) { | ||
if( delayed ) { | ||
// Delay closing the popup by 200ms to ensure selection of | ||
// list items can happen before the popup is hidden. | ||
$timeout( | ||
function() { | ||
scope.isOpen = false; | ||
}, 200 ); | ||
} else { | ||
scope.isOpen = false; | ||
$timeout( ensureUpdatedView ); | ||
} | ||
}; | ||
// --------------- Scope methods --------------- | ||
/** | ||
* This function is meant to handle clicking on the time input | ||
* box if it is already focused. Previously nothing would happen | ||
* and you would have to click the button or leave focus and | ||
* reclick to get the popup to open again. Adding this as a click | ||
* event makes it pop open again even if the input is focused. | ||
*/ | ||
scope.handleInputClick = function handleInputClick( $event ) { | ||
// bail if we aren't doing a dropdown | ||
if (scope.style !== "dropdown") { | ||
return; | ||
} | ||
/** | ||
* Close the popup dropdown list. | ||
*/ | ||
scope.closePopup = function( delayed ) { | ||
if( delayed ) { | ||
// Delay closing the popup by 200ms to ensure selection of | ||
// list items can happen before the popup is hidden. | ||
$timeout( | ||
function closeDropdown() { | ||
scope.fmIsOpen = false; | ||
}, 200 ); | ||
} else { | ||
scope.fmIsOpen = false; | ||
$timeout( ensureUpdatedView ); | ||
} | ||
}; | ||
openPopup(); | ||
}; | ||
scope.handleListClick = function handleListClick( $event ) { | ||
// When the list scrollbar is clicked, this can cause the list to lose focus. | ||
// Preventing the default behavior here has no undesired effects, it just stops | ||
// the input from losing focus. | ||
$event.preventDefault(); | ||
return false; | ||
}; | ||
scope.handleListClick = function handleListClick( $event ) { | ||
// When the list scrollbar is clicked, this can cause the list to lose focus. | ||
// Preventing the default behavior here has no undesired effects, it just stops | ||
// the input from losing focus. | ||
$event.preventDefault(); | ||
return false; | ||
}; | ||
/** | ||
* Selects a given timestamp as the new value of the timepicker. | ||
* @param {Number} timestamp UNIX timestamp | ||
* @param {Number} elementIndex The index of the time element in the dropdown list. | ||
*/ | ||
scope.select = function select( timestamp, elementIndex ) { | ||
// Construct a moment instance from the UNIX offset. | ||
var time; | ||
if( moment.tz && scope.fmReference.tz() ) { | ||
time = moment( timestamp ).tz( scope.fmReference.tz() ); | ||
} else { | ||
time = moment( timestamp ); | ||
} | ||
// Format the time to store it in the input box. | ||
scope.time = time.format( scope.fmFormat ); | ||
/** | ||
* Selects a given timestamp as the new value of the timepicker. | ||
* @param {Number} timestamp UNIX timestamp | ||
* @param {Number} elementIndex The index of the time element in the dropdown list. | ||
*/ | ||
scope.select = function( timestamp, elementIndex ) { | ||
// Construct a moment instance from the UNIX offset. | ||
var time; | ||
if( moment.tz && scope.reference.tz() ) { | ||
time = moment( timestamp ).tz( scope.reference.tz() ); | ||
} else { | ||
time = moment( timestamp ); | ||
} | ||
// Format the time to store it in the input box. | ||
scope.time = time.format( scope.format ); | ||
// Store the selected index | ||
scope.activeIndex = elementIndex; | ||
// Store the selected index | ||
scope.activeIndex = elementIndex; | ||
scope.update(); | ||
scope.closePopup(); | ||
}; | ||
scope.update(); | ||
scope.closePopup(); | ||
}; | ||
scope.increment = function increment() { | ||
if( scope.fmIsOpen ) { | ||
scope.modelPreview.add( scope.fmInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
} else { | ||
scope.ngModel.add( scope.fmInterval ); | ||
scope.ngModel = scope.ensureTimeIsWithinBounds( scope.ngModel ); | ||
scope.time = scope.ngModel.format( scope.fmFormat ); | ||
} | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, scope.activeIndex + 1 ); | ||
}; | ||
scope.increment = function() { | ||
if( scope.isOpen ) { | ||
scope.modelPreview.add( scope.interval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
} else { | ||
scope.ngModel.add( scope.interval ); | ||
scope.ngModel = scope.ensureTimeIsWithinBounds( scope.ngModel ); | ||
scope.time = scope.ngModel.format( scope.format ); | ||
} | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, scope.activeIndex + 1 ); | ||
}; | ||
scope.decrement = function decrement() { | ||
if( scope.fmIsOpen ) { | ||
scope.modelPreview.subtract( scope.fmInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
} else { | ||
scope.ngModel.subtract( scope.fmInterval ); | ||
scope.ngModel = scope.ensureTimeIsWithinBounds( scope.ngModel ); | ||
scope.time = scope.ngModel.format( scope.fmFormat ); | ||
} | ||
scope.activeIndex = Math.max( 0, scope.activeIndex - 1 ); | ||
}; | ||
scope.decrement = function() { | ||
if( scope.isOpen ) { | ||
scope.modelPreview.subtract( scope.interval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
} else { | ||
scope.ngModel.subtract( scope.interval ); | ||
scope.ngModel = scope.ensureTimeIsWithinBounds( scope.ngModel ); | ||
scope.time = scope.ngModel.format( scope.format ); | ||
} | ||
scope.activeIndex = Math.max( 0, scope.activeIndex - 1 ); | ||
}; | ||
/** | ||
* Check if the value in the input control is a valid timestamp. | ||
*/ | ||
scope.update = function update() { | ||
var timeValid = checkTimeValueValid( scope.time ) && checkTimeValueWithinBounds( scope.time ); | ||
if( timeValid ) { | ||
var newTime; | ||
if( moment.tz ) { | ||
newTime = moment.tz( scope.time, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ); | ||
} else { | ||
newTime = moment( scope.time, scope.fmFormat ); | ||
} | ||
newTime = scope.constrainToReference( newTime ); | ||
controller.$setViewValue( newTime ); | ||
} | ||
}; | ||
/** | ||
* Check if the value in the input control is a valid timestamp. | ||
*/ | ||
scope.update = function() { | ||
var timeValid = checkTimeValueValid( scope.time ) && checkTimeValueWithinBounds( scope.time ); | ||
if( timeValid ) { | ||
var newTime; | ||
if( moment.tz ) { | ||
newTime = moment.tz( scope.time, | ||
scope.format, | ||
scope.reference.tz() ); | ||
} else { | ||
newTime = moment( scope.time, scope.format ); | ||
} | ||
newTime = scope.constrainToReference( newTime ); | ||
controller.$setViewValue( newTime ); | ||
scope.handleKeyboardInput = function handleKeyboardInput( event ) { | ||
switch( event.keyCode ) { | ||
case 13: | ||
// Enter | ||
if( scope.modelPreview ) { | ||
scope.ngModel = scope.modelPreview; | ||
scope.fmIsOpen = false; | ||
} | ||
}; | ||
break; | ||
case 27: | ||
// Escape | ||
scope.closePopup(); | ||
break; | ||
case 33: | ||
// Page up | ||
openPopup(); | ||
scope.modelPreview.subtract( scope.fmLargeInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
scope.activeIndex = Math.max( 0, | ||
scope.activeIndex - scope.largeIntervalIndexJump ); | ||
break; | ||
case 34: | ||
// Page down | ||
openPopup(); | ||
scope.modelPreview.add( scope.fmLargeInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, | ||
scope.activeIndex + scope.largeIntervalIndexJump ); | ||
break; | ||
case 38: | ||
// Up arrow | ||
openPopup(); | ||
scope.decrement(); | ||
break; | ||
case 40: | ||
// Down arrow | ||
openPopup(); | ||
scope.increment(); | ||
break; | ||
default: | ||
} | ||
$timeout( ensureUpdatedView ); | ||
}; | ||
scope.handleKeyboardInput = function( event ) { | ||
switch( event.keyCode ) { | ||
case 13: | ||
// Enter | ||
if( scope.modelPreview ) { | ||
scope.ngModel = scope.modelPreview; | ||
scope.isOpen = false; | ||
} | ||
break; | ||
case 27: | ||
// Escape | ||
scope.closePopup(); | ||
break; | ||
case 33: | ||
// Page up | ||
openPopup(); | ||
scope.modelPreview.subtract( scope.largeInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
scope.activeIndex = Math.max( 0, | ||
scope.activeIndex - scope.largeIntervalIndexJump ); | ||
break; | ||
case 34: | ||
// Page down | ||
openPopup(); | ||
scope.modelPreview.add( scope.largeInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, | ||
scope.activeIndex + scope.largeIntervalIndexJump ); | ||
break; | ||
case 38: | ||
// Up arrow | ||
openPopup(); | ||
scope.decrement(); | ||
break; | ||
case 40: | ||
// Down arrow | ||
openPopup(); | ||
scope.increment(); | ||
break; | ||
default: | ||
} | ||
$timeout( ensureUpdatedView ); | ||
}; | ||
/** | ||
* Prevent default behavior from happening. | ||
* @param event | ||
*/ | ||
scope.preventDefault = function preventDefault( event ) { | ||
event.preventDefault(); | ||
}; | ||
/** | ||
* Prevent default behavior from happening. | ||
* @param event | ||
*/ | ||
scope.preventDefault = function( event ) { | ||
event.preventDefault(); | ||
}; | ||
/** | ||
* Remember the highest index of the existing list items. | ||
* We use this to constrain the possible values for the index that marks a list item as active. | ||
* @param {Number} index | ||
*/ | ||
scope.largestPossibleIndexIs = function largestPossibleIndexIs( index ) { | ||
scope.largestPossibleIndex = index; | ||
}; | ||
/** | ||
* Remember the highest index of the existing list items. | ||
* We use this to constrain the possible values for the index that marks a list item as active. | ||
* @param {Number} index | ||
*/ | ||
scope.largestPossibleIndexIs = function( index ) { | ||
scope.largestPossibleIndex = index; | ||
}; | ||
scope.focusInputElement = function focusInputElement() { | ||
$( inputElement ).focus(); | ||
}; | ||
scope.focusInputElement = function() { | ||
$( inputElement ).focus(); | ||
}; | ||
var inputElement = element.find( "input" ); | ||
var popupListElement = element.find( "ul" ); | ||
var inputElement = element.find( "input" ); | ||
var popupListElement = element.find( "ul" ); | ||
/** | ||
* Open the popup when the input box gets focus. | ||
*/ | ||
inputElement.bind( "focus", function onFocus() { | ||
// Without delay the popup can glitch close itself instantly after being opened. | ||
$timeout( openPopup, 150 ); | ||
scope.isFocused = true; | ||
} ); | ||
/** | ||
* Open the popup when the input box gets focus. | ||
*/ | ||
inputElement.bind( "focus", function() { | ||
// Without delay the popup can glitch close itself instantly after being opened. | ||
$timeout( openPopup, 150 ); | ||
scope.isFocused = true; | ||
} ); | ||
/** | ||
* Invoked when the input box loses focus. | ||
*/ | ||
inputElement.bind( "blur", function onBlur() { | ||
// Delay any action by 150ms | ||
$timeout( function checkFocusState() { | ||
// Check if we didn't get refocused in the meantime. | ||
// This can happen if the input box is selected and the user toggles the dropdown. | ||
// This would cause a hide and close in rapid succession, so don't do it. | ||
if( !$( inputElement ).is( ":focus" ) ) { | ||
scope.closePopup(); | ||
validateView(); | ||
} | ||
}, 150 ); | ||
scope.isFocused = false; | ||
} ); | ||
/** | ||
* Invoked when the input box loses focus. | ||
*/ | ||
inputElement.bind( "blur", function() { | ||
// Delay any action by 150ms | ||
$timeout( function() { | ||
// Check if we didn't get refocused in the meantime. | ||
// This can happen if the input box is selected and the user toggles the dropdown. | ||
// This would cause a hide and close in rapid succession, so don't do it. | ||
if( !$( inputElement ).is( ":focus" ) ) { | ||
scope.closePopup(); | ||
validateView(); | ||
} | ||
}, 150 ); | ||
scope.isFocused = false; | ||
} ); | ||
popupListElement.bind( "mousedown", function onMousedown( event ) { | ||
event.preventDefault(); | ||
} ); | ||
popupListElement.bind( "mousedown", function( event ) { | ||
if( typeof Hamster === "function" ) { | ||
Hamster( inputElement[ 0 ] ).wheel( function onMousewheel( event, delta, deltaX, deltaY ) { | ||
if( scope.isFocused ) { | ||
event.preventDefault(); | ||
} ); | ||
if( typeof Hamster === "function" ) { | ||
Hamster( inputElement[ 0 ] ).wheel( function( event, delta, deltaX, deltaY ) { | ||
if( scope.isFocused ) { | ||
event.preventDefault(); | ||
scope.activeIndex -= delta; | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, | ||
Math.max( 0, scope.activeIndex ) ); | ||
scope.activeIndex -= delta; | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, | ||
Math.max( 0, scope.activeIndex ) ); | ||
scope.select( scope.dropDownOptions[ scope.activeIndex ], scope.activeIndex ); | ||
$timeout( ensureUpdatedView ); | ||
} | ||
} ); | ||
scope.select( scope.dropDownOptions[ scope.activeIndex ], scope.activeIndex ); | ||
$timeout( ensureUpdatedView ); | ||
} | ||
} ); | ||
} | ||
} | ||
}; | ||
} | ||
] ); | ||
}; | ||
} | ||
fmTimepicker.$inject = ["$timeout"]; | ||
})(); |
@@ -1,1 +0,1 @@ | ||
!function(){"use strict";try{angular.module("fm.components")}catch(a){angular.module("fm.components",[])}angular.module("fm.components").filter("fmTimeFormat",function(){return function(a,b){return"number"==typeof a&&(a=moment(a)),moment(a).format(b)}}).filter("fmTimeInterval",function(){return function(a,b,c,d){if(!b||!c)return a;b=moment(b),c=moment(c),d=d||moment.duration(30,"minutes");for(var e=b.clone();+c>=+e;e.add(d))a.push(+e);return a}}).controller("fmTimepickerController",["$scope",function(a){if(a.reference=a.reference?moment(a.reference):moment(),a.style=a.style||"dropdown",a.isOpen=a.isOpen||!1,a.format=a.format||"LT",a.startTime=a.startTime||moment(a.reference).startOf("day"),a.endTime=a.endTime||moment(a.reference).endOf("day"),a.interval=a.interval||moment.duration(30,"minutes"),a.largeInterval=a.largeInterval||moment.duration(60,"minutes"),a.strict=a.strict||!1,a.btnClass=a.btnClass||"btn-default",moment.tz&&(a.startTime.tz(a.reference.tz()),a.endTime.tz(a.reference.tz())),a.strict){var b=a.ngModel.valueOf(),c=a.interval.asMilliseconds();b-=b%c,b+=c,a.ngModel=moment(b)}a.constrainToReference=function(b){return b?(moment.tz&&b.tz(a.reference.tz()),b.isSame(a.reference,"day")||b.year(a.reference.year()).month(a.reference.month()).date(a.reference.date()),b):(a.startTime.isSame(a.reference,"day")||a.startTime.year(a.reference.year()).month(a.reference.month()).date(a.reference.date()),a.endTime.isSame(a.reference,"day")||a.endTime.year(a.reference.year()).month(a.reference.month()).date(a.reference.date()),a.ngModel&&!a.ngModel.isSame(a.reference,"day")&&a.ngModel.year(a.reference.year()).month(a.reference.month()).date(a.reference.date()),null)},a.constrainToReference(),a.ensureTimeIsWithinBounds=function(b){return b&&moment.isMoment(b)?b.isBefore(a.startTime)?moment(a.startTime):b.isAfter(a.endTime)?moment(a.endTime):b:b},a.ngModel=a.ensureTimeIsWithinBounds(a.ngModel),a.findActiveIndex=function(b){if(a.activeIndex=0,b)for(var c=a.startTime.clone();+c<=+a.endTime&&!c.isSame(b);c.add(a.interval),++a.activeIndex)if(c.isAfter(b)){a.strict&&(a.activeIndex=-1),a.activeIndex-=1;break}},a.largestPossibleIndex=Number.MAX_VALUE,a.largeIntervalIndexJump=Number.MAX_VALUE,a.findActiveIndex(a.ngModel),a.$watch("interval",function(b,c){b.asMilliseconds()<1&&(console.error("[fm-timepicker] Error: Supplied interval length is smaller than 1ms! Reverting to default."),a.interval=moment.duration(30,"minutes"))}),a.$watch("largeInterval",function(b,c){b.asMilliseconds()<10&&(console.error("[fm-timepicker] Error: Supplied large interval length is smaller than 10ms! Reverting to default."),a.largeInterval=moment.duration(60,"minutes"))}),a.$watchCollection("[interval,largeInterval]",function(b){var c=b[0],d=b[1],e=c.asMilliseconds(),f=d.asMilliseconds();0!==f%e&&(console.warn("[fm-timepicker] Warning: Large interval is not a multiple of interval! Using internally computed value instead."),a.largeInterval=moment.duration(5*e),f=a.largeInterval.asMilliseconds()),a.largeIntervalIndexJump=f/e})}]).directive("fmTimepickerToggle",function(){return{restrict:"A",link:function(a,b,c){b.bind("click",function(){a.isOpen?(a.focusInputElement(),a.closePopup()):a.focusInputElement()})}}}).directive("fmTimepicker",["$timeout",function(a){return{template:"<div> <div class='input-group'> <span class='input-group-btn' ng-if='style==\"sequential\"'> <button type='button' class='btn {{btnClass}}' ng-click='decrement()' ng-disabled='activeIndex == 0 || disabled'> <span class='glyphicon glyphicon-minus'></span> </button> </span> <input type='text' class='form-control' ng-model='time' ng-keyup='handleKeyboardInput($event)' ng-change='update()' ng-disabled='disabled'> <span class='input-group-btn'> <button type='button' class='btn {{btnClass}}' ng-if='style==\"sequential\"' ng-click='increment()' ng-disabled='activeIndex == largestPossibleIndex || disabled'> <span class='glyphicon glyphicon-plus'></span> </button> <button type='button' class='btn {{btnClass}}' ng-if='style==\"dropdown\"' ng-class='{active:isOpen}' fm-timepicker-toggle ng-disabled='disabled'> <span class='glyphicon glyphicon-time'></span> </button> </span> </div> <div class='dropdown' ng-if='style==\"dropdown\" && isOpen' ng-class='{open:isOpen}'> <ul class='dropdown-menu form-control' style='height:auto; max-height:160px; overflow-y:scroll;' ng-mousedown=\"handleListClick($event)\"> <li ng-repeat='time in ( $parent.dropDownOptions = ( [] | fmTimeInterval:startTime:endTime:interval ) )' ng-click='select(time,$index)' ng-class='{active:(activeIndex==$index)}'> {{$last?largestPossibleIndexIs($index):angular.noop()}} <a href='#' ng-click='preventDefault($event)'>{{time|fmTimeFormat:format}}</a> </li> </ul> </div></div>",replace:!0,restrict:"EA",scope:{ngModel:"=",format:"=?",startTime:"=?",endTime:"=?",reference:"=?",interval:"=?",largeInterval:"=?",isOpen:"=?",style:"=?",strict:"=?",btnClass:"=?",disabled:"=?"},controller:"fmTimepickerController",require:"ngModel",link:function(b,c,d,e){function f(a){e.$setValidity("time",a),e.$setValidity("bounds",a),e.$setValidity("interval",a),e.$setValidity("start",a),e.$setValidity("end",a)}function g(){f(!0);var a=h(b.time);if(b.strict&&(a=a&&i(b.time)&&j(b.time)),b.startTime.isValid()||e.$setValidity("start",!1),b.endTime.isValid()||e.$setValidity("end",!1),a){var c;c=moment.tz?moment.tz(b.time,b.format,b.reference.tz()):moment(b.time,b.format),c=b.constrainToReference(c),e.$setViewValue(c),moment.tz?b.time=moment.tz(b.time,b.format,b.reference.tz()).format(b.format):b.time=moment(b.time,b.format).format(b.format)}}function h(a){var c;return c=moment.tz?a?moment.tz(a,b.format,b.reference.tz()):moment.invalid():a?moment(a,b.format):moment.invalid(),c.isValid()?(e.$setValidity("time",!0),!0):(e.$setValidity("time",!1),e.$setViewValue(null),!1)}function i(a){var c;return c=moment.tz?a?moment.tz(a,b.format,b.reference.tz()):moment.invalid():a?moment(a,b.format):moment.invalid(),c=b.constrainToReference(c),!c.isValid()||c.isBefore(b.startTime)||c.isAfter(b.endTime)?(e.$setValidity("bounds",!1),e.$setViewValue(null),!1):(e.$setValidity("bounds",!0),!0)}function j(a){var c;c=moment.tz?a?moment.tz(a,b.format,b.reference.tz()):moment.invalid():a?moment(a,b.format):moment.invalid();var d=c.isValid();if(d){var f=c.diff(b.startTime),g=b.interval.asMilliseconds();d=0===f%g}return d?(e.$setValidity("interval",!0),!0):(e.$setValidity("interval",!1),e.$setViewValue(null),!1)}function k(){a(function(){b.$apply()}),b.isOpen&&a(l)}function l(){var a=c.find("ul");$(a).scrollTop(0);var b=$("li.active",a),d=b.length?b.position().top:0,e=b.length?b.outerHeight(!0):0;$(a).scrollTop(d-e)}function m(){b.isOpen||(b.isOpen=!0,b.modelPreview=b.ngModel?b.ngModel.clone():b.startTime.clone(),a(k))}b.$watchCollection("[startTime,endTime,interval,strict]",function(){b.constrainToReference(),g()}),b.$watchCollection("[startTime,endTime,interval,ngModel]",function(){b.findActiveIndex(b.ngModel)}),e.$render=function(){var a=moment(e.$modelValue).format(b.format),c=h(a);if(b.strict&&(c=c&&i(a)&&j(a)),!c)throw new Error("The provided time value is invalid.");b.time=a},b.closePopup=function(c){c?a(function(){b.isOpen=!1},200):(b.isOpen=!1,a(k))},b.handleInputClick=function(a){"dropdown"===b.style&&m()},b.handleListClick=function(a){return a.preventDefault(),!1},b.select=function(a,c){var d;d=moment.tz&&b.reference.tz()?moment(a).tz(b.reference.tz()):moment(a),b.time=d.format(b.format),b.activeIndex=c,b.update(),b.closePopup()},b.increment=function(){b.isOpen?(b.modelPreview.add(b.interval),b.modelPreview=b.ensureTimeIsWithinBounds(b.modelPreview)):(b.ngModel.add(b.interval),b.ngModel=b.ensureTimeIsWithinBounds(b.ngModel),b.time=b.ngModel.format(b.format)),b.activeIndex=Math.min(b.largestPossibleIndex,b.activeIndex+1)},b.decrement=function(){b.isOpen?(b.modelPreview.subtract(b.interval),b.modelPreview=b.ensureTimeIsWithinBounds(b.modelPreview)):(b.ngModel.subtract(b.interval),b.ngModel=b.ensureTimeIsWithinBounds(b.ngModel),b.time=b.ngModel.format(b.format)),b.activeIndex=Math.max(0,b.activeIndex-1)},b.update=function(){var a=h(b.time)&&i(b.time);if(a){var c;c=moment.tz?moment.tz(b.time,b.format,b.reference.tz()):moment(b.time,b.format),c=b.constrainToReference(c),e.$setViewValue(c)}},b.handleKeyboardInput=function(c){switch(c.keyCode){case 13:b.modelPreview&&(b.ngModel=b.modelPreview,b.isOpen=!1);break;case 27:b.closePopup();break;case 33:m(),b.modelPreview.subtract(b.largeInterval),b.modelPreview=b.ensureTimeIsWithinBounds(b.modelPreview),b.activeIndex=Math.max(0,b.activeIndex-b.largeIntervalIndexJump);break;case 34:m(),b.modelPreview.add(b.largeInterval),b.modelPreview=b.ensureTimeIsWithinBounds(b.modelPreview),b.activeIndex=Math.min(b.largestPossibleIndex,b.activeIndex+b.largeIntervalIndexJump);break;case 38:m(),b.decrement();break;case 40:m(),b.increment()}a(k)},b.preventDefault=function(a){a.preventDefault()},b.largestPossibleIndexIs=function(a){b.largestPossibleIndex=a},b.focusInputElement=function(){$(n).focus()};var n=c.find("input"),o=c.find("ul");n.bind("focus",function(){a(m,150),b.isFocused=!0}),n.bind("blur",function(){a(function(){$(n).is(":focus")||(b.closePopup(),g())},150),b.isFocused=!1}),o.bind("mousedown",function(a){a.preventDefault()}),"function"==typeof Hamster&&Hamster(n[0]).wheel(function(c,d,e,f){b.isFocused&&(c.preventDefault(),b.activeIndex-=d,b.activeIndex=Math.min(b.largestPossibleIndex,Math.max(0,b.activeIndex)),b.select(b.dropDownOptions[b.activeIndex],b.activeIndex),a(k))})}}}])}(); | ||
!function(){"use strict";function a(){return function(a,b){return"number"==typeof a&&(a=moment(a)),moment(a).format(b)}}function b(){return function(a,b,c,d){if(!b||!c)return a;b=moment(b),c=moment(c),d=d||moment.duration(30,"minutes");for(var e=b.clone();+c>=+e;e.add(d))a.push(+e);return a}}function c(a){if(a.fmReference=a.fmReference?moment(a.fmReference):moment(),a.fmStyle=a.fmStyle||"dropdown",a.fmIsOpen=a.fmIsOpen||!1,a.fmFormat=a.fmFormat||"LT",a.fmStartTime=a.fmStartTime||moment(a.fmReference).startOf("day"),a.fmEndTime=a.fmEndTime||moment(a.fmReference).endOf("day"),a.fmInterval=a.fmInterval||moment.duration(30,"minutes"),a.fmLargeInterval=a.fmLargeInterval||moment.duration(60,"minutes"),a.fmStrict=a.fmStrict||!1,a.fmBtnClass=a.fmBtnClass||"btn-default",moment.tz&&(a.fmStartTime.tz(a.fmReference.tz()),a.fmEndTime.tz(a.fmReference.tz())),a.fmStrict){var b=a.ngModel.valueOf(),c=a.fmInterval.asMilliseconds();b-=b%c,b+=c,a.ngModel=moment(b)}a.constrainToReference=function(b){return b?(moment.tz&&b.tz(a.fmReference.tz()),b.isSame(a.fmReference,"day")||b.year(a.fmReference.year()).month(a.fmReference.month()).date(a.fmReference.date()),b):(a.fmStartTime.isSame(a.fmReference,"day")||a.fmStartTime.year(a.fmReference.year()).month(a.fmReference.month()).date(a.fmReference.date()),a.fmEndTime.isSame(a.fmReference,"day")||a.fmEndTime.year(a.fmReference.year()).month(a.fmReference.month()).date(a.fmReference.date()),a.ngModel&&!a.ngModel.isSame(a.fmReference,"day")&&a.ngModel.year(a.fmReference.year()).month(a.fmReference.month()).date(a.fmReference.date()),null)},a.constrainToReference(),a.ensureTimeIsWithinBounds=function(b){return b&&moment.isMoment(b)?b.isBefore(a.fmStartTime)?moment(a.fmStartTime):b.isAfter(a.fmEndTime)?moment(a.fmEndTime):b:b},a.ngModel=a.ensureTimeIsWithinBounds(a.ngModel),a.findActiveIndex=function(b){if(a.activeIndex=0,b)for(var c=a.fmStartTime.clone();+c<=+a.fmEndTime&&!c.isSame(b);c.add(a.fmInterval),++a.activeIndex)if(c.isAfter(b)){a.fmStrict&&(a.activeIndex=-1),a.activeIndex-=1;break}},a.largestPossibleIndex=Number.MAX_VALUE,a.largeIntervalIndexJump=Number.MAX_VALUE,a.findActiveIndex(a.ngModel),a.$watch("fmInterval",function(b,c){b.asMilliseconds()<1&&(console.error("[fm-timepicker] Error: Supplied interval length is smaller than 1ms! Reverting to default."),a.fmInterval=moment.duration(30,"minutes"))}),a.$watch("fmLargeInterval",function(b,c){b.asMilliseconds()<10&&(console.error("[fm-timepicker] Error: Supplied large interval length is smaller than 10ms! Reverting to default."),a.fmLargeInterval=moment.duration(60,"minutes"))}),a.$watchCollection("[fmInterval,fmLargeInterval]",function(b){var c=b[0],d=b[1],e=c.asMilliseconds(),f=d.asMilliseconds();0!==f%e&&(console.warn("[fm-timepicker] Warning: Large interval is not a multiple of interval! Using internally computed value instead."),a.fmLargeInterval=moment.duration(5*e),f=a.fmLargeInterval.asMilliseconds()),a.largeIntervalIndexJump=f/e})}function d(){return{restrict:"A",link:function(a,b,c){b.bind("click",function(){a.fmIsOpen?(a.focusInputElement(),a.closePopup()):a.focusInputElement()})}}}function e(a){return{templateUrl:"fmTimepicker.html",replace:!0,restrict:"EA",scope:{ngModel:"=",fmFormat:"=?",fmStartTime:"=?",fmEndTime:"=?",fmReference:"=?",fmInterval:"=?",fmLargeInterval:"=?",fmIsOpen:"=?",fmStyle:"=?",fmStrict:"=?",fmBtnClass:"=?",fmDisabled:"=?"},controller:"fmTimepickerController",require:"ngModel",link:function(b,c,d,e){function f(a){e.$setValidity("time",a),e.$setValidity("bounds",a),e.$setValidity("interval",a),e.$setValidity("start",a),e.$setValidity("end",a)}function g(){f(!0);var a=h(b.time);if(b.fmStrict&&(a=a&&i(b.time)&&j(b.time)),b.fmStartTime.isValid()||e.$setValidity("start",!1),b.fmEndTime.isValid()||e.$setValidity("end",!1),a){var c;c=moment.tz?moment.tz(b.time,b.fmFormat,b.fmReference.tz()):moment(b.time,b.fmFormat),c=b.constrainToReference(c),e.$setViewValue(c),moment.tz?b.time=moment.tz(b.time,b.fmFormat,b.fmReference.tz()).format(b.fmFormat):b.time=moment(b.time,b.fmFormat).format(b.fmFormat)}}function h(a){var c;return c=moment.tz?a?moment.tz(a,b.fmFormat,b.fmReference.tz()):moment.invalid():a?moment(a,b.fmFormat):moment.invalid(),c.isValid()?(e.$setValidity("time",!0),!0):(e.$setValidity("time",!1),e.$setViewValue(null),!1)}function i(a){var c;return c=moment.tz?a?moment.tz(a,b.fmFormat,b.fmReference.tz()):moment.invalid():a?moment(a,b.fmFormat):moment.invalid(),c=b.constrainToReference(c),!c.isValid()||c.isBefore(b.fmStartTime)||c.isAfter(b.fmEndTime)?(e.$setValidity("bounds",!1),e.$setViewValue(null),!1):(e.$setValidity("bounds",!0),!0)}function j(a){var c;c=moment.tz?a?moment.tz(a,b.fmFormat,b.fmReference.tz()):moment.invalid():a?moment(a,b.fmFormat):moment.invalid();var d=c.isValid();if(d){var f=c.diff(b.fmStartTime),g=b.fmInterval.asMilliseconds();d=0===f%g}return d?(e.$setValidity("interval",!0),!0):(e.$setValidity("interval",!1),e.$setViewValue(null),!1)}function k(){a(function(){b.$apply()}),b.fmIsOpen&&a(l)}function l(){var a=c.find("ul");$(a).scrollTop(0);var b=$("li.active",a),d=b.length?b.position().top:0,e=b.length?b.outerHeight(!0):0;$(a).scrollTop(d-e)}function m(){b.fmIsOpen||(b.fmIsOpen=!0,b.modelPreview=b.ngModel?b.ngModel.clone():b.fmStartTime.clone(),a(k))}b.$watchCollection("[fmStartTime,fmEndTime,fmInterval,fmStrict]",function(){b.constrainToReference(),g()}),b.$watchCollection("[fmStartTime,fmEndTime,fmInterval,ngModel]",function(){b.findActiveIndex(b.ngModel)}),e.$render=function(){var a=moment(e.$modelValue).format(b.fmFormat),c=h(a);if(b.fmStrict&&(c=c&&i(a)&&j(a)),!c)throw new Error("The provided time value is invalid.");b.time=a},b.closePopup=function(c){c?a(function(){b.fmIsOpen=!1},200):(b.fmIsOpen=!1,a(k))},b.handleListClick=function(a){return a.preventDefault(),!1},b.select=function(a,c){var d;d=moment.tz&&b.fmReference.tz()?moment(a).tz(b.fmReference.tz()):moment(a),b.time=d.format(b.fmFormat),b.activeIndex=c,b.update(),b.closePopup()},b.increment=function(){b.fmIsOpen?(b.modelPreview.add(b.fmInterval),b.modelPreview=b.ensureTimeIsWithinBounds(b.modelPreview)):(b.ngModel.add(b.fmInterval),b.ngModel=b.ensureTimeIsWithinBounds(b.ngModel),b.time=b.ngModel.format(b.fmFormat)),b.activeIndex=Math.min(b.largestPossibleIndex,b.activeIndex+1)},b.decrement=function(){b.fmIsOpen?(b.modelPreview.subtract(b.fmInterval),b.modelPreview=b.ensureTimeIsWithinBounds(b.modelPreview)):(b.ngModel.subtract(b.fmInterval),b.ngModel=b.ensureTimeIsWithinBounds(b.ngModel),b.time=b.ngModel.format(b.fmFormat)),b.activeIndex=Math.max(0,b.activeIndex-1)},b.update=function(){var a=h(b.time)&&i(b.time);if(a){var c;c=moment.tz?moment.tz(b.time,b.fmFormat,b.fmReference.tz()):moment(b.time,b.fmFormat),c=b.constrainToReference(c),e.$setViewValue(c)}},b.handleKeyboardInput=function(c){switch(c.keyCode){case 13:b.modelPreview&&(b.ngModel=b.modelPreview,b.fmIsOpen=!1);break;case 27:b.closePopup();break;case 33:m(),b.modelPreview.subtract(b.fmLargeInterval),b.modelPreview=b.ensureTimeIsWithinBounds(b.modelPreview),b.activeIndex=Math.max(0,b.activeIndex-b.largeIntervalIndexJump);break;case 34:m(),b.modelPreview.add(b.fmLargeInterval),b.modelPreview=b.ensureTimeIsWithinBounds(b.modelPreview),b.activeIndex=Math.min(b.largestPossibleIndex,b.activeIndex+b.largeIntervalIndexJump);break;case 38:m(),b.decrement();break;case 40:m(),b.increment()}a(k)},b.preventDefault=function(a){a.preventDefault()},b.largestPossibleIndexIs=function(a){b.largestPossibleIndex=a},b.focusInputElement=function(){$(n).focus()};var n=c.find("input"),o=c.find("ul");n.bind("focus",function(){a(m,150),b.isFocused=!0}),n.bind("blur",function(){a(function(){$(n).is(":focus")||(b.closePopup(),g())},150),b.isFocused=!1}),o.bind("mousedown",function(a){a.preventDefault()}),"function"==typeof Hamster&&Hamster(n[0]).wheel(function(c,d,e,f){b.isFocused&&(c.preventDefault(),b.activeIndex-=d,b.activeIndex=Math.min(b.largestPossibleIndex,Math.max(0,b.activeIndex)),b.select(b.dropDownOptions[b.activeIndex],b.activeIndex),a(k))})}}}angular.module("fmTimepicker",[]),angular.module("fmTimepicker").filter("fmTimeFormat",a).filter("fmTimeInterval",b).controller("fmTimepickerController",c).directive("fmTimepickerToggle",d).directive("fmTimepicker",e),c.$inject=["$scope"],e.$inject=["$timeout"]}(); |
@@ -19,7 +19,28 @@ module.exports = function( grunt ) { | ||
copy : { | ||
js : { | ||
html2js : { | ||
dist : { | ||
options : { | ||
module : "fmTimepicker", | ||
existingModule : true, | ||
singleModule : true, | ||
base : "src", | ||
htmlmin : { | ||
collapseWhitespace : true | ||
} | ||
}, | ||
files : [ { | ||
src : "src/fmTimepicker.html", | ||
dest : "dist/<%= pkg.name %>.html.js" | ||
} ] | ||
} | ||
}, | ||
ngAnnotate : { | ||
js : { | ||
src : "src/<%= pkg.name %>.js", | ||
dest : "dist/<%= pkg.name %>.js" | ||
}, | ||
} | ||
}, | ||
copy : { | ||
hamster : { | ||
@@ -31,6 +52,17 @@ src : "bower_components/hamsterjs/hamster.js", | ||
concat : { | ||
template : { | ||
src : [ "dist/<%= pkg.name %>.js", "dist/<%= pkg.name %>.html.js" ], | ||
dest : "dist/<%= pkg.name %>.tpls.js" | ||
} | ||
}, | ||
uglify : { | ||
js : { | ||
src : "src/<%= pkg.name %>.js", | ||
js : { | ||
src : "dist/<%= pkg.name %>.js", | ||
dest : "dist/<%= pkg.name %>.min.js" | ||
}, | ||
template : { | ||
src : "dist/<%= pkg.name %>.tpls.js", | ||
dest : "dist/<%= pkg.name %>.tpls.min.js" | ||
} | ||
@@ -53,2 +85,3 @@ }, | ||
grunt.loadNpmTasks( "grunt-contrib-concat" ); | ||
grunt.loadNpmTasks( "grunt-contrib-copy" ); | ||
@@ -59,4 +92,6 @@ grunt.loadNpmTasks( "grunt-contrib-jshint" ); | ||
grunt.loadNpmTasks( "grunt-gh-pages" ); | ||
grunt.loadNpmTasks( "grunt-html2js" ); | ||
grunt.loadNpmTasks( "grunt-ng-annotate" ); | ||
grunt.registerTask( "default", [ "jshint", "copy", "uglify" ] ); | ||
grunt.registerTask( "default", [ "jshint", "html2js", "ngAnnotate", "copy", "concat", "uglify" ] ); | ||
}; |
{ | ||
"name": "fm-timepicker", | ||
"version": "3.2.0", | ||
"version": "4.0.0", | ||
"description": "FairManager Time Picker Component", | ||
@@ -8,2 +8,3 @@ "main": "src/fm-timepicker.js", | ||
"grunt": "^0.4.5", | ||
"grunt-contrib-concat": "^0.5.1", | ||
"grunt-contrib-copy": "^0.5.0", | ||
@@ -13,3 +14,6 @@ "grunt-contrib-jshint": "^0.10.0", | ||
"grunt-contrib-watch": "^0.6.1", | ||
"grunt-gh-pages": "^0.9.1" | ||
"grunt-gh-pages": "^0.9.1", | ||
"grunt-html2js": "^0.3.5", | ||
"grunt-ng-annotate": "^1.0.1", | ||
"jscs": "^2.6.0" | ||
}, | ||
@@ -16,0 +20,0 @@ "scripts": { |
@@ -34,712 +34,671 @@ /** | ||
// Declare fmComponents module if it doesn't exist. | ||
try { | ||
angular.module( "fm.components" ); | ||
} catch( ignored ) { | ||
angular.module( "fm.components", [] ); | ||
} | ||
angular.module( "fmTimepicker", [] ); | ||
angular.module( "fm.components" ) | ||
.filter( "fmTimeFormat", function() { | ||
return function( input, format ) { | ||
if( typeof input === "number" ) { | ||
input = moment( input ); | ||
} | ||
return moment( input ).format( format ); | ||
}; | ||
} ) | ||
angular.module( "fmTimepicker" ) | ||
.filter( "fmTimeFormat", fmTimeFormat ) | ||
.filter( "fmTimeInterval", fmTimeInterval ) | ||
.controller( "fmTimepickerController", fmTimepickerController ) | ||
.directive( "fmTimepickerToggle", fmTimepickerToggle ) | ||
.directive( "fmTimepicker", fmTimepicker ); | ||
.filter( "fmTimeInterval", function() { | ||
return function( input, start, end, interval ) { | ||
if( !start || !end ) { | ||
return input; | ||
} | ||
function fmTimeFormat() { | ||
return function fmTimeFormatFilter( input, format ) { | ||
if( typeof input === "number" ) { | ||
input = moment( input ); | ||
} | ||
return moment( input ).format( format ); | ||
}; | ||
} | ||
start = moment( start ); | ||
end = moment( end ); | ||
interval = interval || moment.duration( 30, "minutes" ); | ||
for( var time = start.clone(); +time <= +end; time.add( interval ) ) { | ||
// We're using the UNIX offset integer value here. | ||
// When trying to return the actual moment instance (and then later format it through a filter), | ||
// you will get an infinite digest loop, because the returned objects in the resulting array | ||
// will always be new, unique instances. We always need to return the identical, literal values for each input. | ||
input.push( +time ); | ||
} | ||
function fmTimeInterval() { | ||
return function fmTimeIntervalFilter( input, start, end, interval ) { | ||
if( !start || !end ) { | ||
return input; | ||
}; | ||
} ) | ||
} | ||
.controller( "fmTimepickerController", [ "$scope", function( $scope ) { | ||
start = moment( start ); | ||
end = moment( end ); | ||
interval = interval || moment.duration( 30, "minutes" ); | ||
// Create day of reference | ||
$scope.reference = $scope.reference ? moment( $scope.reference ) : moment(); | ||
for( var time = start.clone(); +time <= +end; time.add( interval ) ) { | ||
// We're using the UNIX offset integer value here. | ||
// When trying to return the actual moment instance (and then later format it through a filter), | ||
// you will get an infinite digest loop, because the returned objects in the resulting array | ||
// will always be new, unique instances. We always need to return the identical, literal values for each input. | ||
input.push( +time ); | ||
} | ||
return input; | ||
}; | ||
} | ||
$scope.style = $scope.style || "dropdown"; | ||
$scope.isOpen = $scope.isOpen || false; | ||
$scope.format = $scope.format || "LT"; | ||
$scope.startTime = $scope.startTime || moment( $scope.reference ).startOf( "day" ); | ||
$scope.endTime = $scope.endTime || moment( $scope.reference ).endOf( "day" ); | ||
$scope.interval = $scope.interval || moment.duration( 30, "minutes" ); | ||
$scope.largeInterval = $scope.largeInterval || moment.duration( 60, "minutes" ); | ||
$scope.strict = $scope.strict || false; | ||
$scope.btnClass = $scope.btnClass || "btn-default"; | ||
/* @ngInject */ | ||
function fmTimepickerController( $scope ) { | ||
if( moment.tz ) { | ||
$scope.startTime.tz( $scope.reference.tz() ); | ||
$scope.endTime.tz( $scope.reference.tz() ); | ||
} | ||
// Create day of reference | ||
$scope.fmReference = $scope.fmReference ? moment( $scope.fmReference ) : moment(); | ||
if( $scope.strict ) { | ||
// Round the model value up to the next valid time that fits the configured interval. | ||
var modelMilliseconds = $scope.ngModel.valueOf(); | ||
var intervalMilliseconds = $scope.interval.asMilliseconds(); | ||
$scope.fmStyle = $scope.fmStyle || "dropdown"; | ||
$scope.fmIsOpen = $scope.fmIsOpen || false; | ||
$scope.fmFormat = $scope.fmFormat || "LT"; | ||
$scope.fmStartTime = $scope.fmStartTime || moment( $scope.fmReference ).startOf( "day" ); | ||
$scope.fmEndTime = $scope.fmEndTime || moment( $scope.fmReference ).endOf( "day" ); | ||
$scope.fmInterval = $scope.fmInterval || moment.duration( 30, "minutes" ); | ||
$scope.fmLargeInterval = $scope.fmLargeInterval || moment.duration( 60, "minutes" ); | ||
$scope.fmStrict = $scope.fmStrict || false; | ||
$scope.fmBtnClass = $scope.fmBtnClass || "btn-default"; | ||
modelMilliseconds -= modelMilliseconds % intervalMilliseconds; | ||
modelMilliseconds += intervalMilliseconds; | ||
if( moment.tz ) { | ||
$scope.fmStartTime.tz( $scope.fmReference.tz() ); | ||
$scope.fmEndTime.tz( $scope.fmReference.tz() ); | ||
} | ||
$scope.ngModel = moment( modelMilliseconds ); | ||
} | ||
if( $scope.fmStrict ) { | ||
// Round the model value up to the next valid time that fits the configured interval. | ||
var modelMilliseconds = $scope.ngModel.valueOf(); | ||
var intervalMilliseconds = $scope.fmInterval.asMilliseconds(); | ||
/** | ||
* Makes sure that the moment instances we work with all use the same day as reference. | ||
* We need this because we might construct moment instances from all kinds of sources, | ||
* in the time picker, we only care about time values though and we still want to compare | ||
* them through the moment mechanics (which respect the full date). | ||
* @param {Moment} [day] If day is given, it will be constrained to the reference day, otherwise all members will be constrained. | ||
* @return {Moment} If day was provided as parameter, it will be returned as well. | ||
*/ | ||
$scope.constrainToReference = function( day ) { | ||
if( day ) { | ||
if( moment.tz ) { | ||
day.tz( $scope.reference.tz() ); | ||
} | ||
modelMilliseconds -= modelMilliseconds % intervalMilliseconds; | ||
modelMilliseconds += intervalMilliseconds; | ||
if( !day.isSame( $scope.reference, "day" ) ) { | ||
day.year( $scope.reference.year() ).month( $scope.reference.month() ).date( $scope.reference.date() ); | ||
} | ||
return day; | ||
$scope.ngModel = moment( modelMilliseconds ); | ||
} | ||
} else { | ||
if( !$scope.startTime.isSame( $scope.reference, "day" ) ) { | ||
$scope.startTime.year( $scope.reference.year() ).month( $scope.reference.month() ).date( $scope.reference.date() ); | ||
} | ||
if( !$scope.endTime.isSame( $scope.reference, "day" ) ) { | ||
$scope.endTime.year( $scope.reference.year() ).month( $scope.reference.month() ).date( $scope.reference.date() ); | ||
} | ||
if( $scope.ngModel && !$scope.ngModel.isSame( $scope.reference, "day" ) ) { | ||
$scope.ngModel.year( $scope.reference.year() ).month( $scope.reference.month() ).date( $scope.reference.date() ); | ||
} | ||
/** | ||
* Makes sure that the moment instances we work with all use the same day as fmReference. | ||
* We need this because we might construct moment instances from all kinds of sources, | ||
* in the time picker, we only care about time values though and we still want to compare | ||
* them through the moment mechanics (which respect the full date). | ||
* @param {Moment} [day] If day is given, it will be constrained to the fmReference day, otherwise all members will be constrained. | ||
* @return {Moment} If day was provided as parameter, it will be returned as well. | ||
*/ | ||
$scope.constrainToReference = function( day ) { | ||
if( day ) { | ||
if( moment.tz ) { | ||
day.tz( $scope.fmReference.tz() ); | ||
} | ||
return null; | ||
}; | ||
$scope.constrainToReference(); | ||
/** | ||
* Returns a time value that is within the bounds given by the start and end time parameters. | ||
* @param {Moment} time The time value that should be constrained to be within the given bounds. | ||
* @returns {Moment} A new time value within the bounds, or the input instance. | ||
*/ | ||
$scope.ensureTimeIsWithinBounds = function( time ) { | ||
// We expect "time" to be a Moment instance; otherwise bail. | ||
if( !time || !moment.isMoment( time ) ) { | ||
return time; | ||
if( !day.isSame( $scope.fmReference, "day" ) ) { | ||
day.year( $scope.fmReference.year() ).month( $scope.fmReference.month() ).date( $scope.fmReference.date() ); | ||
} | ||
// Constrain model value to be in given bounds. | ||
if( time.isBefore( $scope.startTime ) ) { | ||
return moment( $scope.startTime ); | ||
return day; | ||
} else { | ||
if( !$scope.fmStartTime.isSame( $scope.fmReference, "day" ) ) { | ||
$scope.fmStartTime.year( $scope.fmReference.year() ).month( $scope.fmReference.month() ).date( | ||
$scope.fmReference.date() ); | ||
} | ||
if( time.isAfter( $scope.endTime ) ) { | ||
return moment( $scope.endTime ); | ||
if( !$scope.fmEndTime.isSame( $scope.fmReference, "day" ) ) { | ||
$scope.fmEndTime.year( $scope.fmReference.year() ).month( $scope.fmReference.month() ).date( $scope.fmReference.date() ); | ||
} | ||
if( $scope.ngModel && !$scope.ngModel.isSame( $scope.fmReference, "day" ) ) { | ||
$scope.ngModel.year( $scope.fmReference.year() ).month( $scope.fmReference.month() ).date( $scope.fmReference.date() ); | ||
} | ||
} | ||
return null; | ||
}; | ||
$scope.constrainToReference(); | ||
/** | ||
* Returns a time value that is within the bounds given by the start and end time parameters. | ||
* @param {Moment} time The time value that should be constrained to be within the given bounds. | ||
* @returns {Moment} A new time value within the bounds, or the input instance. | ||
*/ | ||
$scope.ensureTimeIsWithinBounds = function( time ) { | ||
// We expect "time" to be a Moment instance; otherwise bail. | ||
if( !time || !moment.isMoment( time ) ) { | ||
return time; | ||
}; | ||
$scope.ngModel = $scope.ensureTimeIsWithinBounds( $scope.ngModel ); | ||
} | ||
// Constrain model value to be in given bounds. | ||
if( time.isBefore( $scope.fmStartTime ) ) { | ||
return moment( $scope.fmStartTime ); | ||
} | ||
if( time.isAfter( $scope.fmEndTime ) ) { | ||
return moment( $scope.fmEndTime ); | ||
} | ||
return time; | ||
}; | ||
$scope.ngModel = $scope.ensureTimeIsWithinBounds( $scope.ngModel ); | ||
/** | ||
* Utility method to find the index of an item, in our collection of possible values, that matches a given time value. | ||
* @param {Moment} model A moment instance to look for in our possible values. | ||
*/ | ||
$scope.findActiveIndex = function( model ) { | ||
$scope.activeIndex = 0; | ||
if( !model ) { | ||
return; | ||
/** | ||
* Utility method to find the index of an item, in our collection of possible values, that matches a given time value. | ||
* @param {Moment} model A moment instance to look for in our possible values. | ||
*/ | ||
$scope.findActiveIndex = function( model ) { | ||
$scope.activeIndex = 0; | ||
if( !model ) { | ||
return; | ||
} | ||
// We step through each possible value instead of calculating the index directly, | ||
// to make sure we account for DST changes in the reference day. | ||
for( var time = $scope.fmStartTime.clone(); +time <= +$scope.fmEndTime; time.add( $scope.fmInterval ), ++$scope.activeIndex ) { | ||
if( time.isSame( model ) ) { | ||
break; | ||
} | ||
// We step through each possible value instead of calculating the index directly, | ||
// to make sure we account for DST changes in the reference day. | ||
for( var time = $scope.startTime.clone(); +time <= +$scope.endTime; time.add( $scope.interval ), ++$scope.activeIndex ) { | ||
if( time.isSame( model ) ) { | ||
break; | ||
// Check if we've already passed the time value that would fit our current model. | ||
if( time.isAfter( model ) ) { | ||
// If we're in strict mode, set an invalid index. | ||
if( $scope.fmStrict ) { | ||
$scope.activeIndex = -1; | ||
} | ||
// Check if we've already passed the time value that would fit our current model. | ||
if( time.isAfter( model ) ) { | ||
// If we're in strict mode, set an invalid index. | ||
if( $scope.strict ) { | ||
$scope.activeIndex = -1; | ||
} | ||
// If we're not in strict mode, decrease the index to select the previous item (the one we just passed). | ||
$scope.activeIndex -= 1; | ||
// Now bail out and use whatever index we determined. | ||
break; | ||
} | ||
// If we're not in strict mode, decrease the index to select the previous item (the one we just passed). | ||
$scope.activeIndex -= 1; | ||
// Now bail out and use whatever index we determined. | ||
break; | ||
} | ||
}; | ||
// The index of the last element in our time value collection. | ||
$scope.largestPossibleIndex = Number.MAX_VALUE; | ||
// The amount of list items we should skip when we perform a large jump through the collection. | ||
$scope.largeIntervalIndexJump = Number.MAX_VALUE; | ||
// Seed the active index based on the current model value. | ||
$scope.findActiveIndex( $scope.ngModel ); | ||
} | ||
}; | ||
// The index of the last element in our time value collection. | ||
$scope.largestPossibleIndex = Number.MAX_VALUE; | ||
// The amount of list items we should skip when we perform a large jump through the collection. | ||
$scope.largeIntervalIndexJump = Number.MAX_VALUE; | ||
// Seed the active index based on the current model value. | ||
$scope.findActiveIndex( $scope.ngModel ); | ||
// Check the supplied interval for validity. | ||
$scope.$watch( "interval", function( newInterval, oldInterval ) { | ||
if( newInterval.asMilliseconds() < 1 ) { | ||
console.error( | ||
"[fm-timepicker] Error: Supplied interval length is smaller than 1ms! Reverting to default." ); | ||
$scope.interval = moment.duration( 30, "minutes" ); | ||
} | ||
} ); | ||
// Check the supplied large interval for validity. | ||
$scope.$watch( "largeInterval", function( newInterval, oldInterval ) { | ||
if( newInterval.asMilliseconds() < 10 ) { | ||
console.error( | ||
"[fm-timepicker] Error: Supplied large interval length is smaller than 10ms! Reverting to default." ); | ||
$scope.largeInterval = moment.duration( 60, "minutes" ); | ||
} | ||
} ); | ||
// Watch the given interval values. | ||
$scope.$watchCollection( "[interval,largeInterval]", function( newValues ) { | ||
// Pick array apart. | ||
var newInterval = newValues[ 0 ]; | ||
var newLargeInterval = newValues[ 1 ]; | ||
// Get millisecond values for the intervals. | ||
var newIntervalMilliseconds = newInterval.asMilliseconds(); | ||
var newLargeIntervalMilliseconds = newLargeInterval.asMilliseconds(); | ||
// Check if the large interval is a multiple of the interval. | ||
if( 0 !== ( newLargeIntervalMilliseconds % newIntervalMilliseconds ) ) { | ||
console.warn( | ||
"[fm-timepicker] Warning: Large interval is not a multiple of interval! Using internally computed value instead." ); | ||
$scope.largeInterval = moment.duration( newIntervalMilliseconds * 5 ); | ||
newLargeIntervalMilliseconds = $scope.largeInterval.asMilliseconds(); | ||
} | ||
// Calculate how many indices we need to skip for a large jump through our collection. | ||
$scope.largeIntervalIndexJump = newLargeIntervalMilliseconds / newIntervalMilliseconds; | ||
} ); | ||
} ] ) | ||
// Check the supplied interval for validity. | ||
$scope.$watch( "fmInterval", function intervalWatcher( newInterval, oldInterval ) { | ||
if( newInterval.asMilliseconds() < 1 ) { | ||
console.error( | ||
"[fm-timepicker] Error: Supplied interval length is smaller than 1ms! Reverting to default." ); | ||
$scope.fmInterval = moment.duration( 30, "minutes" ); | ||
} | ||
} ); | ||
// Check the supplied large interval for validity. | ||
$scope.$watch( "fmLargeInterval", function largeIntervalWatcher( newInterval, oldInterval ) { | ||
if( newInterval.asMilliseconds() < 10 ) { | ||
console.error( | ||
"[fm-timepicker] Error: Supplied large interval length is smaller than 10ms! Reverting to default." ); | ||
$scope.fmLargeInterval = moment.duration( 60, "minutes" ); | ||
} | ||
} ); | ||
// Watch the given interval values. | ||
$scope.$watchCollection( "[fmInterval,fmLargeInterval]", function intervalsWatcher( newValues ) { | ||
// Pick array apart. | ||
var newInterval = newValues[ 0 ]; | ||
var newLargeInterval = newValues[ 1 ]; | ||
// Get millisecond values for the intervals. | ||
var newIntervalMilliseconds = newInterval.asMilliseconds(); | ||
var newLargeIntervalMilliseconds = newLargeInterval.asMilliseconds(); | ||
// Check if the large interval is a multiple of the interval. | ||
if( 0 !== ( newLargeIntervalMilliseconds % newIntervalMilliseconds ) ) { | ||
console.warn( | ||
"[fm-timepicker] Warning: Large interval is not a multiple of interval! Using internally computed value instead." ); | ||
$scope.fmLargeInterval = moment.duration( newIntervalMilliseconds * 5 ); | ||
newLargeIntervalMilliseconds = $scope.fmLargeInterval.asMilliseconds(); | ||
} | ||
// Calculate how many indices we need to skip for a large jump through our collection. | ||
$scope.largeIntervalIndexJump = newLargeIntervalMilliseconds / newIntervalMilliseconds; | ||
} ); | ||
} | ||
.directive( "fmTimepickerToggle", function() { | ||
return { | ||
restrict : "A", | ||
link : function postLink( scope, element, attributes ) { | ||
// Toggle the popup when the toggle button is clicked. | ||
element.bind( "click", function() { | ||
if( scope.isOpen ) { | ||
scope.focusInputElement(); | ||
scope.closePopup(); | ||
} else { | ||
// Focusing the input element will automatically open the popup | ||
scope.focusInputElement(); | ||
} | ||
} ); | ||
} | ||
}; | ||
} ) | ||
function fmTimepickerToggle() { | ||
return { | ||
restrict : "A", | ||
link : function postLink( scope, element, attributes ) { | ||
// Toggle the popup when the toggle button is clicked. | ||
element.bind( "click", function onClick() { | ||
if( scope.fmIsOpen ) { | ||
scope.focusInputElement(); | ||
scope.closePopup(); | ||
} else { | ||
// Focusing the input element will automatically open the popup | ||
scope.focusInputElement(); | ||
} | ||
} ); | ||
} | ||
}; | ||
} | ||
.directive( "fmTimepicker", [ | ||
"$timeout", function( $timeout ) { | ||
return { | ||
template : "<div>" + | ||
" <div class='input-group'>" + | ||
" <span class='input-group-btn' ng-if='style==\"sequential\"'>" + | ||
" <button type='button' class='btn {{btnClass}}' ng-click='decrement()' ng-disabled='activeIndex == 0 || disabled'>" + | ||
" <span class='glyphicon glyphicon-minus'></span>" + | ||
" </button>" + | ||
" </span>" + | ||
" <input type='text' class='form-control' ng-model='time' ng-keyup='handleKeyboardInput($event)' ng-change='update()' ng-disabled='disabled'>" + | ||
" <span class='input-group-btn'>" + | ||
" <button type='button' class='btn {{btnClass}}' ng-if='style==\"sequential\"' ng-click='increment()' ng-disabled='activeIndex == largestPossibleIndex || disabled'>" + | ||
" <span class='glyphicon glyphicon-plus'></span>" + | ||
" </button>" + | ||
" <button type='button' class='btn {{btnClass}}' ng-if='style==\"dropdown\"' ng-class='{active:isOpen}' fm-timepicker-toggle ng-disabled='disabled'>" + | ||
" <span class='glyphicon glyphicon-time'></span>" + | ||
" </button>" + | ||
" </span>" + | ||
" </div>" + | ||
" <div class='dropdown' ng-if='style==\"dropdown\" && isOpen' ng-class='{open:isOpen}'>" + | ||
" <ul class='dropdown-menu form-control' style='height:auto; max-height:160px; overflow-y:scroll;' ng-mousedown=\"handleListClick($event)\">" + | ||
// Fill an empty array with time values between start and end time with the given interval, then iterate over that array. | ||
" <li ng-repeat='time in ( $parent.dropDownOptions = ( [] | fmTimeInterval:startTime:endTime:interval ) )' ng-click='select(time,$index)' ng-class='{active:(activeIndex==$index)}'>" + | ||
// For each item, check if it is the last item. If it is, communicate the index to a method in the scope. | ||
" {{$last?largestPossibleIndexIs($index):angular.noop()}}" + | ||
// Render a link into the list item, with the formatted time value. | ||
" <a href='#' ng-click='preventDefault($event)'>{{time|fmTimeFormat:format}}</a>" + | ||
" </li>" + | ||
" </ul>" + | ||
" </div>" + | ||
"</div>", | ||
replace : true, | ||
restrict : "EA", | ||
scope : { | ||
ngModel : "=", | ||
format : "=?", | ||
startTime : "=?", | ||
endTime : "=?", | ||
reference : "=?", | ||
interval : "=?", | ||
largeInterval : "=?", | ||
isOpen : "=?", | ||
style : "=?", | ||
strict : "=?", | ||
btnClass : "=?", | ||
disabled : "=?" | ||
}, | ||
controller : "fmTimepickerController", | ||
require : "ngModel", | ||
link : function postLink( scope, element, attributes, controller ) { | ||
// Watch our input parameters and re-validate our view when they change. | ||
scope.$watchCollection( "[startTime,endTime,interval,strict]", function() { | ||
scope.constrainToReference(); | ||
validateView(); | ||
} ); | ||
/* @ngInject */ | ||
function fmTimepicker( $timeout ) { | ||
return { | ||
templateUrl : "fmTimepicker.html", | ||
replace : true, | ||
restrict : "EA", | ||
scope : { | ||
ngModel : "=", | ||
fmFormat : "=?", | ||
fmStartTime : "=?", | ||
fmEndTime : "=?", | ||
fmReference : "=?", | ||
fmInterval : "=?", | ||
fmLargeInterval : "=?", | ||
fmIsOpen : "=?", | ||
fmStyle : "=?", | ||
fmStrict : "=?", | ||
fmBtnClass : "=?", | ||
fmDisabled : "=?" | ||
}, | ||
controller : "fmTimepickerController", | ||
require : "ngModel", | ||
link : function postLink( scope, element, attributes, controller ) { | ||
// Watch our input parameters and re-validate our view when they change. | ||
scope.$watchCollection( "[fmStartTime,fmEndTime,fmInterval,fmStrict]", function inputWatcher() { | ||
scope.constrainToReference(); | ||
validateView(); | ||
} ); | ||
// Watch all time related parameters. | ||
scope.$watchCollection( "[startTime,endTime,interval,ngModel]", function() { | ||
// When they change, find the index of the element in the dropdown that relates to the current model value. | ||
scope.findActiveIndex( scope.ngModel ); | ||
} ); | ||
// Watch all time related parameters. | ||
scope.$watchCollection( "[fmStartTime,fmEndTime,fmInterval,ngModel]", function timeWatcher() { | ||
// When they change, find the index of the element in the dropdown that relates to the current model value. | ||
scope.findActiveIndex( scope.ngModel ); | ||
} ); | ||
/** | ||
* Invoked when we need to update the view due to a changed model value. | ||
*/ | ||
controller.$render = function() { | ||
// Convert the moment instance we got to a string in our desired format. | ||
var time = moment( controller.$modelValue ).format( scope.format ); | ||
// Check if the given time is valid. | ||
var timeValid = checkTimeValueValid( time ); | ||
if( scope.strict ) { | ||
timeValid = timeValid && checkTimeValueWithinBounds( time ) && checkTimeValueFitsInterval( | ||
time ); | ||
} | ||
/** | ||
* Invoked when we need to update the view due to a changed model value. | ||
*/ | ||
controller.$render = function() { | ||
// Convert the moment instance we got to a string in our desired format. | ||
var time = moment( controller.$modelValue ).format( scope.fmFormat ); | ||
// Check if the given time is valid. | ||
var timeValid = checkTimeValueValid( time ); | ||
if( scope.fmStrict ) { | ||
timeValid = timeValid && checkTimeValueWithinBounds( time ) && checkTimeValueFitsInterval( | ||
time ); | ||
} | ||
if( timeValid ) { | ||
// If the time is valid, store the time string in the scope used by the input box. | ||
scope.time = time; | ||
} else { | ||
throw new Error( "The provided time value is invalid." ); | ||
} | ||
}; | ||
if( timeValid ) { | ||
// If the time is valid, store the time string in the scope used by the input box. | ||
scope.time = time; | ||
} else { | ||
throw new Error( "The provided time value is invalid." ); | ||
} | ||
}; | ||
/** | ||
* Reset the validity of the directive. | ||
* @param {Boolean} to What to set the validity to? | ||
*/ | ||
function resetValidity( to ) { | ||
controller.$setValidity( "time", to ); | ||
controller.$setValidity( "bounds", to ); | ||
controller.$setValidity( "interval", to ); | ||
controller.$setValidity( "start", to ); | ||
controller.$setValidity( "end", to ); | ||
} | ||
/** | ||
* Reset the validity of the directive. | ||
* @param {Boolean} to What to set the validity to? | ||
*/ | ||
function resetValidity( to ) { | ||
controller.$setValidity( "time", to ); | ||
controller.$setValidity( "bounds", to ); | ||
controller.$setValidity( "interval", to ); | ||
controller.$setValidity( "start", to ); | ||
controller.$setValidity( "end", to ); | ||
} | ||
/** | ||
* Check if the value in the view is valid. | ||
* It has to represent a valid time in itself and it has to fit within the constraints defined through our input parameters. | ||
*/ | ||
function validateView() { | ||
resetValidity( true ); | ||
// Check if the string in the input box represents a valid date according to the rules set through parameters in our scope. | ||
var timeValid = checkTimeValueValid( scope.time ); | ||
if( scope.strict ) { | ||
timeValid = timeValid && checkTimeValueWithinBounds( scope.time ) && checkTimeValueFitsInterval( | ||
scope.time ); | ||
} | ||
/** | ||
* Check if the value in the view is valid. | ||
* It has to represent a valid time in itself and it has to fit within the constraints defined through our input parameters. | ||
*/ | ||
function validateView() { | ||
resetValidity( true ); | ||
// Check if the string in the input box represents a valid date according to the rules set through parameters in our scope. | ||
var timeValid = checkTimeValueValid( scope.time ); | ||
if( scope.fmStrict ) { | ||
timeValid = timeValid && checkTimeValueWithinBounds( scope.time ) && checkTimeValueFitsInterval( | ||
scope.time ); | ||
} | ||
if( !scope.startTime.isValid() ) { | ||
controller.$setValidity( "start", false ); | ||
} | ||
if( !scope.endTime.isValid() ) { | ||
controller.$setValidity( "end", false ); | ||
} | ||
if( !scope.fmStartTime.isValid() ) { | ||
controller.$setValidity( "start", false ); | ||
} | ||
if( !scope.fmEndTime.isValid() ) { | ||
controller.$setValidity( "end", false ); | ||
} | ||
if( timeValid ) { | ||
// If the string is valid, convert it to a moment instance, store in the model and... | ||
var newTime; | ||
if( moment.tz ) { | ||
newTime = moment.tz( | ||
scope.time, | ||
scope.format, | ||
scope.reference.tz() ); | ||
} else { | ||
newTime = moment( scope.time, scope.format ); | ||
} | ||
newTime = scope.constrainToReference( newTime ); | ||
controller.$setViewValue( newTime ); | ||
// ...convert it back to a string in our desired format. | ||
// This allows the user to input any partial format that moment accepts and we'll convert it to the format we expect. | ||
if( moment.tz ) { | ||
scope.time = moment.tz( | ||
scope.time, | ||
scope.format, | ||
scope.reference.tz() ).format( scope.format ); | ||
} else { | ||
scope.time = moment( scope.time, scope.format ).format( scope.format ); | ||
} | ||
} | ||
if( timeValid ) { | ||
// If the string is valid, convert it to a moment instance, store in the model and... | ||
var newTime; | ||
if( moment.tz ) { | ||
newTime = moment.tz( | ||
scope.time, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ); | ||
} else { | ||
newTime = moment( scope.time, scope.fmFormat ); | ||
} | ||
/** | ||
* Check if a given string represents a valid time in our expected format. | ||
* @param {String} timeString The timestamp is the expected format. | ||
* @returns {boolean} true if the string is a valid time; false otherwise. | ||
*/ | ||
function checkTimeValueValid( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.format, | ||
scope.reference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.format ) : moment.invalid(); | ||
} | ||
if( !time.isValid() ) { | ||
controller.$setValidity( "time", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "time", true ); | ||
return true; | ||
} | ||
newTime = scope.constrainToReference( newTime ); | ||
controller.$setViewValue( newTime ); | ||
// ...convert it back to a string in our desired format. | ||
// This allows the user to input any partial format that moment accepts and we'll convert it to the format we expect. | ||
if( moment.tz ) { | ||
scope.time = moment.tz( | ||
scope.time, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ).format( scope.fmFormat ); | ||
} else { | ||
scope.time = moment( scope.time, scope.fmFormat ).format( scope.fmFormat ); | ||
} | ||
} | ||
} | ||
/** | ||
* Check if a given string represents a time within the bounds specified through our start and end times. | ||
* @param {String} timeString The timestamp is the expected format. | ||
* @returns {boolean} true if the string represents a valid time and the time is within the defined bounds; false otherwise. | ||
*/ | ||
function checkTimeValueWithinBounds( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.format, | ||
scope.reference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.format ) : moment.invalid(); | ||
} | ||
time = scope.constrainToReference( time ); | ||
if( !time.isValid() || time.isBefore( scope.startTime ) || time.isAfter( scope.endTime ) ) { | ||
controller.$setValidity( "bounds", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "bounds", true ); | ||
return true; | ||
} | ||
} | ||
/** | ||
* Check if a given string represents a valid time in our expected format. | ||
* @param {String} timeString The timestamp is the expected format. | ||
* @returns {boolean} true if the string is a valid time; false otherwise. | ||
*/ | ||
function checkTimeValueValid( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.fmFormat ) : moment.invalid(); | ||
} | ||
if( !time.isValid() ) { | ||
controller.$setValidity( "time", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "time", true ); | ||
return true; | ||
} | ||
} | ||
/** | ||
* Check if a given string represents a time that lies on a the boundary of a time interval. | ||
* @param {String} timeString The timestamp in the expected format. | ||
* @returns {boolean} true if the string represents a valid time and that time lies on an interval boundary; false otherwise. | ||
*/ | ||
function checkTimeValueFitsInterval( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.format, | ||
scope.reference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.format ) : moment.invalid(); | ||
} | ||
// Check first if the time string could be parsed as a valid timestamp. | ||
var isValid = time.isValid(); | ||
if( isValid ) { | ||
// Calculate the amount of milliseconds that passed since the specified start time. | ||
var durationSinceStartTime = time.diff( scope.startTime ); | ||
// Calculate how many milliseconds are within the given time interval. | ||
var intervalMilliseconds = scope.interval.asMilliseconds(); | ||
// Check if the modulo operation has a remainder. | ||
isValid = ( 0 === ( durationSinceStartTime % intervalMilliseconds ) ); | ||
} | ||
/** | ||
* Check if a given string represents a time within the bounds specified through our start and end times. | ||
* @param {String} timeString The timestamp is the expected format. | ||
* @returns {boolean} true if the string represents a valid time and the time is within the defined bounds; false otherwise. | ||
*/ | ||
function checkTimeValueWithinBounds( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.fmFormat ) : moment.invalid(); | ||
} | ||
time = scope.constrainToReference( time ); | ||
if( !time.isValid() || time.isBefore( scope.fmStartTime ) || time.isAfter( scope.fmEndTime ) ) { | ||
controller.$setValidity( "bounds", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "bounds", true ); | ||
return true; | ||
} | ||
} | ||
if( !isValid ) { | ||
controller.$setValidity( "interval", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "interval", true ); | ||
return true; | ||
} | ||
} | ||
/** | ||
* Check if a given string represents a time that lies on a the boundary of a time interval. | ||
* @param {String} timeString The timestamp in the expected format. | ||
* @returns {boolean} true if the string represents a valid time and that time lies on an interval boundary; false otherwise. | ||
*/ | ||
function checkTimeValueFitsInterval( timeString ) { | ||
var time; | ||
if( moment.tz ) { | ||
time = timeString ? moment.tz( | ||
timeString, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ) : moment.invalid(); | ||
} else { | ||
time = timeString ? moment( timeString, scope.fmFormat ) : moment.invalid(); | ||
} | ||
// Check first if the time string could be parsed as a valid timestamp. | ||
var isValid = time.isValid(); | ||
if( isValid ) { | ||
// Calculate the amount of milliseconds that passed since the specified start time. | ||
var durationSinceStartTime = time.diff( scope.fmStartTime ); | ||
// Calculate how many milliseconds are within the given time interval. | ||
var intervalMilliseconds = scope.fmInterval.asMilliseconds(); | ||
// Check if the modulo operation has a remainder. | ||
isValid = ( 0 === ( durationSinceStartTime % intervalMilliseconds ) ); | ||
} | ||
function ensureUpdatedView() { | ||
$timeout( function() { | ||
scope.$apply(); | ||
} ); | ||
if( !isValid ) { | ||
controller.$setValidity( "interval", false ); | ||
controller.$setViewValue( null ); | ||
return false; | ||
} else { | ||
controller.$setValidity( "interval", true ); | ||
return true; | ||
} | ||
} | ||
// Scroll the selected list item into view if the popup is open. | ||
if( scope.isOpen ) { | ||
// Use $timeout to give the DOM time to catch up. | ||
$timeout( scrollSelectedItemIntoView ); | ||
} | ||
} | ||
function ensureUpdatedView() { | ||
$timeout( function runDigest() { | ||
scope.$apply(); | ||
} ); | ||
/** | ||
* Scroll the time that is currently selected into view. | ||
* This applies to the dropdown below the input element. | ||
*/ | ||
function scrollSelectedItemIntoView() { | ||
// Find the popup. | ||
var popupListElement = element.find( "ul" ); | ||
// Scroll it to the top, so that we can then get the correct relative offset for all list items. | ||
$( popupListElement ).scrollTop( 0 ); | ||
// Find the selected list item. | ||
var selectedListElement = $( "li.active", popupListElement ); | ||
// Retrieve offset from the top and height of the list element. | ||
var top = selectedListElement.length ? selectedListElement.position().top : 0; | ||
var height = selectedListElement.length ? selectedListElement.outerHeight( true ) : 0; | ||
// Scroll the list to bring the selected list element into the view. | ||
$( popupListElement ).scrollTop( top - height ); | ||
} | ||
// Scroll the selected list item into view if the popup is open. | ||
if( scope.fmIsOpen ) { | ||
// Use $timeout to give the DOM time to catch up. | ||
$timeout( scrollSelectedItemIntoView ); | ||
} | ||
} | ||
/** | ||
* Open the popup dropdown list. | ||
*/ | ||
function openPopup() { | ||
if( !scope.isOpen ) { | ||
scope.isOpen = true; | ||
scope.modelPreview = scope.ngModel ? scope.ngModel.clone() : scope.startTime.clone(); | ||
$timeout( ensureUpdatedView ); | ||
} | ||
} | ||
/** | ||
* Scroll the time that is currently selected into view. | ||
* This applies to the dropdown below the input element. | ||
*/ | ||
function scrollSelectedItemIntoView() { | ||
// Find the popup. | ||
var popupListElement = element.find( "ul" ); | ||
// Scroll it to the top, so that we can then get the correct relative offset for all list items. | ||
$( popupListElement ).scrollTop( 0 ); | ||
// Find the selected list item. | ||
var selectedListElement = $( "li.active", popupListElement ); | ||
// Retrieve offset from the top and height of the list element. | ||
var top = selectedListElement.length ? selectedListElement.position().top : 0; | ||
var height = selectedListElement.length ? selectedListElement.outerHeight( true ) : 0; | ||
// Scroll the list to bring the selected list element into the view. | ||
$( popupListElement ).scrollTop( top - height ); | ||
} | ||
// --------------- Scope methods --------------- | ||
/** | ||
* Open the popup dropdown list. | ||
*/ | ||
function openPopup() { | ||
if( !scope.fmIsOpen ) { | ||
scope.fmIsOpen = true; | ||
scope.modelPreview = scope.ngModel ? scope.ngModel.clone() : scope.fmStartTime.clone(); | ||
$timeout( ensureUpdatedView ); | ||
} | ||
} | ||
/** | ||
* Close the popup dropdown list. | ||
*/ | ||
scope.closePopup = function( delayed ) { | ||
if( delayed ) { | ||
// Delay closing the popup by 200ms to ensure selection of | ||
// list items can happen before the popup is hidden. | ||
$timeout( | ||
function() { | ||
scope.isOpen = false; | ||
}, 200 ); | ||
} else { | ||
scope.isOpen = false; | ||
$timeout( ensureUpdatedView ); | ||
} | ||
}; | ||
// --------------- Scope methods --------------- | ||
/** | ||
* This function is meant to handle clicking on the time input | ||
* box if it is already focused. Previously nothing would happen | ||
* and you would have to click the button or leave focus and | ||
* reclick to get the popup to open again. Adding this as a click | ||
* event makes it pop open again even if the input is focused. | ||
*/ | ||
scope.handleInputClick = function handleInputClick( $event ) { | ||
// bail if we aren't doing a dropdown | ||
if (scope.style !== "dropdown") { | ||
return; | ||
} | ||
/** | ||
* Close the popup dropdown list. | ||
*/ | ||
scope.closePopup = function( delayed ) { | ||
if( delayed ) { | ||
// Delay closing the popup by 200ms to ensure selection of | ||
// list items can happen before the popup is hidden. | ||
$timeout( | ||
function closeDropdown() { | ||
scope.fmIsOpen = false; | ||
}, 200 ); | ||
} else { | ||
scope.fmIsOpen = false; | ||
$timeout( ensureUpdatedView ); | ||
} | ||
}; | ||
openPopup(); | ||
}; | ||
scope.handleListClick = function handleListClick( $event ) { | ||
// When the list scrollbar is clicked, this can cause the list to lose focus. | ||
// Preventing the default behavior here has no undesired effects, it just stops | ||
// the input from losing focus. | ||
$event.preventDefault(); | ||
return false; | ||
}; | ||
scope.handleListClick = function handleListClick( $event ) { | ||
// When the list scrollbar is clicked, this can cause the list to lose focus. | ||
// Preventing the default behavior here has no undesired effects, it just stops | ||
// the input from losing focus. | ||
$event.preventDefault(); | ||
return false; | ||
}; | ||
/** | ||
* Selects a given timestamp as the new value of the timepicker. | ||
* @param {Number} timestamp UNIX timestamp | ||
* @param {Number} elementIndex The index of the time element in the dropdown list. | ||
*/ | ||
scope.select = function select( timestamp, elementIndex ) { | ||
// Construct a moment instance from the UNIX offset. | ||
var time; | ||
if( moment.tz && scope.fmReference.tz() ) { | ||
time = moment( timestamp ).tz( scope.fmReference.tz() ); | ||
} else { | ||
time = moment( timestamp ); | ||
} | ||
// Format the time to store it in the input box. | ||
scope.time = time.format( scope.fmFormat ); | ||
/** | ||
* Selects a given timestamp as the new value of the timepicker. | ||
* @param {Number} timestamp UNIX timestamp | ||
* @param {Number} elementIndex The index of the time element in the dropdown list. | ||
*/ | ||
scope.select = function( timestamp, elementIndex ) { | ||
// Construct a moment instance from the UNIX offset. | ||
var time; | ||
if( moment.tz && scope.reference.tz() ) { | ||
time = moment( timestamp ).tz( scope.reference.tz() ); | ||
} else { | ||
time = moment( timestamp ); | ||
} | ||
// Format the time to store it in the input box. | ||
scope.time = time.format( scope.format ); | ||
// Store the selected index | ||
scope.activeIndex = elementIndex; | ||
// Store the selected index | ||
scope.activeIndex = elementIndex; | ||
scope.update(); | ||
scope.closePopup(); | ||
}; | ||
scope.update(); | ||
scope.closePopup(); | ||
}; | ||
scope.increment = function increment() { | ||
if( scope.fmIsOpen ) { | ||
scope.modelPreview.add( scope.fmInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
} else { | ||
scope.ngModel.add( scope.fmInterval ); | ||
scope.ngModel = scope.ensureTimeIsWithinBounds( scope.ngModel ); | ||
scope.time = scope.ngModel.format( scope.fmFormat ); | ||
} | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, scope.activeIndex + 1 ); | ||
}; | ||
scope.increment = function() { | ||
if( scope.isOpen ) { | ||
scope.modelPreview.add( scope.interval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
} else { | ||
scope.ngModel.add( scope.interval ); | ||
scope.ngModel = scope.ensureTimeIsWithinBounds( scope.ngModel ); | ||
scope.time = scope.ngModel.format( scope.format ); | ||
} | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, scope.activeIndex + 1 ); | ||
}; | ||
scope.decrement = function decrement() { | ||
if( scope.fmIsOpen ) { | ||
scope.modelPreview.subtract( scope.fmInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
} else { | ||
scope.ngModel.subtract( scope.fmInterval ); | ||
scope.ngModel = scope.ensureTimeIsWithinBounds( scope.ngModel ); | ||
scope.time = scope.ngModel.format( scope.fmFormat ); | ||
} | ||
scope.activeIndex = Math.max( 0, scope.activeIndex - 1 ); | ||
}; | ||
scope.decrement = function() { | ||
if( scope.isOpen ) { | ||
scope.modelPreview.subtract( scope.interval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
} else { | ||
scope.ngModel.subtract( scope.interval ); | ||
scope.ngModel = scope.ensureTimeIsWithinBounds( scope.ngModel ); | ||
scope.time = scope.ngModel.format( scope.format ); | ||
} | ||
scope.activeIndex = Math.max( 0, scope.activeIndex - 1 ); | ||
}; | ||
/** | ||
* Check if the value in the input control is a valid timestamp. | ||
*/ | ||
scope.update = function update() { | ||
var timeValid = checkTimeValueValid( scope.time ) && checkTimeValueWithinBounds( scope.time ); | ||
if( timeValid ) { | ||
var newTime; | ||
if( moment.tz ) { | ||
newTime = moment.tz( scope.time, | ||
scope.fmFormat, | ||
scope.fmReference.tz() ); | ||
} else { | ||
newTime = moment( scope.time, scope.fmFormat ); | ||
} | ||
newTime = scope.constrainToReference( newTime ); | ||
controller.$setViewValue( newTime ); | ||
} | ||
}; | ||
/** | ||
* Check if the value in the input control is a valid timestamp. | ||
*/ | ||
scope.update = function() { | ||
var timeValid = checkTimeValueValid( scope.time ) && checkTimeValueWithinBounds( scope.time ); | ||
if( timeValid ) { | ||
var newTime; | ||
if( moment.tz ) { | ||
newTime = moment.tz( scope.time, | ||
scope.format, | ||
scope.reference.tz() ); | ||
} else { | ||
newTime = moment( scope.time, scope.format ); | ||
} | ||
newTime = scope.constrainToReference( newTime ); | ||
controller.$setViewValue( newTime ); | ||
scope.handleKeyboardInput = function handleKeyboardInput( event ) { | ||
switch( event.keyCode ) { | ||
case 13: | ||
// Enter | ||
if( scope.modelPreview ) { | ||
scope.ngModel = scope.modelPreview; | ||
scope.fmIsOpen = false; | ||
} | ||
}; | ||
break; | ||
case 27: | ||
// Escape | ||
scope.closePopup(); | ||
break; | ||
case 33: | ||
// Page up | ||
openPopup(); | ||
scope.modelPreview.subtract( scope.fmLargeInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
scope.activeIndex = Math.max( 0, | ||
scope.activeIndex - scope.largeIntervalIndexJump ); | ||
break; | ||
case 34: | ||
// Page down | ||
openPopup(); | ||
scope.modelPreview.add( scope.fmLargeInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, | ||
scope.activeIndex + scope.largeIntervalIndexJump ); | ||
break; | ||
case 38: | ||
// Up arrow | ||
openPopup(); | ||
scope.decrement(); | ||
break; | ||
case 40: | ||
// Down arrow | ||
openPopup(); | ||
scope.increment(); | ||
break; | ||
default: | ||
} | ||
$timeout( ensureUpdatedView ); | ||
}; | ||
scope.handleKeyboardInput = function( event ) { | ||
switch( event.keyCode ) { | ||
case 13: | ||
// Enter | ||
if( scope.modelPreview ) { | ||
scope.ngModel = scope.modelPreview; | ||
scope.isOpen = false; | ||
} | ||
break; | ||
case 27: | ||
// Escape | ||
scope.closePopup(); | ||
break; | ||
case 33: | ||
// Page up | ||
openPopup(); | ||
scope.modelPreview.subtract( scope.largeInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
scope.activeIndex = Math.max( 0, | ||
scope.activeIndex - scope.largeIntervalIndexJump ); | ||
break; | ||
case 34: | ||
// Page down | ||
openPopup(); | ||
scope.modelPreview.add( scope.largeInterval ); | ||
scope.modelPreview = scope.ensureTimeIsWithinBounds( scope.modelPreview ); | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, | ||
scope.activeIndex + scope.largeIntervalIndexJump ); | ||
break; | ||
case 38: | ||
// Up arrow | ||
openPopup(); | ||
scope.decrement(); | ||
break; | ||
case 40: | ||
// Down arrow | ||
openPopup(); | ||
scope.increment(); | ||
break; | ||
default: | ||
} | ||
$timeout( ensureUpdatedView ); | ||
}; | ||
/** | ||
* Prevent default behavior from happening. | ||
* @param event | ||
*/ | ||
scope.preventDefault = function preventDefault( event ) { | ||
event.preventDefault(); | ||
}; | ||
/** | ||
* Prevent default behavior from happening. | ||
* @param event | ||
*/ | ||
scope.preventDefault = function( event ) { | ||
event.preventDefault(); | ||
}; | ||
/** | ||
* Remember the highest index of the existing list items. | ||
* We use this to constrain the possible values for the index that marks a list item as active. | ||
* @param {Number} index | ||
*/ | ||
scope.largestPossibleIndexIs = function largestPossibleIndexIs( index ) { | ||
scope.largestPossibleIndex = index; | ||
}; | ||
/** | ||
* Remember the highest index of the existing list items. | ||
* We use this to constrain the possible values for the index that marks a list item as active. | ||
* @param {Number} index | ||
*/ | ||
scope.largestPossibleIndexIs = function( index ) { | ||
scope.largestPossibleIndex = index; | ||
}; | ||
scope.focusInputElement = function focusInputElement() { | ||
$( inputElement ).focus(); | ||
}; | ||
scope.focusInputElement = function() { | ||
$( inputElement ).focus(); | ||
}; | ||
var inputElement = element.find( "input" ); | ||
var popupListElement = element.find( "ul" ); | ||
var inputElement = element.find( "input" ); | ||
var popupListElement = element.find( "ul" ); | ||
/** | ||
* Open the popup when the input box gets focus. | ||
*/ | ||
inputElement.bind( "focus", function onFocus() { | ||
// Without delay the popup can glitch close itself instantly after being opened. | ||
$timeout( openPopup, 150 ); | ||
scope.isFocused = true; | ||
} ); | ||
/** | ||
* Open the popup when the input box gets focus. | ||
*/ | ||
inputElement.bind( "focus", function() { | ||
// Without delay the popup can glitch close itself instantly after being opened. | ||
$timeout( openPopup, 150 ); | ||
scope.isFocused = true; | ||
} ); | ||
/** | ||
* Invoked when the input box loses focus. | ||
*/ | ||
inputElement.bind( "blur", function onBlur() { | ||
// Delay any action by 150ms | ||
$timeout( function checkFocusState() { | ||
// Check if we didn't get refocused in the meantime. | ||
// This can happen if the input box is selected and the user toggles the dropdown. | ||
// This would cause a hide and close in rapid succession, so don't do it. | ||
if( !$( inputElement ).is( ":focus" ) ) { | ||
scope.closePopup(); | ||
validateView(); | ||
} | ||
}, 150 ); | ||
scope.isFocused = false; | ||
} ); | ||
/** | ||
* Invoked when the input box loses focus. | ||
*/ | ||
inputElement.bind( "blur", function() { | ||
// Delay any action by 150ms | ||
$timeout( function() { | ||
// Check if we didn't get refocused in the meantime. | ||
// This can happen if the input box is selected and the user toggles the dropdown. | ||
// This would cause a hide and close in rapid succession, so don't do it. | ||
if( !$( inputElement ).is( ":focus" ) ) { | ||
scope.closePopup(); | ||
validateView(); | ||
} | ||
}, 150 ); | ||
scope.isFocused = false; | ||
} ); | ||
popupListElement.bind( "mousedown", function onMousedown( event ) { | ||
event.preventDefault(); | ||
} ); | ||
popupListElement.bind( "mousedown", function( event ) { | ||
if( typeof Hamster === "function" ) { | ||
Hamster( inputElement[ 0 ] ).wheel( function onMousewheel( event, delta, deltaX, deltaY ) { | ||
if( scope.isFocused ) { | ||
event.preventDefault(); | ||
} ); | ||
if( typeof Hamster === "function" ) { | ||
Hamster( inputElement[ 0 ] ).wheel( function( event, delta, deltaX, deltaY ) { | ||
if( scope.isFocused ) { | ||
event.preventDefault(); | ||
scope.activeIndex -= delta; | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, | ||
Math.max( 0, scope.activeIndex ) ); | ||
scope.activeIndex -= delta; | ||
scope.activeIndex = Math.min( scope.largestPossibleIndex, | ||
Math.max( 0, scope.activeIndex ) ); | ||
scope.select( scope.dropDownOptions[ scope.activeIndex ], scope.activeIndex ); | ||
$timeout( ensureUpdatedView ); | ||
} | ||
} ); | ||
scope.select( scope.dropDownOptions[ scope.activeIndex ], scope.activeIndex ); | ||
$timeout( ensureUpdatedView ); | ||
} | ||
} ); | ||
} | ||
} | ||
}; | ||
} | ||
] ); | ||
}; | ||
} | ||
})(); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
199868
35
3357
10