Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fm-timepicker

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fm-timepicker - npm Package Compare versions

Comparing version 3.2.0 to 4.0.0

.idea/inspectionProfiles/profiles_settings.xml

1243

dist/fm-timepicker.js

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc