vendor/assets/javascripts/pickadate/picker.date.js in pickadate-rails-1.3.2 vs vendor/assets/javascripts/pickadate/picker.date.js in pickadate-rails-1.4.0

- old
+ new

@@ -1,24 +1,15 @@ /*! - * Date picker for pickadate.js v3.3.2 + * Date picker for pickadate.js v3.4.0 * http://amsul.github.io/pickadate.js/date.htm */ -/*jshint - debug: true, - devel: true, - browser: true, - asi: true, - unused: true, - boss: true - */ - (function ( factory ) { // Register as an anonymous module. - if ( typeof define === 'function' && define.amd ) + if ( typeof define == 'function' && define.amd ) define( ['picker','jquery'], factory ) // Or using browser globals. else factory( Picker, jQuery ) @@ -27,11 +18,12 @@ /** * Globals and constants */ var DAYS_IN_WEEK = 7, - WEEKS_IN_CALENDAR = 6 + WEEKS_IN_CALENDAR = 6, + _ = Picker._ /** * The date picker constructor @@ -46,21 +38,22 @@ isRTL = function() { return getComputedStyle( picker.$root[0] ).direction === 'rtl' } calendar.settings = settings + calendar.$node = picker.$node // The queue of methods that will be used to build item objects. calendar.queue = { min: 'measure create', max: 'measure create', now: 'now create', select: 'parse create validate', - highlight: 'navigate create validate', - view: 'create validate viewset', - disable: 'flipItem', - enable: 'flipItem' + highlight: 'parse navigate create validate', + view: 'parse create validate viewset', + disable: 'deactivate', + enable: 'activate' } // The component's item object. calendar.item = {} @@ -70,53 +63,64 @@ })( calendar.item.disable ) calendar. set( 'min', settings.min ). set( 'max', settings.max ). - set( 'now' ). + set( 'now' ) - // Setting the `select` also sets the `highlight` and `view`. - set( 'select', + // When there’s a value, set the `select`, which in turn + // also sets the `highlight` and `view`. + if ( valueString ) { + calendar.set( 'select', valueString, { + format: formatString, + fromValue: !!elementValue + }) + } - // Use the value provided or default to selecting “today”. - valueString || calendar.item.now, - { - // Use the appropriate format. - format: formatString, + // If there’s no value, default to highlighting “today”. + else { + calendar. + set( 'select', null ). + set( 'highlight', calendar.item.now ) + } - // Set user-provided month data as true when there is a - // “mm” or “m” used in the relative format string. - data: (function( formatArray ) { - return valueString && ( formatArray.indexOf( 'mm' ) > -1 || formatArray.indexOf( 'm' ) > -1 ) - })( calendar.formats.toArray( formatString ) ) - } - ) - // The keycode to movement mapping. calendar.key = { 40: 7, // Down 38: -7, // Up 39: function() { return isRTL() ? -1 : 1 }, // Right 37: function() { return isRTL() ? 1 : -1 }, // Left go: function( timeChange ) { - calendar.set( 'highlight', [ calendar.item.highlight.year, calendar.item.highlight.month, calendar.item.highlight.date + timeChange ], { interval: timeChange } ) + var highlightedObject = calendar.item.highlight, + targetDate = new Date( highlightedObject.year, highlightedObject.month, highlightedObject.date + timeChange ) + calendar.set( + 'highlight', + [ targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate() ], + { interval: timeChange } + ) this.render() } } // Bind some picker events. picker. on( 'render', function() { picker.$root.find( '.' + settings.klass.selectMonth ).on( 'change', function() { - picker.set( 'highlight', [ picker.get( 'view' ).year, this.value, picker.get( 'highlight' ).date ] ) - picker.$root.find( '.' + settings.klass.selectMonth ).trigger( 'focus' ) + var value = this.value + if ( value ) { + picker.set( 'highlight', [ picker.get( 'view' ).year, value, picker.get( 'highlight' ).date ] ) + picker.$root.find( '.' + settings.klass.selectMonth ).trigger( 'focus' ) + } }) picker.$root.find( '.' + settings.klass.selectYear ).on( 'change', function() { - picker.set( 'highlight', [ this.value, picker.get( 'view' ).month, picker.get( 'highlight' ).date ] ) - picker.$root.find( '.' + settings.klass.selectYear ).trigger( 'focus' ) + var value = this.value + if ( value ) { + picker.set( 'highlight', [ value, picker.get( 'view' ).month, picker.get( 'highlight' ).date ] ) + picker.$root.find( '.' + settings.klass.selectYear ).trigger( 'focus' ) + } }) }). on( 'open', function() { picker.$root.find( 'button, select' ).attr( 'disabled', false ) }). @@ -130,31 +134,42 @@ /** * Set a datepicker item object. */ DatePicker.prototype.set = function( type, value, options ) { - var calendar = this + var calendar = this, + calendarItem = calendar.item - // 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. + // If the value is `null` just set it immediately. + if ( value === null ) { + calendarItem[ type ] = value + return calendar + } + + // 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. - calendar.item[ ( type == 'enable' ? 'disable' : type == 'flip' ? 'enable' : type ) ] = calendar.queue[ type ].split( ' ' ).map( function( method ) { - return value = calendar[ method ]( type, value, options ) + calendarItem[ ( type == 'enable' ? 'disable' : type == 'flip' ? 'enable' : type ) ] = calendar.queue[ type ].split( ' ' ).map( function( method ) { + value = calendar[ method ]( type, value, options ) + return value }).pop() // Check if we need to cascade through more updates. if ( type == 'select' ) { - calendar.set( 'highlight', calendar.item.select, options ) + calendar.set( 'highlight', calendarItem.select, options ) } else if ( type == 'highlight' ) { - calendar.set( 'view', calendar.item.highlight, options ) + calendar.set( 'view', calendarItem.highlight, options ) } - else if ( ( type == 'flip' || type == 'min' || type == 'max' || type == 'disable' || type == 'enable' ) && calendar.item.select && calendar.item.highlight ) { - calendar. - set( 'select', calendar.item.select, options ). - set( 'highlight', calendar.item.highlight, options ) + else if ( type.match( /^(flip|min|max|disable|enable)$/ ) ) { + if ( calendarItem.select && calendar.disabled( calendarItem.select ) ) { + calendar.set( 'select', calendarItem.select, options ) + } + if ( calendarItem.highlight && calendar.disabled( calendarItem.highlight ) ) { + calendar.set( 'highlight', calendarItem.highlight, options ) + } } return calendar } //DatePicker.prototype.set @@ -183,23 +198,23 @@ if ( value == -Infinity || value == Infinity ) { isInfiniteValue = value } // If it’s an object, use the native date object. - else if ( $.isPlainObject( value ) && Picker._.isInteger( value.pick ) ) { + else if ( $.isPlainObject( value ) && _.isInteger( value.pick ) ) { value = value.obj } // If it’s an array, convert it into a date and make sure // that it’s a valid date – otherwise default to today. else if ( $.isArray( value ) ) { value = new Date( value[ 0 ], value[ 1 ], value[ 2 ] ) - value = Picker._.isDate( value ) ? value : calendar.create().obj + value = _.isDate( value ) ? value : calendar.create().obj } // If it’s a number or date object, make a normalized date. - else if ( Picker._.isInteger( value ) || Picker._.isDate( value ) ) { + else if ( _.isInteger( value ) || _.isDate( value ) ) { value = calendar.normalize( new Date( value ), options ) } // If it’s a literal true or any other case, set it to now. else /*if ( value === true )*/ { @@ -217,40 +232,134 @@ } } //DatePicker.prototype.create /** + * Create a range limit object using an array, date object, + * literal “true”, or integer relative to another time. + */ +DatePicker.prototype.createRange = function( from, to ) { + + var calendar = this, + createDate = function( date ) { + if ( date === true || $.isArray( date ) || _.isDate( date ) ) { + return calendar.create( date ) + } + return date + } + + // Create objects if possible. + if ( !_.isInteger( from ) ) { + from = createDate( from ) + } + if ( !_.isInteger( to ) ) { + to = createDate( to ) + } + + // Create relative dates. + if ( _.isInteger( from ) && $.isPlainObject( to ) ) { + from = [ to.year, to.month, to.date + from ]; + } + else if ( _.isInteger( to ) && $.isPlainObject( from ) ) { + to = [ from.year, from.month, from.date + to ]; + } + + return { + from: createDate( from ), + to: createDate( to ) + } +} //DatePicker.prototype.createRange + + +/** + * Check if a date unit falls within a date range object. + */ +DatePicker.prototype.withinRange = function( range, dateUnit ) { + range = this.createRange(range.from, range.to) + return dateUnit.pick >= range.from.pick && dateUnit.pick <= range.to.pick +} + + +/** + * Check if two date range objects overlap. + */ +DatePicker.prototype.overlapRanges = function( one, two ) { + + var calendar = this + + // Convert the ranges into comparable dates. + one = calendar.createRange( one.from, one.to ) + two = calendar.createRange( two.from, two.to ) + + return calendar.withinRange( one, two.from ) || calendar.withinRange( one, two.to ) || + calendar.withinRange( two, one.from ) || calendar.withinRange( two, one.to ) +} + + +/** * Get the date today. */ DatePicker.prototype.now = function( type, value, options ) { value = new Date() if ( options && options.rel ) { value.setDate( value.getDate() + options.rel ) } return this.normalize( value, options ) -} //DatePicker.prototype.now +} /** * Navigate to next/prev month. */ DatePicker.prototype.navigate = function( type, value, options ) { - if ( $.isPlainObject( value ) ) { + var targetDateObject, + targetYear, + targetMonth, + targetDate, + isTargetArray = $.isArray( value ), + isTargetObject = $.isPlainObject( value ), + viewsetObject = this.item.view/*, + safety = 100*/ - var targetDateObject = new Date( value.year, value.month + ( options && options.nav ? options.nav : 0 ), 1 ), - year = targetDateObject.getFullYear(), - month = targetDateObject.getMonth(), - date = value.date - // Make sure the date is valid and if the month we’re going to doesn’t have enough - // days, keep decreasing the date until we reach the month’s last date. - while ( Picker._.isDate( targetDateObject ) && new Date( year, month, date ).getMonth() !== month ) { - date -= 1 + if ( isTargetArray || isTargetObject ) { + + if ( isTargetObject ) { + targetYear = value.year + targetMonth = value.month + targetDate = value.date } + else { + targetYear = +value[0] + targetMonth = +value[1] + targetDate = +value[2] + } - value = [ year, month, date ] + // If we’re navigating months but the view is in a different + // month, navigate to the view’s year and month. + if ( options && options.nav && viewsetObject && viewsetObject.month !== targetMonth ) { + targetYear = viewsetObject.year + targetMonth = viewsetObject.month + } + + // Figure out the expected target year and month. + targetDateObject = new Date( targetYear, targetMonth + ( options && options.nav ? options.nav : 0 ), 1 ) + targetYear = targetDateObject.getFullYear() + targetMonth = targetDateObject.getMonth() + + // If the month we’re going to doesn’t have enough days, + // keep decreasing the date until we reach the month’s last date. + while ( /*safety &&*/ new Date( targetYear, targetMonth, targetDate ).getMonth() !== targetMonth ) { + targetDate -= 1 + /*safety -= 1 + if ( !safety ) { + throw 'Fell into an infinite loop while navigating to ' + new Date( targetYear, targetMonth, targetDate ) + '.' + }*/ + } + + value = [ targetYear, targetMonth, targetDate ] } return value } //DatePicker.prototype.navigate @@ -275,11 +384,11 @@ if ( !value ) { value = type == 'min' ? -Infinity : Infinity } // If it's an integer, get a date relative to today. - else if ( Picker._.isInteger( value ) ) { + else if ( _.isInteger( value ) ) { value = calendar.now( type, value, { rel: value } ) } return value } ///DatePicker.prototype.measure @@ -328,29 +437,31 @@ if ( dateTime < dateObject.pick ) hasEnabledBeforeTarget = true else if ( dateTime > dateObject.pick ) hasEnabledAfterTarget = true } // Return only integers for enabled weekdays. - return Picker._.isInteger( value ) - }).length + return _.isInteger( value ) + }).length/*, + safety = 100*/ + // Cases to validate for: // [1] Not inverted and date disabled. // [2] Inverted and some dates enabled. - // [3] Out of range. + // [3] Not inverted and out of range. // // Cases to **not** validate for: // • Navigating months. // • Not inverted and date enabled. // • Inverted and all dates disabled. // • ..and anything else. - if ( !options.nav ) if ( + if ( !options || !options.nav ) if ( /* 1 */ ( !isFlippedBase && calendar.disabled( dateObject ) ) || /* 2 */ ( isFlippedBase && calendar.disabled( dateObject ) && ( hasEnabledWeekdays || hasEnabledBeforeTarget || hasEnabledAfterTarget ) ) || - /* 3 */ ( dateObject.pick <= minLimitObject.pick || dateObject.pick >= maxLimitObject.pick ) + /* 3 */ ( !isFlippedBase && (dateObject.pick <= minLimitObject.pick || dateObject.pick >= maxLimitObject.pick) ) ) { // When inverted, flip the direction if there aren’t any enabled weekdays // and there are no enabled dates in the direction of the interval. @@ -358,28 +469,35 @@ interval *= -1 } // Keep looping until we reach an enabled date. - while ( calendar.disabled( dateObject ) ) { + while ( /*safety &&*/ calendar.disabled( dateObject ) ) { + /*safety -= 1 + if ( !safety ) { + throw 'Fell into an infinite loop while validating ' + dateObject.obj + '.' + }*/ - // If we’ve looped into the next/prev month, return to the original date and flatten the interval. + + // If we’ve looped into the next/prev month with a large interval, return to the original date and flatten the interval. if ( Math.abs( interval ) > 1 && ( dateObject.month < originalDateObject.month || dateObject.month > originalDateObject.month ) ) { dateObject = originalDateObject - interval = Math.abs( interval ) / interval + interval = interval > 0 ? 1 : -1 } - // If we’ve reached the min/max limit, reverse the direction and flatten the interval. + // If we’ve reached the min/max limit, reverse the direction, flatten the interval and set it to the limit. if ( dateObject.pick <= minLimitObject.pick ) { reachedMin = true interval = 1 + dateObject = calendar.create([ minLimitObject.year, minLimitObject.month, minLimitObject.date - 1 ]) } else if ( dateObject.pick >= maxLimitObject.pick ) { reachedMax = true interval = -1 + dateObject = calendar.create([ maxLimitObject.year, maxLimitObject.month, maxLimitObject.date + 1 ]) } // If we’ve reached both limits, just break out of the loop. if ( reachedMin && reachedMax ) { @@ -400,70 +518,81 @@ /** * Check if a date is disabled. */ -DatePicker.prototype.disabled = function( dateObject ) { +DatePicker.prototype.disabled = function( dateToVerify ) { - var calendar = this, + var + calendar = this, // Filter through the disabled dates to check if this is one. isDisabledMatch = calendar.item.disable.filter( function( dateToDisable ) { // If the date is a number, match the weekday with 0index and `firstDay` check. - if ( Picker._.isInteger( dateToDisable ) ) { - return dateObject.day === ( calendar.settings.firstDay ? dateToDisable : dateToDisable - 1 ) % 7 + if ( _.isInteger( dateToDisable ) ) { + return dateToVerify.day === ( calendar.settings.firstDay ? dateToDisable : dateToDisable - 1 ) % 7 } // If it’s an array or a native JS date, create and match the exact date. - if ( $.isArray( dateToDisable ) || Picker._.isDate( dateToDisable ) ) { - return dateObject.pick === calendar.create( dateToDisable ).pick + if ( $.isArray( dateToDisable ) || _.isDate( dateToDisable ) ) { + return dateToVerify.pick === calendar.create( dateToDisable ).pick } + + // If it’s an object, match a date within the “from” and “to” range. + if ( $.isPlainObject( dateToDisable ) ) { + return calendar.withinRange( dateToDisable, dateToVerify ) + } }) // If this date matches a disabled date, confirm it’s not inverted. isDisabledMatch = isDisabledMatch.length && !isDisabledMatch.filter(function( dateToDisable ) { - return $.isArray( dateToDisable ) && dateToDisable[3] == 'inverted' + return $.isArray( dateToDisable ) && dateToDisable[3] == 'inverted' || + $.isPlainObject( dateToDisable ) && dateToDisable.inverted }).length // Check the calendar “enabled” flag and respectively flip the // disabled state. Then also check if it’s beyond the min/max limits. return calendar.item.enable === -1 ? !isDisabledMatch : isDisabledMatch || - dateObject.pick < calendar.item.min.pick || - dateObject.pick > calendar.item.max.pick + dateToVerify.pick < calendar.item.min.pick || + dateToVerify.pick > calendar.item.max.pick } //DatePicker.prototype.disabled /** * Parse a string into a usable type. */ DatePicker.prototype.parse = function( type, value, options ) { var calendar = this, - parsingObject = {} + parsingObject = {}, + monthIndex - if ( !value || Picker._.isInteger( value ) || $.isArray( value ) || Picker._.isDate( value ) || $.isPlainObject( value ) && Picker._.isInteger( value.pick ) ) { + if ( !value || _.isInteger( value ) || $.isArray( value ) || _.isDate( value ) || $.isPlainObject( value ) && _.isInteger( value.pick ) ) { return value } - // We need a `.format` to parse the value. + // We need a `.format` to parse the value with. if ( !( options && options.format ) ) { - // should probably default to the default format. - throw "Need a formatting option to parse this.." + options = options || {} + options.format = calendar.settings.format } + // Calculate the month index to adjust with. + monthIndex = typeof value == 'string' && !options.fromValue ? 1 : 0 + // Convert the format into an array and then map through it. calendar.formats.toArray( options.format ).map( function( label ) { var // Grab the formatting label. formattingLabel = calendar.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, calendar, [ value, parsingObject ] ) : label.replace( /^!/, '' ).length + formatLength = formattingLabel ? _.trigger( formattingLabel, calendar, [ 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 ) @@ -472,11 +601,15 @@ // Update the value as the substring from format length to end. value = value.substr( formatLength ) }) // If it’s parsing a user provided month value, compensate for month 0index. - return [ parsingObject.yyyy || parsingObject.yy, +( parsingObject.mm || parsingObject.m ) - ( options.data ? 1 : 0 ), parsingObject.dd || parsingObject.d ] + return [ + parsingObject.yyyy || parsingObject.yy, + +( parsingObject.mm || parsingObject.m ) - monthIndex, + parsingObject.dd || parsingObject.d + ] } //DatePicker.prototype.parse /** * Various formats to display the object in. @@ -507,17 +640,17 @@ d: function( string, dateObject ) { // If there's string, then get the digits length. // Otherwise return the selected date. - return string ? Picker._.digits( string ) : dateObject.date + return string ? _.digits( string ) : dateObject.date }, dd: function( string, dateObject ) { // If there's a string, then the length is always 2. // Otherwise return the selected date with a leading zero. - return string ? 2 : Picker._.lead( dateObject.date ) + return string ? 2 : _.lead( dateObject.date ) }, ddd: function( string, dateObject ) { // If there's a string, then get the length of the first word. // Otherwise return the short selected weekday. @@ -531,17 +664,17 @@ }, m: function( string, dateObject ) { // If there's a string, then get the length of the digits // Otherwise return the selected month with 0index compensation. - return string ? Picker._.digits( string ) : dateObject.month + 1 + return string ? _.digits( string ) : dateObject.month + 1 }, mm: function( string, dateObject ) { // If there's a string, then the length is always 2. // Otherwise return the selected month with 0index and leading zero. - return string ? 2 : Picker._.lead( dateObject.month + 1 ) + return string ? 2 : _.lead( dateObject.month + 1 ) }, mmm: function( string, dateObject ) { var collection = this.settings.monthsShort @@ -575,185 +708,251 @@ // Format an object into a string using the formatting options. toString: function ( formatString, itemObject ) { var calendar = this return calendar.formats.toArray( formatString ).map( function( label ) { - return Picker._.trigger( calendar.formats[ label ], calendar, [ 0, itemObject ] ) || label.replace( /^!/, '' ) + return _.trigger( calendar.formats[ label ], calendar, [ 0, itemObject ] ) || label.replace( /^!/, '' ) }).join( '' ) } } })() //DatePicker.prototype.formats + + /** - * Flip an item as enabled or disabled. + * Check if two date units are the exact. */ -DatePicker.prototype.flipItem = function( type, value/*, options*/ ) { +DatePicker.prototype.isDateExact = function( one, two ) { - var calendar = this, - collection = calendar.item.disable, - isFlippedBase = calendar.item.enable === -1 + var calendar = this - // Flip the enabled and disabled dates. - if ( value == 'flip' ) { - calendar.item.enable = isFlippedBase ? 1 : -1 + // When we’re working with weekdays, do a direct comparison. + if ( + ( _.isInteger( one ) && _.isInteger( two ) ) || + ( typeof one == 'boolean' && typeof two == 'boolean' ) + ) { + return one === two } - // Reset the collection and enable the base state. - else if ( ( type == 'enable' && value === true ) || ( type == 'disable' && value === false ) ) { - calendar.item.enable = 1 - collection = [] + // When we’re working with date representations, compare the “pick” value. + if ( + ( _.isDate( one ) || $.isArray( one ) ) && + ( _.isDate( two ) || $.isArray( two ) ) + ) { + return calendar.create( one ).pick === calendar.create( two ).pick } - // Reset the collection and disable the base state. - else if ( ( type == 'enable' && value === false ) || ( type == 'disable' && value === true ) ) { - calendar.item.enable = -1 - collection = [] + // When we’re working with range objects, compare the “from” and “to”. + if ( $.isPlainObject( one ) && $.isPlainObject( two ) ) { + return calendar.isDateExact( one.from, two.from ) && calendar.isDateExact( one.to, two.to ) } - // Make sure a collection of things was passed to add/remove. - else if ( $.isArray( value ) ) { + return false +} - // Check if we have to add/remove from collection. - if ( isFlippedBase && type == 'enable' || !isFlippedBase && type == 'disable' ) { - collection = calendar.addDisabled( collection, value ) - } - else if ( !isFlippedBase && type == 'enable' ) { - collection = calendar.addEnabled( collection, value ) - } - else if ( isFlippedBase && type == 'disable' ) { - collection = calendar.removeDisabled( collection, value ) - } - } - return collection -} //DatePicker.prototype.flipItem - - /** - * Add an enabled (inverted) item to the disabled collection. + * Check if two date units overlap. */ -DatePicker.prototype.addEnabled = function( collection, item ) { +DatePicker.prototype.isDateOverlap = function( one, two ) { var calendar = this - // Go through each item to enable. - item.map( function( timeUnit ) { + // When we’re working with a weekday index, compare the days. + if ( _.isInteger( one ) && ( _.isDate( two ) || $.isArray( two ) ) ) { + return one === calendar.create( two ).day + 1 + } + if ( _.isInteger( two ) && ( _.isDate( one ) || $.isArray( one ) ) ) { + return two === calendar.create( one ).day + 1 + } - // Check if the time unit is already within the collection. - if ( calendar.filterDisabled( collection, timeUnit, 1 ).length ) { + // When we’re working with range objects, check if the ranges overlap. + if ( $.isPlainObject( one ) && $.isPlainObject( two ) ) { + return calendar.overlapRanges( one, two ) + } - // Remove the unit directly from the collection. - collection = calendar.removeDisabled( collection, [timeUnit] ) + return false +} - // If the unit is an array and it falls within a - // disabled weekday, invert it and then insert it. - if ( - $.isArray( timeUnit ) && - collection.filter( function( disabledDate ) { - return Picker._.isInteger( disabledDate ) && calendar.create( timeUnit ).day === disabledDate - 1 - }).length - ) { - timeUnit = timeUnit.slice(0) - timeUnit.push( 'inverted' ) - collection.push( timeUnit ) - } - } - }) - // Return the final collection. - return collection -} //DatePicker.prototype.addEnabled +/** + * Flip the “enabled” state. + */ +DatePicker.prototype.flipEnable = function(val) { + var itemObject = this.item + itemObject.enable = val || (itemObject.enable == -1 ? 1 : -1) +} /** - * Add an item to the disabled collection. + * Mark a collection of dates as “disabled”. */ -DatePicker.prototype.addDisabled = function( collection, item ) { +DatePicker.prototype.deactivate = function( type, datesToDisable ) { - var calendar = this + var calendar = this, + disabledItems = calendar.item.disable.slice(0) - // Go through each item to disable. - item.map( function( timeUnit ) { - // Add the time unit if it isn’t already within the collection. - if ( !calendar.filterDisabled( collection, timeUnit ).length ) { - collection.push( timeUnit ) - } + // If we’re flipping, that’s all we need to do. + if ( datesToDisable == 'flip' ) { + calendar.flipEnable() + } - // If the time unit is an array and falls within the range, just remove it. - else if ( $.isArray( timeUnit ) && calendar.filterDisabled( collection, timeUnit, 1 ).length ) { - collection = calendar.removeDisabled( collection, [timeUnit] ) - } - }) + else if ( datesToDisable === false ) { + calendar.flipEnable(1) + disabledItems = [] + } - // Return the final collection. - return collection -} //DatePicker.prototype.addDisabled + else if ( datesToDisable === true ) { + calendar.flipEnable(-1) + disabledItems = [] + } + // Otherwise go through the dates to disable. + else { -/** - * Remove an item from the disabled collection. - */ -DatePicker.prototype.removeDisabled = function( collection, item ) { + datesToDisable.map(function( unitToDisable ) { - var calendar = this + var matchFound - // Go through each item to enable. - item.map( function( timeUnit ) { + // 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 ( calendar.isDateExact( unitToDisable, disabledItems[index] ) ) { + matchFound = true + break + } + } - // Filter each item out of the collection. - collection = calendar.filterDisabled( collection, timeUnit, 1 ) - }) + // 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 final colleciton. - return collection -} //DatePicker.prototype.removeDisabled + // Return the updated collection. + return disabledItems +} //DatePicker.prototype.deactivate /** - * Filter through the disabled collection to find a time unit. + * Mark a collection of dates as “enabled”. */ -DatePicker.prototype.filterDisabled = function( collection, timeUnit, isRemoving ) { +DatePicker.prototype.activate = function( type, datesToEnable ) { var calendar = this, + disabledItems = calendar.item.disable, + disabledItemsCount = disabledItems.length - // Check if the time unit passed is an array or date object. - timeIsObject = $.isArray( timeUnit ) || Picker._.isDate( timeUnit ), + // If we’re flipping, that’s all we need to do. + if ( datesToEnable == 'flip' ) { + calendar.flipEnable() + } - // Grab the comparison value if it’s an object. - timeObjectValue = timeIsObject && calendar.create( timeUnit ).pick + else if ( datesToEnable === true ) { + calendar.flipEnable(1) + disabledItems = [] + } - // Go through the disabled collection and try to match this time unit. - return collection.filter( function( disabledTimeUnit ) { + else if ( datesToEnable === false ) { + calendar.flipEnable(-1) + disabledItems = [] + } - // Check if it’s an object and the collection item is an object, - // use the comparison values. Otherwise to a direct comparison. - var isMatch = timeIsObject && ( $.isArray( disabledTimeUnit ) || Picker._.isDate( disabledTimeUnit ) ) ? - timeObjectValue === calendar.create( disabledTimeUnit ).pick : timeUnit === disabledTimeUnit + // Otherwise go through the disabled dates. + else { - // Invert the match if we’re removing from the collection. - return isRemoving ? !isMatch : isMatch - }) -} //DatePicker.prototype.filterDisabled + datesToEnable.map(function( unitToEnable ) { + var matchFound, + disabledUnit, + index, + isExactRange + // 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 ( calendar.isDateExact( disabledUnit, unitToEnable ) ) { + matchFound = disabledItems[index] = null + isExactRange = true + break + } + + // When an overlapped match is found, add the “inverted” state to it. + else if ( calendar.isDateOverlap( disabledUnit, unitToEnable ) ) { + if ( $.isPlainObject( unitToEnable ) ) { + unitToEnable.inverted = true + matchFound = unitToEnable + } + else if ( $.isArray( unitToEnable ) ) { + matchFound = unitToEnable + if ( !matchFound[3] ) 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 ( calendar.isDateExact( disabledItems[index], unitToEnable ) ) { + disabledItems[index] = null + break + } + } + + // In the event that we’re dealing with an exact range of dates, + // make sure there are no “inverted” dates because of it. + if ( isExactRange ) for ( index = 0; index < disabledItemsCount; index += 1 ) { + if ( calendar.isDateOverlap( 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 }) +} //DatePicker.prototype.activate + + /** * Create a string for the nodes in the picker. */ DatePicker.prototype.nodes = function( isOpen ) { var calendar = this, settings = calendar.settings, - nowObject = calendar.item.now, - selectedObject = calendar.item.select, - highlightedObject = calendar.item.highlight, - viewsetObject = calendar.item.view, - disabledCollection = calendar.item.disable, - minLimitObject = calendar.item.min, - maxLimitObject = calendar.item.max, + calendarItem = calendar.item, + nowObject = calendarItem.now, + selectedObject = calendarItem.select, + highlightedObject = calendarItem.highlight, + viewsetObject = calendarItem.view, + disabledCollection = calendarItem.disable, + minLimitObject = calendarItem.min, + maxLimitObject = calendarItem.max, // Create the calendar table head using a copy of weekday labels collection. // * We do a copy so we don't mutate the original array. tableHead = (function( collection ) { @@ -762,33 +961,36 @@ if ( settings.firstDay ) { collection.push( collection.shift() ) } // Create and return the table head group. - return Picker._.node( + return _.node( 'thead', - Picker._.group({ - min: 0, - max: DAYS_IN_WEEK - 1, - i: 1, - node: 'th', - item: function( counter ) { - return [ - collection[ counter ], - settings.klass.weekdays - ] - } - }) + _.node( + 'tr', + _.group({ + min: 0, + max: DAYS_IN_WEEK - 1, + i: 1, + node: 'th', + item: function( counter ) { + return [ + collection[ counter ], + settings.klass.weekdays + ] + } + }) + ) ) //endreturn })( ( settings.showWeekdaysFull ? settings.weekdaysFull : settings.weekdaysShort ).slice( 0 ) ), //tableHead // Create the nav for next/prev month. createMonthNav = function( next ) { // Otherwise, return the created month tag. - return Picker._.node( + return _.node( 'div', ' ', settings.klass[ 'nav' + ( next ? 'Next' : 'Prev' ) ] + ( // If the focused month is outside the range, disabled the button. @@ -805,11 +1007,11 @@ createMonthLabel = function( monthsCollection ) { // If there are months to select, add a dropdown menu. if ( settings.selectMonths ) { - return Picker._.node( 'select', Picker._.group({ + return _.node( 'select', _.group({ min: 0, max: 11, i: 1, node: 'option', item: function( loopedMonth ) { @@ -833,11 +1035,11 @@ } }), settings.klass.selectMonth, isOpen ? '' : 'disabled' ) } // If there's a need for a month selector - return Picker._.node( 'div', monthsCollection[ viewsetObject.month ], settings.klass.month ) + return _.node( 'div', monthsCollection[ viewsetObject.month ], settings.klass.month ) }, //createMonthLabel // Create the year label. createYearLabel = function() { @@ -874,11 +1076,11 @@ lowestYear -= availableYears > neededYears ? neededYears : availableYears highestYear = maxYear } - return Picker._.node( 'select', Picker._.group({ + return _.node( 'select', _.group({ min: lowestYear, max: highestYear, i: 1, node: 'option', item: function( loopedYear ) { @@ -893,38 +1095,38 @@ } }), settings.klass.selectYear, isOpen ? '' : 'disabled' ) } // Otherwise just return the year focused - return Picker._.node( 'div', focusedYear, settings.klass.year ) + return _.node( 'div', focusedYear, settings.klass.year ) } //createYearLabel // Create and return the entire calendar. - return Picker._.node( + return _.node( 'div', createMonthNav() + createMonthNav( 1 ) + createMonthLabel( settings.showMonthsShort ? settings.monthsShort : settings.monthsFull ) + createYearLabel(), settings.klass.header - ) + Picker._.node( + ) + _.node( 'table', tableHead + - Picker._.node( + _.node( 'tbody', - Picker._.group({ + _.group({ min: 0, max: WEEKS_IN_CALENDAR - 1, i: 1, node: 'tr', item: function( rowCounter ) { // If Monday is the first day and the month starts on Sunday, shift the date back a week. var shiftDateBy = settings.firstDay && calendar.create([ viewsetObject.year, viewsetObject.month, 1 ]).day === 0 ? -7 : 0 return [ - Picker._.group({ + _.group({ min: DAYS_IN_WEEK * rowCounter - viewsetObject.day + shiftDateBy + 1, // Add 1 for weekday 0index max: function() { return this.min + DAYS_IN_WEEK - 1 }, i: 1, @@ -932,12 +1134,16 @@ item: function( targetDate ) { // Convert the time date from a relative date to a target date. targetDate = calendar.create([ viewsetObject.year, viewsetObject.month, targetDate + ( settings.firstDay ? 1 : 0 ) ]) + var isSelected = selectedObject && selectedObject.pick == targetDate.pick, + isHighlighted = highlightedObject && highlightedObject.pick == targetDate.pick, + isDisabled = disabledCollection && calendar.disabled( targetDate ) || targetDate.pick < minLimitObject.pick || targetDate.pick > maxLimitObject.pick + return [ - Picker._.node( + _.node( 'div', targetDate.date, (function( klasses ) { // Add the `infocus` or `outfocus` classes based on month in view. @@ -947,27 +1153,37 @@ if ( nowObject.pick == targetDate.pick ) { klasses.push( settings.klass.now ) } // Add the `selected` class if something's selected and the time matches. - if ( selectedObject && selectedObject.pick == targetDate.pick ) { + if ( isSelected ) { klasses.push( settings.klass.selected ) } // Add the `highlighted` class if something's highlighted and the time matches. - if ( highlightedObject && highlightedObject.pick == targetDate.pick ) { + if ( isHighlighted ) { klasses.push( settings.klass.highlighted ) } // Add the `disabled` class if something's disabled and the object matches. - if ( disabledCollection && calendar.disabled( targetDate ) || targetDate.pick < minLimitObject.pick || targetDate.pick > maxLimitObject.pick ) { + if ( isDisabled ) { klasses.push( settings.klass.disabled ) } return klasses.join( ' ' ) })([ settings.klass.day ]), - 'data-pick=' + targetDate.pick + 'data-pick=' + targetDate.pick + ' ' + _.ariaAttr({ + role: 'button', + controls: calendar.$node[0].id, + checked: isSelected && calendar.$node.val() === _.trigger( + calendar.formats.toString, + calendar, + [ settings.format, targetDate ] + ) ? true : null, + activedescendant: isHighlighted ? true : null, + disabled: isDisabled ? true : null + }) ) ] //endreturn } }) ] //endreturn @@ -976,13 +1192,13 @@ ), settings.klass.table ) + // * For Firefox forms to submit, make sure to set the buttons’ `type` attributes as “button”. - Picker._.node( + _.node( 'div', - Picker._.node( 'button', settings.today, settings.klass.buttonToday, 'type=button data-pick=' + nowObject.pick + ( isOpen ? '' : ' disabled' ) ) + - Picker._.node( 'button', settings.clear, settings.klass.buttonClear, 'type=button data-clear=1' + ( isOpen ? '' : ' disabled' ) ), + _.node( 'button', settings.today, settings.klass.buttonToday, 'type=button data-pick=' + nowObject.pick + ( isOpen ? '' : ' disabled' ) ) + + _.node( 'button', settings.clear, settings.klass.buttonClear, 'type=button data-clear=1' + ( isOpen ? '' : ' disabled' ) ), settings.klass.footer ) //endreturn } //DatePicker.prototype.nodes