/*! * Time picker for pickadate.js v3.6.4 * http://amsul.github.io/pickadate.js/time.htm */ (function (factory) { // AMD. if (typeof define == 'function' && define.amd) define(['./picker', 'jquery'], factory) // Node.js/browserify. else if (typeof exports == 'object') module.exports = factory(require('./picker.js'), require('jquery')) // Browser globals. else factory(Picker, jQuery) }(function (Picker, $) { /** * 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, _ = Picker._ /** * The time picker constructor */ function TimePicker(picker, settings) { var clock = this, elementValue = picker.$node[0].value, elementDataValue = picker.$node.data('value'), valueString = elementDataValue || elementValue, formatString = elementDataValue ? settings.formatSubmit : settings.format clock.settings = settings clock.$node = picker.$node // 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: 'parse create validate', view: 'parse create validate', disable: 'deactivate', enable: 'activate' } // The component's item object. clock.item = {} clock.item.clear = null 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') // When there’s a value, set the `select`, which in turn // also sets the `highlight` and `view`. if (valueString) { clock.set('select', valueString, { format: formatString }) } // If there’s no value, default to highlighting “today”. else { clock. set('select', null). set('highlight', clock.item.now) } // 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), vendors = function (prop) { return ['webkit', 'moz', 'ms', 'o', ''].map(function (vendor) { return (vendor ? '-' + vendor + '-' : '') + prop }) }, animations = function ($el, state) { vendors('transform').map(function (prop) { $el.css(prop, state) }) vendors('transition').map(function (prop) { $el.css(prop, state) }) } if ($viewset.length) { animations($pickerHolder, 'none') $pickerHolder[0].scrollTop = ~~$viewset.position().top - ($viewset[0].clientHeight * 2) animations($pickerHolder, '') } }, 1). on('open', function () { picker.$root.find('button').attr('disabled', false) }, 1). on('close', function () { picker.$root.find('button').attr('disabled', true) }, 1) } //TimePicker /** * Set a timepicker item object. */ TimePicker.prototype.set = function (type, value, options) { var clock = this, clockItem = clock.item // If the value is `null` just set it immediately. if (value === null) { if (type == 'clear') type = 'select' clockItem[type] = value return clock } // Otherwise go through the queue of methods, and invoke the functions. // Update this as the time unit, and set the final value as this item. // * 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. clockItem[(type == 'enable' ? 'disable' : type == 'flip' ? 'enable' : type)] = clock.queue[type].split(' ').map(function (method) { value = clock[method](type, value, options) return value }).pop() // Check if we need to cascade through more updates. if (type == 'select') { clock.set('highlight', clockItem.select, options) } else if (type == 'highlight') { clock.set('view', clockItem.highlight, options) } else if (type == 'interval') { clock. set('min', clockItem.min, options). set('max', clockItem.max, options) } else if (type.match(/^(flip|min|max|disable|enable)$/)) { if (clockItem.select && clock.disabled(clockItem.select)) { clock.set('select', value, options) } if (clockItem.highlight && clock.disabled(clockItem.highlight)) { clock.set('highlight', value, options) } if (type == 'min') { clock.set('max', clockItem.max, 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 a date object, convert it into an array. if (_.isDate(value)) { value = [value.getHours(), value.getMinutes()] } // If it’s an object, use the “pick” value. if ($.isPlainObject(value) && _.isInteger(value.pick)) { value = value.pick } // If it’s an array, convert it into minutes. else if ($.isArray(value)) { value = +value[0] * MINUTES_IN_HOUR + (+value[1]) } // If no valid value is passed, set it to “now”. else if (!_.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 } // If the value doesn’t fall directly on the interval, // add one interval to indicate it as “passed”. if (type != 'min' && type != 'max' && (value - clock.item.min.pick) % clock.item.interval !== 0) { value += clock.item.interval } // Normalize it into a “reachable” interval. value = clock.normalize(type, 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 % MINUTES_IN_DAY } } //TimePicker.prototype.create /** * Create a range limit object using an array, date object, * literal “true”, or integer relative to another time. */ TimePicker.prototype.createRange = function (from, to) { var clock = this, createTime = function (time) { if (time === true || $.isArray(time) || _.isDate(time)) { return clock.create(time) } return time } // Create objects if possible. if (!_.isInteger(from)) { from = createTime(from) } if (!_.isInteger(to)) { to = createTime(to) } // Create relative times. if (_.isInteger(from) && $.isPlainObject(to)) { from = [to.hour, to.mins + (from * clock.settings.interval)]; } else if (_.isInteger(to) && $.isPlainObject(from)) { to = [from.hour, from.mins + (to * clock.settings.interval)]; } return { from: createTime(from), to: createTime(to) } } //TimePicker.prototype.createRange /** * Check if a time unit falls within a time range object. */ TimePicker.prototype.withinRange = function (range, timeUnit) { range = this.createRange(range.from, range.to) return timeUnit.pick >= range.from.pick && timeUnit.pick <= range.to.pick } /** * Check if two time range objects overlap. */ TimePicker.prototype.overlapRanges = function (one, two) { var clock = this // Convert the ranges into comparable times. one = clock.createRange(one.from, one.to) two = clock.createRange(two.from, two.to) return clock.withinRange(one, two.from) || clock.withinRange(one, two.to) || clock.withinRange(two, one.from) || clock.withinRange(two, one.to) } /** * Get the time relative to now. */ TimePicker.prototype.now = function (type, value/*, options*/) { var interval = this.item.interval, date = new Date(), nowMinutes = date.getHours() * MINUTES_IN_HOUR + date.getMinutes(), isValueInteger = _.isInteger(value), isBelowInterval // Make sure “now” falls within the interval range. nowMinutes -= nowMinutes % interval // Check if the difference is less than the interval itself. isBelowInterval = value < 0 && interval * value + nowMinutes <= -interval // Add an interval because the time has “passed”. nowMinutes += type == 'min' && isBelowInterval ? 0 : interval // If the value is a number, adjust by that many intervals. if (isValueInteger) { nowMinutes += interval * ( isBelowInterval && type != 'max' ? value + 1 : value ) } // Return the final calculation. return nowMinutes } //TimePicker.prototype.now /** * Normalize minutes to be “reachable” based on the min and interval. */ TimePicker.prototype.normalize = function (type, value/*, options*/) { var interval = this.item.interval, minTime = this.item.min && this.item.min.pick || 0 // If setting min time, don’t shift anything. // Otherwise get the value and min difference and then // normalize the difference with the interval. value -= type == 'min' ? 0 : (value - minTime) % interval // Return the adjusted value. return value } //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 string, parse it. if (typeof value == 'string') { value = clock.parse(type, value) } // If it’s a literal true, or an integer, make it relative to now. else if (value === true || _.isInteger(value)) { value = clock.now(type, value, options) } // If it’s an object already, just normalize it. else if ($.isPlainObject(value) && _.isInteger(value.pick)) { value = clock.normalize(type, 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 (timeToVerify) { var clock = this, // Filter through the disabled times to check if this is one. isDisabledMatch = clock.item.disable.filter(function (timeToDisable) { // If the time is a number, match the hours. if (_.isInteger(timeToDisable)) { return timeToVerify.hour == timeToDisable } // If it’s an array, create the object and match the times. if ($.isArray(timeToDisable) || _.isDate(timeToDisable)) { return timeToVerify.pick == clock.create(timeToDisable).pick } // If it’s an object, match a time within the “from” and “to” range. if ($.isPlainObject(timeToDisable)) { return clock.withinRange(timeToDisable, timeToVerify) } }) // If this time matches a disabled time, confirm it’s not inverted. isDisabledMatch = isDisabledMatch.length && !isDisabledMatch.filter(function (timeToDisable) { return $.isArray(timeToDisable) && timeToDisable[2] == 'inverted' || $.isPlainObject(timeToDisable) && timeToDisable.inverted }).length // If the clock is "enabled" flag is flipped, flip the condition. return clock.item.enable === -1 ? !isDisabledMatch : isDisabledMatch || timeToVerify.pick < clock.item.min.pick || timeToVerify.pick > clock.item.max.pick } //TimePicker.prototype.disabled /** * Shift an object by an interval until we reach an enabled object. */ TimePicker.prototype.shift = function (timeObject, interval) { var clock = this, minLimit = clock.item.min.pick, maxLimit = clock.item.max.pick/*, safety = 1000*/ interval = interval || clock.item.interval // Keep looping as long as the time is disabled. while ( /*safety &&*/ clock.disabled(timeObject)) { /*safety -= 1 if ( !safety ) { throw 'Fell into an infinite loop while shifting to ' + timeObject.hour + ':' + timeObject.mins + '.' }*/ // Increase/decrease the time by the interval and keep looping. timeObject = clock.create(timeObject.pick += interval) // If we've looped beyond the limits, break out of the loop. if (timeObject.pick <= minLimit || timeObject.pick >= maxLimit) { 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 hour, minutes, isPM, item, parseValue, clock = this, parsingObject = {} // If it’s already parsed, we’re good. if (!value || typeof value != 'string') { return value } // We need a `.format` to parse the value with. if (!(options && options.format)) { options = options || {} options.format = clock.settings.format } // Convert the format into an array and then map through it. clock.formats.toArray(options.format).map(function (label) { var substring, // 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 ? _.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) { substring = value.substr(0, formatLength) parsingObject[label] = substring.match(/^\d+$/) ? +substring : substring } // Update the time value as the substring from format length to end. value = value.substr(formatLength) }) // Grab the hour and minutes from the parsing object. for (item in parsingObject) { parseValue = parsingObject[item] if (_.isInteger(parseValue)) { if (item.match(/^(h|hh)$/i)) { hour = parseValue if (item == 'h' || item == 'hh') { hour %= 12 } } else if (item == 'i') { minutes = parseValue } } else if (item.match(/^a$/i) && parseValue.match(/^p/i) && ('h' in parsingObject || 'hh' in parsingObject)) { isPM = true } } // Calculate it in minutes and return. return (isPM ? hour + 12 : hour) * MINUTES_IN_HOUR + minutes } //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 ? _.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 : _.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 ? _.digits(string) : '' + (timeObject.hour % 24) }, 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 ? _.digits(string) : _.lead(timeObject.hour % 24) }, i: function (string, timeObject) { // If there's a string, then the length is always 2. // Otherwise return the selected minutes. return string ? 2 : _.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 _.trigger(clock.formats[label], clock, [0, itemObject]) || label.replace(/^!/, '') }).join('') } } //TimePicker.prototype.formats /** * Check if two time units are the exact. */ TimePicker.prototype.isTimeExact = function (one, two) { var clock = this // When we’re working with minutes, do a direct comparison. if ( (_.isInteger(one) && _.isInteger(two)) || (typeof one == 'boolean' && typeof two == 'boolean') ) { return one === two } // When we’re working with time representations, compare the “pick” value. if ( (_.isDate(one) || $.isArray(one)) && (_.isDate(two) || $.isArray(two)) ) { return clock.create(one).pick === clock.create(two).pick } // When we’re working with range objects, compare the “from” and “to”. if ($.isPlainObject(one) && $.isPlainObject(two)) { return clock.isTimeExact(one.from, two.from) && clock.isTimeExact(one.to, two.to) } return false } /** * Check if two time units overlap. */ TimePicker.prototype.isTimeOverlap = function (one, two) { var clock = this // When we’re working with an integer, compare the hours. if (_.isInteger(one) && (_.isDate(two) || $.isArray(two))) { return one === clock.create(two).hour } if (_.isInteger(two) && (_.isDate(one) || $.isArray(one))) { return two === clock.create(one).hour } // When we’re working with range objects, check if the ranges overlap. if ($.isPlainObject(one) && $.isPlainObject(two)) { return clock.overlapRanges(one, two) } return false } /** * Flip the “enabled” state. */ TimePicker.prototype.flipEnable = function (val) { var itemObject = this.item itemObject.enable = val || (itemObject.enable == -1 ? 1 : -1) } /** * Mark a collection of times as “disabled”. */ TimePicker.prototype.deactivate = function (type, timesToDisable) { var clock = this, disabledItems = clock.item.disable.slice(0) // If we’re flipping, that’s all we need to do. if (timesToDisable == 'flip') { clock.flipEnable() } else if (timesToDisable === false) { clock.flipEnable(1) disabledItems = [] } else if (timesToDisable === true) { clock.flipEnable(-1) disabledItems = [] } // Otherwise go through the times to disable. else { timesToDisable.map(function (unitToDisable) { var matchFound // When we have disabled items, check for matches. // If something is matched, immediately break out. for (var index = 0; index < disabledItems.length; index += 1) { if (clock.isTimeExact(unitToDisable, disabledItems[index])) { matchFound = true break } } // If nothing was found, add the validated unit to the collection. if (!matchFound) { if ( _.isInteger(unitToDisable) || _.isDate(unitToDisable) || $.isArray(unitToDisable) || ($.isPlainObject(unitToDisable) && unitToDisable.from && unitToDisable.to) ) { disabledItems.push(unitToDisable) } } }) } // Return the updated collection. return disabledItems } //TimePicker.prototype.deactivate /** * Mark a collection of times as “enabled”. */ TimePicker.prototype.activate = function (type, timesToEnable) { var clock = this, disabledItems = clock.item.disable, disabledItemsCount = disabledItems.length // If we’re flipping, that’s all we need to do. if (timesToEnable == 'flip') { clock.flipEnable() } else if (timesToEnable === true) { clock.flipEnable(1) disabledItems = [] } else if (timesToEnable === false) { clock.flipEnable(-1) disabledItems = [] } // Otherwise go through the disabled times. else { timesToEnable.map(function (unitToEnable) { var matchFound, disabledUnit, index, isRangeMatched // Go through the disabled items and try to find a match. for (index = 0; index < disabledItemsCount; index += 1) { disabledUnit = disabledItems[index] // When an exact match is found, remove it from the collection. if (clock.isTimeExact(disabledUnit, unitToEnable)) { matchFound = disabledItems[index] = null isRangeMatched = true break } // When an overlapped match is found, add the “inverted” state to it. else if (clock.isTimeOverlap(disabledUnit, unitToEnable)) { if ($.isPlainObject(unitToEnable)) { unitToEnable.inverted = true matchFound = unitToEnable } else if ($.isArray(unitToEnable)) { matchFound = unitToEnable if (!matchFound[2]) matchFound.push('inverted') } else if (_.isDate(unitToEnable)) { matchFound = [unitToEnable.getFullYear(), unitToEnable.getMonth(), unitToEnable.getDate(), 'inverted'] } break } } // If a match was found, remove a previous duplicate entry. if (matchFound) for (index = 0; index < disabledItemsCount; index += 1) { if (clock.isTimeExact(disabledItems[index], unitToEnable)) { disabledItems[index] = null break } } // In the event that we’re dealing with an overlap of range times, // make sure there are no “inverted” times because of it. if (isRangeMatched) for (index = 0; index < disabledItemsCount; index += 1) { if (clock.isTimeOverlap(disabledItems[index], unitToEnable)) { disabledItems[index] = null break } } // If something is still matched, add it into the collection. if (matchFound) { disabledItems.push(matchFound) } }) } // Return the updated collection. return disabledItems.filter(function (val) { return val != null }) } //TimePicker.prototype.activate /** * The division to use for the range intervals. */ TimePicker.prototype.i = function (type, value/*, options*/) { return _.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 _.node( 'ul', _.group({ min: clock.item.min.pick, max: clock.item.max.pick, i: clock.item.interval, node: 'li', item: function (loopedTime) { loopedTime = clock.create(loopedTime) var timeMinutes = loopedTime.pick, isSelected = selectedObject && selectedObject.pick == timeMinutes, isHighlighted = highlightedObject && highlightedObject.pick == timeMinutes, isDisabled = disabledCollection && clock.disabled(loopedTime), formattedTime = _.trigger(clock.formats.toString, clock, [settings.format, loopedTime]) return [ _.trigger(clock.formats.toString, clock, [_.trigger(settings.formatLabel, clock, [loopedTime]) || settings.format, loopedTime]), (function (klasses) { if (isSelected) { klasses.push(settings.klass.selected) } if (isHighlighted) { klasses.push(settings.klass.highlighted) } if (viewsetObject && viewsetObject.pick == timeMinutes) { klasses.push(settings.klass.viewset) } if (isDisabled) { klasses.push(settings.klass.disabled) } return klasses.join(' ') })([settings.klass.listItem]), 'data-pick=' + loopedTime.pick + ' ' + _.ariaAttr({ role: 'option', label: formattedTime, selected: isSelected && clock.$node.val() === formattedTime ? true : null, activedescendant: isHighlighted ? true : null, disabled: isDisabled ? true : null }) ] } }) + // * For Firefox forms to submit, make sure to set the button’s `type` attribute as “button”. _.node( 'li', _.node( 'button', settings.clear, settings.klass.buttonClear, 'type=button data-clear=1' + (isOpen ? '' : ' disabled') + ' ' + _.ariaAttr({ controls: clock.$node[0].id }) ), '', _.ariaAttr({ role: 'presentation' }) ), settings.klass.list, _.ariaAttr({ role: 'listbox', controls: clock.$node[0].id }) ) } //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, // Picker close behavior closeOnSelect: true, closeOnClear: true, // Update input value on select/clear updateInput: true, // 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 time picker. */ Picker.extend('pickatime', TimePicker) }));