/*! * Time picker for pickadate.js v3.1.1 * http://amsul.github.io/pickadate.js/time.htm */ /*jshint debug: true, devel: true, browser: true, asi: true, unused: true, boss: true */ // Create a new scope. (function() { /** * Globals and constants */ var HOURS_IN_DAY = 24, MINUTES_IN_HOUR = 60, HOURS_TO_NOON = 12, MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR /** * The time picker constructor */ function TimePicker( picker, settings ) { var clock = this, elementDataValue = picker.$node.data( 'value' ) clock.settings = settings // The queue of methods that will be used to build item objects. clock.queue = { interval: 'i', min: 'measure create', max: 'measure create', now: 'now create', select: 'parse create validate', highlight: 'create validate', view: 'create validate', disable: 'flipItem', enable: 'flipItem' } // The component's item object. clock.item = {} clock.item.interval = settings.interval || 30 clock.item.disable = ( settings.disable || [] ).slice( 0 ) clock.item.enable = -(function( collectionDisabled ) { return collectionDisabled[ 0 ] === true ? collectionDisabled.shift() : -1 })( clock.item.disable ) clock. set( 'min', settings.min ). set( 'max', settings.max ). set( 'now' ). // Setting the `select` also sets the `highlight` and `view`. set( 'select', // If there's a `value` or `data-value`, use that with formatting. // Otherwise default to the minimum selectable time. elementDataValue || picker.$node[ 0 ].value || clock.item.min, // Use the relevant format. { format: elementDataValue ? settings.formatSubmit : settings.format } ) // The keycode to movement mapping. clock.key = { 40: 1, // Down 38: -1, // Up 39: 1, // Right 37: -1, // Left go: function( timeChange ) { clock.set( 'highlight', clock.item.highlight.pick + timeChange * clock.item.interval, { interval: timeChange * clock.item.interval } ) this.render() } } // Bind some picker events. picker. on( 'render', function() { var $pickerHolder = picker.$root.children(), $viewset = $pickerHolder.find( '.' + settings.klass.viewset ) if ( $viewset.length ) { $pickerHolder[ 0 ].scrollTop = ~~( $viewset.position().top - ( $viewset[ 0 ].clientHeight * 2 ) ) } else { console.warn( 'Nothing to viewset with', clock.item.view ) } }). on( 'open', function() { picker.$root.find( 'button' ).attr( 'disable', false ) }). on( 'close', function() { picker.$root.find( 'button' ).attr( 'disable', true ) }) } //TimePicker /** * Set a timepicker item object. */ TimePicker.prototype.set = function( type, value, options ) { var clock = this // Go through the queue of methods, and invoke the function. Update this // as the time unit, and set the final resultant as this item type. // * In the case of `enable`, keep the queue but set `disable` instead. // And in the case of `flip`, keep the queue but set `enable` instead. clock.item[ ( type == 'enable' ? 'disable' : type == 'flip' ? 'enable' : type ) ] = clock.queue[ type ].split( ' ' ).map( function( method ) { return value = clock[ method ]( type, value, options ) }).pop() // Check if we need to cascade through more updates. if ( type == 'select' ) { clock.set( 'highlight', clock.item.select, options ) } else if ( type == 'highlight' ) { clock.set( 'view', clock.item.highlight, options ) } else if ( type == 'interval' ) { clock. set( 'min', clock.item.min, options ). set( 'max', clock.item.max, options ) } else if ( ( type == 'flip' || type == 'min' || type == 'max' || type == 'disable' || type == 'enable' ) && clock.item.select && clock.item.highlight ) { if ( type == 'min' ) { clock.set( 'max', clock.item.max, options ) } clock. set( 'select', clock.item.select, options ). set( 'highlight', clock.item.highlight, options ) } return clock } //TimePicker.prototype.set /** * Get a timepicker item object. */ TimePicker.prototype.get = function( type ) { return this.item[ type ] } //TimePicker.prototype.get /** * Create a picker time object. */ TimePicker.prototype.create = function( type, value, options ) { var clock = this // If there's no value, use the type as the value. value = value === undefined ? type : value // If it's an object, use the "pick" value. if ( Picker._.isObject( value ) && Picker._.isInteger( value.pick ) ) { value = value.pick } // If it's an array, convert it into minutes. else if ( Array.isArray( value ) ) { value = +value[ 0 ] * MINUTES_IN_HOUR + (+value[ 1 ]) } // If no valid value is passed, set it to "now". else if ( !Picker._.isInteger( value ) ) { value = clock.now( type, value, options ) } // If we're setting the max, make sure it's greater than the min. if ( type == 'max' && value < clock.item.min.pick ) { value += MINUTES_IN_DAY } // Normalize it into a "reachable" interval. value = clock.normalize( value, options ) // Return the compiled object. return { // Divide to get hours from minutes. hour: ~~( HOURS_IN_DAY + value / MINUTES_IN_HOUR ) % HOURS_IN_DAY, // The remainder is the minutes. mins: ( MINUTES_IN_HOUR + value % MINUTES_IN_HOUR ) % MINUTES_IN_HOUR, // The time in total minutes. time: ( MINUTES_IN_DAY + value ) % MINUTES_IN_DAY, // Reference to the "relative" value to pick. pick: value } } //TimePicker.prototype.create /** * Get the time relative to now. */ TimePicker.prototype.now = function( type, value/*, options*/ ) { var date = new Date(), dateMinutes = date.getHours() * MINUTES_IN_HOUR + date.getMinutes() // If the value is a number, adjust by that many intervals because // the time has passed. In the case of “midnight” and a negative `min`, // increase the value by 2. Otherwise increase it by 1. if ( Picker._.isInteger( value ) ) { value += type == 'min' && value < 0 && dateMinutes === 0 ? 2 : 1 } // If the value isn’t a number, default to 1 passed interval. else { value = 1 } // Calculate the final relative time. return value * this.item.interval + dateMinutes } //TimePicker.prototype.now /** * Normalize minutes or an object to be "reachable" based on the interval. */ TimePicker.prototype.normalize = function( value/*, options*/ ) { // If it's a negative value, add one interval to keep it as "passed". return value - ( ( value < 0 ? this.item.interval : 0 ) + value % this.item.interval ) } //TimePicker.prototype.normalize /** * Measure the range of minutes. */ TimePicker.prototype.measure = function( type, value, options ) { var clock = this // If it's anything false-y, set it to the default. if ( !value ) { value = type == 'min' ? [ 0, 0 ] : [ HOURS_IN_DAY - 1, MINUTES_IN_HOUR - 1 ] } // If it's a literal true, or an integer, make it relative to now. else if ( value === true || Picker._.isInteger( value ) ) { value = clock.now( type, value, options ) } // If it's an object already, just normalize it. else if ( Picker._.isObject( value ) && Picker._.isInteger( value.pick ) ) { value = clock.normalize( value.pick, options ) } return value } ///TimePicker.prototype.measure /** * Validate an object as enabled. */ TimePicker.prototype.validate = function( type, timeObject, options ) { var clock = this, interval = options && options.interval ? options.interval : clock.item.interval // Check if the object is disabled. if ( clock.disabled( timeObject ) ) { // Shift with the interval until we reach an enabled time. timeObject = clock.shift( timeObject, interval ) } // Scope the object into range. timeObject = clock.scope( timeObject ) // Do a second check to see if we landed on a disabled min/max. // In that case, shift using the opposite interval as before. if ( clock.disabled( timeObject ) ) { timeObject = clock.shift( timeObject, interval * -1 ) } // Return the final object. return timeObject } //TimePicker.prototype.validate /** * Check if an object is disabled. */ TimePicker.prototype.disabled = function( timeObject ) { var clock = this, // Filter through the disabled times to check if this is one. isDisabledTime = clock.item.disable.filter( function( timeToDisable ) { // If the time is a number, match the hours. if ( Picker._.isInteger( timeToDisable ) ) { return timeObject.hour == timeToDisable } // If it's an array, create the object and match the times. if ( Array.isArray( timeToDisable ) ) { return timeObject.pick == clock.create( timeToDisable ).pick } }).length // If the clock is "enabled" flag is flipped, flip the condition. return clock.item.enable === -1 ? !isDisabledTime : isDisabledTime } //TimePicker.prototype.disabled /** * Shift an object by an interval until we reach an enabled object. */ TimePicker.prototype.shift = function( timeObject, interval ) { var clock = this // Keep looping as long as the time is disabled. while ( clock.disabled( timeObject ) ) { // Increase/decrease the time by the interval and keep looping. timeObject = clock.create( timeObject.pick += interval || clock.item.interval ) // If we've looped beyond the limits, break out of the loop. if ( timeObject.pick <= clock.item.min.pick || timeObject.pick >= clock.item.max.pick ) { break } } // Return the final object. return timeObject } //TimePicker.prototype.shift /** * Scope an object to be within range of min and max. */ TimePicker.prototype.scope = function( timeObject ) { var minLimit = this.item.min.pick, maxLimit = this.item.max.pick return this.create( timeObject.pick > maxLimit ? maxLimit : timeObject.pick < minLimit ? minLimit : timeObject ) } //TimePicker.prototype.scope /** * Parse a string into a usable type. */ TimePicker.prototype.parse = function( type, value, options ) { var clock = this, parsingObject = {} if ( !value || Picker._.isInteger( value ) || Array.isArray( value ) || Picker._.isDate( value ) || Picker._.isObject( value ) && Picker._.isInteger( value.pick ) ) { return value } // We need a `.format` to parse the value. if ( !( options && options.format ) ) { throw "Need a formatting option to parse this.." } // Convert the format into an array and then map through it. clock.formats.toArray( options.format ).map( function( label ) { var // Grab the formatting label. formattingLabel = clock.formats[ label ], // The format length is from the formatting label function or the // label length without the escaping exclamation (!) mark. formatLength = formattingLabel ? Picker._.trigger( formattingLabel, clock, [ value, parsingObject ] ) : label.replace( /^!/, '' ).length // If there's a format label, split the value up to the format length. // Then add it to the parsing object with appropriate label. if ( formattingLabel ) { parsingObject[ label ] = value.substr( 0, formatLength ) } // Update the time value as the substring from format length to end. value = value.substr( formatLength ) }) return +parsingObject.i + MINUTES_IN_HOUR * ( +( parsingObject.H || parsingObject.HH ) || ( +( parsingObject.h || parsingObject.hh ) % 12 + ( /^p/i.test( parsingObject.A || parsingObject.a ) ? 12 : 0 ) ) ) } //TimePicker.prototype.parse /** * Various formats to display the object in. */ TimePicker.prototype.formats = { h: function( string, timeObject ) { // If there's string, then get the digits length. // Otherwise return the selected hour in "standard" format. return string ? Picker._.digits( string ) : timeObject.hour % HOURS_TO_NOON || HOURS_TO_NOON }, hh: function( string, timeObject ) { // If there's a string, then the length is always 2. // Otherwise return the selected hour in "standard" format with a leading zero. return string ? 2 : Picker._.lead( timeObject.hour % HOURS_TO_NOON || HOURS_TO_NOON ) }, H: function( string, timeObject ) { // If there's string, then get the digits length. // Otherwise return the selected hour in "military" format as a string. return string ? Picker._.digits( string ) : '' + timeObject.hour }, HH: function( string, timeObject ) { // If there's string, then get the digits length. // Otherwise return the selected hour in "military" format with a leading zero. return string ? Picker._.digits( string ) : Picker._.lead( timeObject.hour ) }, i: function( string, timeObject ) { // If there's a string, then the length is always 2. // Otherwise return the selected minutes. return string ? 2 : Picker._.lead( timeObject.mins ) }, a: function( string, timeObject ) { // If there's a string, then the length is always 4. // Otherwise check if it's more than "noon" and return either am/pm. return string ? 4 : MINUTES_IN_DAY / 2 > timeObject.time % MINUTES_IN_DAY ? 'a.m.' : 'p.m.' }, A: function( string, timeObject ) { // If there's a string, then the length is always 2. // Otherwise check if it's more than "noon" and return either am/pm. return string ? 2 : MINUTES_IN_DAY / 2 > timeObject.time % MINUTES_IN_DAY ? 'AM' : 'PM' }, // Create an array by splitting the formatting string passed. toArray: function( formatString ) { return formatString.split( /(h{1,2}|H{1,2}|i|a|A|!.)/g ) }, // Format an object into a string using the formatting options. toString: function ( formatString, itemObject ) { var clock = this return clock.formats.toArray( formatString ).map( function( label ) { return Picker._.trigger( clock.formats[ label ], clock, [ 0, itemObject ] ) || label.replace( /^!/, '' ) }).join( '' ) } } //TimePicker.prototype.formats /** * Flip an item as enabled or disabled. */ TimePicker.prototype.flipItem = function( type, value/*, options*/ ) { var clock = this, collection = clock.item.disable, isFlipped = clock.item.enable === -1 // Flip the enabled and disabled times. if ( value == 'flip' ) { clock.item.enable = isFlipped ? 1 : -1 } // Check if we have to add/remove from collection. else if ( !isFlipped && type == 'enable' || isFlipped && type == 'disable' ) { collection = clock.removeDisabled( collection, value ) } else if ( !isFlipped && type == 'disable' || isFlipped && type == 'enable' ) { collection = clock.addDisabled( collection, value ) } return collection } //TimePicker.prototype.flipItem /** * Add an item to the disabled collection. */ TimePicker.prototype.addDisabled = function( collection, item ) { var clock = this item.map( function( timeUnit ) { if ( !clock.filterDisabled( collection, timeUnit ).length ) { collection.push( timeUnit ) } }) return collection } //TimePicker.prototype.addDisabled /** * Remove an item from the disabled collection. */ TimePicker.prototype.removeDisabled = function( collection, item ) { var clock = this item.map( function( timeUnit ) { collection = clock.filterDisabled( collection, timeUnit, 1 ) }) return collection } //TimePicker.prototype.removeDisabled /** * Filter through the disabled collection to find a time unit. */ TimePicker.prototype.filterDisabled = function( collection, timeUnit, isRemoving ) { var timeIsArray = Array.isArray( timeUnit ) return collection.filter( function( disabledTimeUnit ) { var isMatch = !timeIsArray && timeUnit === disabledTimeUnit || timeIsArray && Array.isArray( disabledTimeUnit ) && timeUnit.toString() === disabledTimeUnit.toString() return isRemoving ? !isMatch : isMatch }) } //TimePicker.prototype.filterDisabled /** * The division to use for the range intervals. */ TimePicker.prototype.i = function( type, value/*, options*/ ) { return Picker._.isInteger( value ) && value > 0 ? value : this.item.interval } /** * Create a string for the nodes in the picker. */ TimePicker.prototype.nodes = function( isOpen ) { var clock = this, settings = clock.settings, selectedObject = clock.item.select, highlightedObject = clock.item.highlight, viewsetObject = clock.item.view, disabledCollection = clock.item.disable return Picker._.node( 'ul', Picker._.group({ min: clock.item.min.pick, max: clock.item.max.pick, i: clock.item.interval, node: 'li', item: function( loopedTime ) { loopedTime = clock.create( loopedTime ) return [ Picker._.trigger( clock.formats.toString, clock, [ Picker._.trigger( settings.formatLabel, clock, [ loopedTime ] ) || settings.format, loopedTime ] ), (function( klasses, timeMinutes ) { if ( selectedObject && selectedObject.pick == timeMinutes ) { klasses.push( settings.klass.selected ) } if ( highlightedObject && highlightedObject.pick == timeMinutes ) { klasses.push( settings.klass.highlighted ) } if ( viewsetObject && viewsetObject.pick == timeMinutes ) { klasses.push( settings.klass.viewset ) } if ( disabledCollection && clock.disabled( loopedTime ) ) { klasses.push( settings.klass.disabled ) } return klasses.join( ' ' ) })( [ settings.klass.listItem ], loopedTime.pick ), 'data-pick=' + loopedTime.pick ] } }) + Picker._.node( 'li', Picker._.node( 'button', settings.clear, settings.klass.buttonClear, 'data-clear=1' + ( isOpen ? '' : ' disable' ) ) ), settings.klass.list ) } //TimePicker.prototype.nodes /* ========================================================================== Extend the picker to add the component with the defaults. ========================================================================== */ TimePicker.defaults = (function( prefix ) { return { // Clear clear: 'Clear', // The format to show on the `input` element format: 'h:i A', // The interval between each time interval: 30, // Classes klass: { picker: prefix + ' ' + prefix + '--time', holder: prefix + '__holder', list: prefix + '__list', listItem: prefix + '__list-item', disabled: prefix + '__list-item--disabled', selected: prefix + '__list-item--selected', highlighted: prefix + '__list-item--highlighted', viewset: prefix + '__list-item--viewset', now: prefix + '__list-item--now', buttonClear: prefix + '__button--clear' } } })( Picker.klasses().picker ) /** * Extend the picker to add the date picker. */ Picker.extend( 'pickatime', TimePicker ) // Close the scope. })();