summaryrefslogtreecommitdiff
path: root/pickadate.js
diff options
context:
space:
mode:
authorJules Laplace <jules@okfoc.us>2016-06-16 20:21:51 -0400
committerJules Laplace <jules@okfoc.us>2016-06-16 20:21:51 -0400
commitfcfe603abdb124a318c42f958ce7db456bdb278c (patch)
treeae615cf9a50b6098fe7e5a800ff3336ba2ef985a /pickadate.js
first commit
Diffstat (limited to 'pickadate.js')
-rwxr-xr-xpickadate.js1576
1 files changed, 1576 insertions, 0 deletions
diff --git a/pickadate.js b/pickadate.js
new file mode 100755
index 0000000..132418e
--- /dev/null
+++ b/pickadate.js
@@ -0,0 +1,1576 @@
+/*!
+ * pickadate.js v1.4.0 - 06 December, 2012
+ * By Amsul (http://amsul.ca)
+ * Hosted on https://github.com/amsul/pickadate.js
+ * Licensed under MIT ("expat" flavour) license.
+ */
+
+/**
+ * TODO: scroll calendar into view
+ * TODO: click to close on iOS
+ */
+
+/*jshint
+ debug: true,
+ devel: true,
+ browser: true,
+ asi: true,
+ unused: true,
+ eqnull: true
+ */
+
+
+
+;(function( $, window, document, undefined ) {
+
+ 'use strict';
+
+
+
+ var
+
+ // Globals & constants
+ DAYS_IN_WEEK = 7,
+ WEEKS_IN_CALENDAR = 6,
+ DAYS_IN_CALENDAR = WEEKS_IN_CALENDAR * DAYS_IN_WEEK,
+
+ STRING_DIV = 'div',
+ STRING_PREFIX_DATEPICKER = 'pickadate__',
+
+ $document = $( document ),
+
+
+ /**
+ * The picker constructor that acceps the
+ * jQuery element and the merged settings
+ */
+ Picker = function( $ELEMENT, SETTINGS ) {
+
+ var
+ // Pseudo picker constructor
+ Picker = function() {},
+
+
+ // The picker prototype
+ P = Picker.prototype = {
+
+ constructor: Picker,
+
+ /**
+ * Initialize everything
+ */
+ init: function() {
+
+ // Insert everything after the element
+ // while binding the events to the element
+ $ELEMENT.on({
+ 'focusin click': P.open,
+ keydown: function( event ) {
+
+ var keycode = event.keyCode
+
+ // If backspace was pressed or if the calendar
+ // is closed and the keycode warrants a date change,
+ // prevent it from going any further.
+ if ( keycode == 8 || !CALENDAR.isOpen && KEYCODE_TO_DATE[ keycode ] ) {
+
+ // Prevent it from moving the page
+ event.preventDefault()
+
+ // Prevent it from propagating to document
+ event.stopPropagation()
+
+ // Open the calendar if backspace wasn't pressed
+ if ( keycode != 8 ) {
+ P.open()
+ }
+ }
+ }
+ }).after( [ $HOLDER, ELEMENT_HIDDEN ] )
+
+
+ // If the element has autofocus open the calendar
+ if ( ELEMENT.autofocus ) {
+ P.open()
+ }
+
+
+ // Do stuff after rendering the calendar
+ postRender()
+
+
+ // Trigger the onStart method within scope of the picker
+ triggerFunction( SETTINGS.onStart, P )
+
+
+ return P
+ }, //init
+
+
+ /**
+ * Open the calendar
+ */
+ open: function() {
+
+ // If it's already open, do nothing
+ if ( CALENDAR.isOpen ) { return P }
+
+
+ // Set calendar as open
+ CALENDAR.isOpen = true
+
+
+ // Add the "focused" class to the element
+ $ELEMENT.addClass( CLASSES.inputFocus )
+
+
+ // Add the "opened" class to the calendar holder
+ $HOLDER.addClass( CLASSES.open )
+
+
+ // Allow month and year selectors to be focusable
+ if ( CALENDAR.selectMonth ) {
+ CALENDAR.selectMonth.tabIndex = 0
+ }
+ if ( CALENDAR.selectYear ) {
+ CALENDAR.selectYear.tabIndex = 0
+ }
+
+
+ // Bind all the events to the document
+ $document.on( 'click.P' + CALENDAR.id + ' focusin.P' + CALENDAR.id + ' keydown.P' + CALENDAR.id, onDocumentEvent )
+
+
+ // Trigger the onOpen method within scope of the picker
+ triggerFunction( SETTINGS.onOpen, P )
+
+ triggerFunction( SETTINGS.onChangeMonth, P )
+
+ return P
+ }, //open
+
+
+ /**
+ * Close the calendar
+ */
+ close: function() {
+
+ return;
+
+ // Set calendar as closed
+ CALENDAR.isOpen = false
+
+
+ // Remove the "focused" class from the element
+ $ELEMENT.removeClass( CLASSES.inputFocus )
+
+ // Remove the "opened" class from the calendar holder
+ $HOLDER.removeClass( CLASSES.open )
+
+
+ // Disable month and year selectors from being focusable
+ if ( CALENDAR.selectMonth ) {
+ CALENDAR.selectMonth.tabIndex = -1
+ }
+ if ( CALENDAR.selectYear ) {
+ CALENDAR.selectYear.tabIndex = -1
+ }
+
+
+ // Unbind the Picker events from the document
+ $document.off( '.P' + CALENDAR.id )
+
+
+ // Trigger the onClose method within scope of the picker
+ triggerFunction( SETTINGS.onClose, P )
+
+ return P
+ }, //close
+
+
+ /**
+ * Show a month in focus with 0index compensation
+ */
+ show: function( month, year ) {
+ showMonth( --month, year )
+ return P
+ }, //show
+
+
+ /**
+ * Get a date in any format.
+ * Defaults to getting the selected date
+ */
+ getDate: function( format, date ) {
+
+ // Go through the date formats array and
+ // convert the format passed into an array to map
+ // which we join into a string at the end
+ return DATE_FORMATS.toArray( format || SETTINGS.format ).map( function( value ) {
+
+ // Trigger the date formats function
+ // or just return value itself
+ return triggerFunction( DATE_FORMATS[ value ], date || DATE_SELECTED ) || value
+ }).join( '' )
+ }, //getDate
+
+
+ /**
+ * Set the date with month 0index compensation
+ * and an option to do a superficial selection
+ */
+ setDate: function( year, month, date, isSuperficial ) {
+
+ // Compensate for month 0index and create a validated date.
+ // Then set it as the date selected
+ setDateSelected( createValidatedDate([ year, --month, date ]), isSuperficial )
+
+ return P
+ }, //setDate
+
+
+ /**
+ * Get the min or max date based on
+ * the argument being truthy or falsey
+ */
+ getDateLimit: function( upper, format ) {
+
+ // Get the max or min date depending on the `upper` flag
+ return P.getDate( format, upper ? DATE_MAX : DATE_MIN )
+ }, //getDateLimit
+
+
+ /**
+ * Set the min or max date based on second
+ * argument being truthy or falsey.
+ */
+ setDateLimit: function( limit, upper ) {
+
+ // If it's the upper limit
+ if ( upper ) {
+
+ // Set the max date
+ DATE_MAX = createBoundaryDate( limit, upper )
+
+ // If focused month is more than max date set it to max date
+ if ( MONTH_FOCUSED.TIME > DATE_MAX.TIME ) {
+ MONTH_FOCUSED = DATE_MAX
+ }
+ }
+
+ // Otherwise it's the lower limit
+ else {
+
+ // So set the min date
+ DATE_MIN = createBoundaryDate( limit )
+
+ // If focused month is less than min date set it to min date
+ if ( MONTH_FOCUSED.TIME < DATE_MIN.TIME ) {
+ MONTH_FOCUSED = DATE_MIN
+ }
+ }
+
+ // Render a new calendar
+ calendarRender()
+
+ return P
+ }, //setDateLimit
+
+ getMonth: function(){
+ return MONTH_FOCUSED;
+ }
+
+ }, //Picker.prototype
+
+
+ // The element node
+ ELEMENT = (function( element ) {
+
+ // Check the autofocus state, convert the element into
+ // a regular text input to remove user-agent stylings,
+ // and then set the element as readonly
+ element.autofocus = ( element == document.activeElement )
+ element.type = 'text'
+ element.readOnly = true
+ return element
+ })( $ELEMENT[ 0 ] ), //ELEMENT
+
+
+ // The calendar object
+ CALENDAR = {
+ id: ~~( Math.random() * 1e9 )
+ }, //CALENDAR
+
+
+ // The classes
+ CLASSES = SETTINGS.klass,
+
+
+ // The date in various formats
+ DATE_FORMATS = (function() {
+
+ // Get the length of the first word
+ function getFirstWordLength( string ) {
+ return string.match( /\w+/ )[ 0 ].length
+ }
+
+ // If the second character is a digit, length is 2 otherwise 1.
+ function getDigitsLength( string ) {
+ return (/\d/).test( string[ 1 ] ) ? 2 : 1
+ }
+
+ // Get the length of the month from a string
+ function getMonthLength( string, dateObj, collection ) {
+
+ // Grab the first word
+ var word = string.match( /\w+/ )[ 0 ]
+
+ // If there's no index for the date object's month,
+ // find it in the relevant months collection and add 1
+ // because we subtract 1 when we create the date object
+ if ( !dateObj.mm && !dateObj.m ) {
+ dateObj.m = collection.indexOf( word ) + 1
+ }
+
+ // Return the length of the word
+ return word.length
+ }
+
+
+ // Return the date formats object
+ return {
+ d: function( string ) {
+
+ // If there's string, then get the digits length.
+ // Otherwise return the selected date.
+ return string ? getDigitsLength( string ) : this.DATE
+ },
+ dd: function( string ) {
+
+ // If there's a string, then the length is always 2.
+ // Otherwise return the selected date with a leading zero.
+ return string ? 2 : leadZero( this.DATE )
+ },
+ ddd: function( string ) {
+
+ // If there's a string, then get the length of the first word.
+ // Otherwise return the short selected weekday.
+ return string ? getFirstWordLength( string ) : SETTINGS.weekdaysShort[ this.DAY ]
+ },
+ dddd: function( string ) {
+
+ // If there's a string, then get the length of the first word.
+ // Otherwise return the full selected weekday.
+ return string ? getFirstWordLength( string ) : SETTINGS.weekdaysFull[ this.DAY ]
+ },
+ m: function( string ) {
+
+ // If there's a string, then get the length of the digits
+ // Otherwise return the selected month with 0index compensation.
+ return string ? getDigitsLength( string ) : this.MONTH + 1
+ },
+ mm: function( string ) {
+
+ // If there's a string, then the length is always 2.
+ // Otherwise return the selected month with 0index and leading zero.
+ return string ? 2 : leadZero( this.MONTH + 1 )
+ },
+ mmm: function( string, dateObject ) {
+
+ var collection = SETTINGS.monthsShort
+
+ // If there's a string, get length of the relevant month string
+ // from the short months collection. Otherwise return the
+ // selected month from that collection.
+ return string ? getMonthLength( string, dateObject, collection ) : collection[ this.MONTH ]
+ },
+ mmmm: function( string, dateObject ) {
+
+ var collection = SETTINGS.monthsFull
+
+ // If there's a string, get length of the relevant month string
+ // from the full months collection. Otherwise return the
+ // selected month from that collection.
+ return string ? getMonthLength( string, dateObject, collection ) : collection[ this.MONTH ]
+ },
+ yy: function( string ) {
+
+ // If there's a string, then the length is always 2.
+ // Otherwise return the selected year by slicing out the first 2 digits.
+ return string ? 2 : ( '' + this.YEAR ).slice( 2 )
+ },
+ yyyy: function( string ) {
+
+ // If there's a string, then the length is always 4.
+ // Otherwise return the selected year.
+ return string ? 4 : this.YEAR
+ },
+
+ // Create an array by splitting the format passed
+ toArray: function( format ) { return format.split( /(?=\b)(d{1,4}|m{1,4}|y{4}|yy)+(\b)/g ) }
+
+ } //endreturn
+ })(), //DATE_FORMATS
+
+
+ // Create calendar object for today
+ DATE_TODAY = createDate(),
+
+
+ // Create the min date
+ DATE_MIN = createBoundaryDate( SETTINGS.dateMin ),
+
+
+ // Create the max date
+ // * A truthy second argument creates max date
+ DATE_MAX = createBoundaryDate( SETTINGS.dateMax, 1 ),
+
+
+ // Create a collection of dates to disable
+ DATES_TO_DISABLE = (function( datesCollection ) {
+
+ // If a collection was passed
+ // we need to create a calendar date object
+ if ( Array.isArray( datesCollection ) ) {
+
+ // If the "all" flag is true,
+ // remove the flag from the collection and
+ // flip the condition of which dates to disable
+ if ( datesCollection[ 0 ] === true ) {
+ CALENDAR.disabled = datesCollection.shift()
+ }
+
+ // Map through the dates passed
+ // and return the collection
+ return datesCollection.map( function( date ) {
+
+ // If the date is a number, return the date minus 1
+ // for weekday 0index plus the first day of the week
+ if ( !isNaN( date ) ) {
+ return --date + SETTINGS.firstDay
+ }
+
+ // Otherwise assume it's an array and fix the month 0index
+ --date[ 1 ]
+
+ // Then create and return the date,
+ // replacing it in the collection
+ return createDate( date )
+ })
+ }
+ })( SETTINGS.datesDisabled ), //DATES_TO_DISABLE
+
+
+ // Create a function that will filter through the dates
+ // and return true if looped date is to be disabled
+ DISABLED_DATES = (function() {
+
+ // Check if the looped date should be disabled
+ // based on the time being the same as a disabled date
+ // or the day index being within the collection
+ var isDisabledDate = function( date ) {
+ return this.TIME == date.TIME || DATES_TO_DISABLE.indexOf( this.DAY ) > -1
+ }
+
+
+ // If all calendar dates should be disabled,
+ // return a function that maps each date
+ // in the collection of dates to not disable.
+ // Otherwise check if this date should be disabled
+ return CALENDAR.disabled ? function( date, i, collection ) {
+
+ // Map the array of disabled dates
+ // and check if this is not one
+ return ( collection.map( isDisabledDate, this ).indexOf( true ) < 0 )
+ } : isDisabledDate
+ })(), //DISABLED_DATES
+
+
+ // Create calendar object for the highlighted day
+ DATE_HIGHLIGHTED = (function( dateDataValue, dateEntered ) {
+
+ // If there a date `data-value`
+ if ( dateDataValue ) {
+
+ // Set the date entered to an empty object
+ dateEntered = {}
+
+ // Map through the submit format array
+ DATE_FORMATS.toArray( SETTINGS.formatSubmit ).map( function( formatItem ) {
+
+ // If the formatting length function exists, invoke it with the
+ // the format length in the `data-value` and the date we are creating.
+ // Otherwise it is the length of the formatting item being mapped
+ var formattingLength = DATE_FORMATS[ formatItem ] ? DATE_FORMATS[ formatItem ]( dateDataValue, dateEntered ) : formatItem.length
+
+ // If the formatting length function exists, slice up
+ // the value and pass it into the date we're creating.
+ if ( DATE_FORMATS[ formatItem ] ) {
+ dateEntered[ formatItem ] = dateDataValue.slice( 0, formattingLength )
+ }
+
+ // Update the remainder of the string by slicing the format length
+ dateDataValue = dateDataValue.slice( formattingLength )
+ })
+
+ // Finally, create an array with the date entered while
+ // parsing each item as an integer and compensating for 0index
+ dateEntered = [ +(dateEntered.yyyy || dateEntered.yy), +(dateEntered.mm || dateEntered.m) - 1, +(dateEntered.dd || dateEntered.d) ]
+ }
+
+
+ // Otherwise, try to parse the date in the input
+ else {
+ dateEntered = Date.parse( dateEntered )
+ }
+
+
+ // If there's a valid date in the input or the dateEntered
+ // is now an array, create a validated date with it.
+ // Otherwise set the highlighted date to today after validating.
+ return createValidatedDate( !isNaN( dateEntered ) || Array.isArray( dateEntered ) ? dateEntered : DATE_TODAY )
+ })( ELEMENT.getAttribute( 'data-value' ), ELEMENT.value ),
+
+
+ // The date selected is initially the date highlighted
+ DATE_SELECTED = DATE_HIGHLIGHTED,
+
+
+ // Month focused is based on highlighted date
+ MONTH_FOCUSED = DATE_HIGHLIGHTED,
+
+
+ // If there's a format for the hidden input element, create the element
+ // using the name of the original input plus suffix and update the value
+ // with whatever is entered in the input on load. Otherwise set it to zero.
+ ELEMENT_HIDDEN = SETTINGS.formatSubmit ? $( '<input type=hidden name=' + ELEMENT.name + SETTINGS.hiddenSuffix + '>' ).val( ELEMENT.value ? P.getDate( SETTINGS.formatSubmit ) : '' )[ 0 ] : null,
+
+
+ // Create the calendar table head with weekday labels
+ // by "copying" the weekdays collection based on the settings.
+ // * We do a copy so we don't mutate the original array.
+ TABLE_HEAD = (function( weekdaysCollection ) {
+
+ // If the first day should be Monday
+ if ( SETTINGS.firstDay ) {
+
+ // Grab Sunday and push it to the end of the collection
+ weekdaysCollection.push( weekdaysCollection.splice( 0, 1 )[ 0 ] )
+ }
+
+ // Go through each day of the week
+ // and return a wrapped header row.
+ // Take the result and apply another
+ // table head wrapper to group it all.
+ return createNode( 'thead',
+ createNode( 'tr',
+ weekdaysCollection.map( function( weekday ) {
+ return createNode( 'th', weekday, CLASSES.weekdays )
+ })
+ )
+ )
+ })( ( SETTINGS.showWeekdaysShort ? SETTINGS.weekdaysShort : SETTINGS.weekdaysFull ).slice( 0 ) ), //TABLE_HEAD
+
+
+ // Create the calendar holder with a new wrapped calendar and bind the click
+ $HOLDER = $( createNode( STRING_DIV, createCalendarWrapped(), CLASSES.holder ) ).on( 'click', onClickCalendar ),
+
+
+ // Translate a keycode to a relative change in date
+ KEYCODE_TO_DATE = {
+
+ // Down
+ 40: 7,
+
+ // Up
+ 38: -7,
+
+ // Right
+ 39: 1,
+
+ // Left
+ 37: -1
+ } //KEYCODE_TO_DATE
+
+
+
+
+ /**
+ * Create the nav for next/prev month
+ */
+ function createMonthNav( next ) {
+
+ // If the focused month is outside the range
+ // return an empty string
+ if ( ( next && MONTH_FOCUSED.YEAR >= DATE_MAX.YEAR && MONTH_FOCUSED.MONTH >= DATE_MAX.MONTH ) || ( !next && MONTH_FOCUSED.YEAR <= DATE_MIN.YEAR && MONTH_FOCUSED.MONTH <= DATE_MIN.MONTH ) ) {
+ return ''
+ }
+
+ var monthTag = 'month' + ( next ? 'Next' : 'Prev' )
+
+ // Otherwise, return the created tag
+ return createNode( STRING_DIV,
+ SETTINGS[ monthTag ],
+ CLASSES[ monthTag ],
+ 'data-nav=' + ( next || -1 )
+ ) //endreturn
+ } //createMonthNav
+
+
+ /**
+ * Create the month label
+ */
+ function createMonthLabel( monthsCollection ) {
+
+
+ // If there's a need for a month selector
+ return SETTINGS.monthSelector ?
+
+ // Create the dom string node for a select element
+ createNode( 'select',
+
+ // Map through the months collection
+ monthsCollection.map( function( month, monthIndex ) {
+
+ // Create a dom string node for each option
+ return createNode( 'option',
+
+ // With the month and no classes
+ month, 0,
+
+ // Set the value and selected index
+ 'value=' + monthIndex + ( MONTH_FOCUSED.MONTH == monthIndex ? ' selected' : '' ) +
+
+ // Plus the disabled attribute if it's outside the range
+ getMonthInRange( monthIndex, MONTH_FOCUSED.YEAR, ' disabled', '' )
+ )
+ }),
+
+ // The month selector class
+ CLASSES.monthSelector,
+
+ // And some tabindex
+ 'tabindex=' + ( CALENDAR.isOpen ? 0 : -1 )
+
+ // Otherwise just return the month focused
+ ) : createNode( STRING_DIV, monthsCollection[ MONTH_FOCUSED.MONTH ], CLASSES.month )
+ } //createMonthLabel
+
+
+ /**
+ * Create the year label
+ */
+ function createYearLabel() {
+
+ var
+ yearFocused = MONTH_FOCUSED.YEAR,
+ yearsInSelector = SETTINGS.yearSelector
+
+
+ // If there is a need for a years selector
+ // then create a dropdown within the valid range
+ if ( yearsInSelector ) {
+
+ // If year selector setting is true, default to 5.
+ // Otherwise divide the years in selector in half
+ // to get half before and half after
+ yearsInSelector = yearsInSelector === true ? 5 : ~~( yearsInSelector / 2 )
+
+ var
+ // Create a collection to hold the years
+ yearsCollection = [],
+
+ // The lowest year possible is the difference between
+ // the focused year and the number of years in the selector
+ lowestYear = yearFocused - yearsInSelector,
+
+ // The first year is the lower of the two numbers.
+ // The lowest year or the minimum year.
+ firstYear = getNumberInRange( lowestYear, DATE_MIN.YEAR ),
+
+ // The highest year is the sum of the focused year
+ // and the years in selector plus the left over years.
+ highestYear = yearFocused + yearsInSelector + ( firstYear - lowestYear ),
+
+ // The last year is the higher of the two numbers.
+ // The highest year or the maximum year.
+ lastYear = getNumberInRange( highestYear, DATE_MAX.YEAR, 1 )
+
+
+ // Check if there are left over years to put in the selector
+ yearsInSelector = highestYear - lastYear
+
+
+ // If there are left overs
+ if ( yearsInSelector ) {
+
+ // The first year is the lower of the two numbers.
+ // The lowest year minus years in selector, or the minimum year
+ firstYear = getNumberInRange( lowestYear - yearsInSelector, DATE_MIN.YEAR )
+ }
+
+
+ // Add the years to the collection by looping through the range
+ for ( var index = 0; index <= lastYear - firstYear; index += 1 ) {
+ yearsCollection.push( firstYear + index )
+ }
+
+
+ // Create the dom string node for a select element
+ return createNode( 'select',
+
+ // Map through the years collection
+ yearsCollection.map( function( year ) {
+
+ // Create a dom string node for each option
+ return createNode( 'option',
+
+ // With the year and no classes
+ year, 0,
+
+ // Set the value and selected index
+ 'value=' + year + ( yearFocused == year ? ' selected' : '' )
+ )
+ }),
+
+ // The year selector class
+ CLASSES.yearSelector,
+
+ // And some tabindex
+ 'tabindex=' + ( CALENDAR.isOpen ? 0 : -1 )
+ )
+ }
+
+
+ // Otherwise just return the year focused
+ return createNode( STRING_DIV, yearFocused, CLASSES.year )
+ } //createYearLabel
+
+
+ /**
+ * Create the calendar table body
+ */
+ function createTableBody() {
+
+ var
+ // The loop date object
+ loopDate,
+
+ // A pseudo index will be the divider between
+ // the previous month and the focused month
+ pseudoIndex,
+
+ // An array that will hold the classes
+ // and binding for each looped date
+ classAndBinding,
+
+ // Collection of the dates visible on the calendar
+ // * This gets discarded at the end
+ calendarDates = [],
+
+ // Weeks visible on the calendar
+ calendarWeeks = '',
+
+ // Count the number of days in the focused month
+ // by getting the 0-th date of the next month
+ countMonthDays = createDate([ MONTH_FOCUSED.YEAR, MONTH_FOCUSED.MONTH + 1, 0 ]).DATE,
+
+ // Count the days to shift the start of the month
+ countShiftby = getCountShiftDays( MONTH_FOCUSED.DATE, MONTH_FOCUSED.DAY ),
+
+
+ // Set the class and binding for each looped date.
+ // Returns an array with 2 items:
+ // 1) The classes string
+ // 2) The data binding string
+ createDateClassAndBinding = function( loopDate, isMonthFocused ) {
+
+ var
+ // Boolean check for date state
+ isDateDisabled = false,
+
+ // Create a collection for the classes
+ // with the default classes already included
+ klassCollection = [
+
+ // The generic day class
+ CLASSES.day,
+
+ // The class for in or out of focus
+ ( isMonthFocused ? CLASSES.dayInfocus : CLASSES.dayOutfocus )
+ ]
+
+
+ // If it's less than the minimum date
+ // or greater than the maximum date
+ // or if there are dates to disable
+ // and this looped date is one of them
+ if ( loopDate.TIME < DATE_MIN.TIME || loopDate.TIME > DATE_MAX.TIME || ( DATES_TO_DISABLE && DATES_TO_DISABLE.filter( DISABLED_DATES, loopDate ).length ) ) {
+
+ // Flip the boolen
+ isDateDisabled = true
+
+ // Add the disabled class
+ klassCollection.push( CLASSES.dayDisabled )
+ }
+
+
+ // If it's today, add the class
+ if ( loopDate.TIME == DATE_TODAY.TIME ) {
+ klassCollection.push( CLASSES.dayToday )
+ }
+
+
+ // If it's the highlighted date, add the class
+ if ( loopDate.TIME == DATE_HIGHLIGHTED.TIME ) {
+ klassCollection.push( CLASSES.dayHighlighted )
+ }
+
+
+ // If it's the selected date, add the class
+ if ( loopDate.TIME == DATE_SELECTED.TIME ) {
+ klassCollection.push( CLASSES.daySelected )
+ }
+
+
+ // Return an array with the classes and data binding
+ return [
+
+ // Return the classes joined
+ // by a single whitespace
+ klassCollection.join( ' ' ),
+
+ // Create the data binding object
+ // with the value as a string
+ 'data-' + ( isDateDisabled ? 'disabled' : 'date' ) + '=' + [
+ loopDate.YEAR,
+ loopDate.MONTH,
+ loopDate.DATE
+ ].join( '/' )
+ ]
+ } //createDateClassAndBinding
+
+
+
+ // Go through all the days in the calendar
+ // and map a calendar date
+ for ( var index = 0; index < DAYS_IN_CALENDAR; index += 1 ) {
+
+ // Get the distance between the index
+ // and the count to shift by.
+ // This will serve as the separator
+ // between the previous, current,
+ // and next months.
+ pseudoIndex = index - countShiftby
+
+
+ // Create a calendar date with
+ // a negative or positive pseudoIndex
+ loopDate = createDate([ MONTH_FOCUSED.YEAR, MONTH_FOCUSED.MONTH, pseudoIndex ])
+
+
+ // Set the date class and bindings on the looped date.
+ // If the pseudoIndex is greater than zero,
+ // and less than the days in the month,
+ // we need dates from the focused month.
+ classAndBinding = createDateClassAndBinding( loopDate, ( pseudoIndex > 0 && pseudoIndex <= countMonthDays ) )
+
+
+ // Create the looped date wrapper,
+ // and then create the table cell wrapper
+ // and finally pass it to the calendar array
+ calendarDates.push( createNode( 'td', createNode( STRING_DIV, loopDate.DATE, classAndBinding[ 0 ], classAndBinding[ 1 ] ) ) )
+
+
+ // Check if it's the end of a week.
+ // * We add 1 for 0index compensation
+ if ( ( index % DAYS_IN_WEEK ) + 1 == DAYS_IN_WEEK ) {
+
+ // Wrap the week and append it into the calendar weeks
+ calendarWeeks += createNode( 'tr', calendarDates.splice( 0, DAYS_IN_WEEK ) )
+ }
+
+ } //endfor
+
+
+
+ // Join the dates and wrap the calendar body
+ return createNode( 'tbody', calendarWeeks, CLASSES.calendarBody )
+ } //createTableBody
+
+
+ /**
+ * Create the wrapped calendar
+ * using the collection of calendar items
+ * and creating a new table body
+ */
+ function createCalendarWrapped() {
+
+ // Create a calendar wrapper node
+ return createNode( STRING_DIV,
+
+ // Create a calendar box node
+ createNode( STRING_DIV,
+
+ // The prev/next month tags
+ // * Truthy argument creates "next" tag
+ createNode( STRING_DIV, createMonthNav() + createMonthNav( 1 ), CLASSES.monthNav ) +
+
+ // The calendar month tag
+ createNode( STRING_DIV, createMonthLabel( SETTINGS.showMonthsFull ? SETTINGS.monthsFull : SETTINGS.monthsShort ), CLASSES.monthWrap ) +
+
+ // The calendar year tag
+ createNode( STRING_DIV, createYearLabel(), CLASSES.yearWrap ) +
+
+ // The calendar table with table head
+ // and a new calendar table body
+ createNode( 'table', [ TABLE_HEAD, createTableBody() ], CLASSES.calendarTable ),
+
+ // Calendar class
+ CLASSES.calendar
+ ),
+
+ // Calendar wrap class
+ CLASSES.calendarWrap
+ ) //endreturn
+ } //calendarWrapped
+
+
+ /**
+ * Get the number that's allowed within an
+ * upper or lower limit. A truthy third argument
+ * test against the upper limit.
+ */
+ function getNumberInRange( number, limit, upper ) {
+
+ // If we need to test against the upper limit
+ // and number is less than the limit,
+ // or we need to test against the lower limit
+ // and number is more than the limit,
+ // return the number. Otherwise return the limit.
+ return ( ( upper && number < limit ) || ( !upper && number > limit ) ? number : limit )
+ } //getNumberInRange
+
+
+ /**
+ * Get the count of the number of
+ * days to shift the month by,
+ * given the date and day of week
+ */
+ function getCountShiftDays( date, dayIndex ) {
+
+ var
+ // Get the column index for the
+ // day if month starts on 0
+ dayColumnIndexAtZero = date % DAYS_IN_WEEK,
+
+ // Get the difference between the actual
+ // day index and the column index at zero.
+ // Then, if the first day should be Monday,
+ // reduce the difference by 1
+ difference = dayIndex - dayColumnIndexAtZero + ( SETTINGS.firstDay ? -1 : 0 )
+
+
+ // Compare the day index if the
+ // month starts on the first day
+ // with the day index
+ // the date actually falls on
+ return dayIndex >= dayColumnIndexAtZero ?
+
+ // If the actual position is greater
+ // shift by the difference in the two
+ difference :
+
+ // Otherwise shift by the adding the negative
+ // difference to the days in week
+ DAYS_IN_WEEK + difference
+ } //getCountShiftDays
+
+
+
+ /**
+ * Set a date as selected or only highlighted
+ */
+ function setDateSelected( dateTargeted, isHighlight ) {
+
+ // Set the target as the highlight
+ DATE_HIGHLIGHTED = dateTargeted
+
+ // Set the target as the focus
+ MONTH_FOCUSED = dateTargeted
+
+ // If it's just a highlight, render a new calendar
+ if ( isHighlight ) {
+ calendarRender()
+ }
+
+ // Otherwise set the element value as well
+ // * A truthy second argument renders new calendar
+ else {
+ setElementsValue( dateTargeted, 1 )
+ }
+ } //setDateSelected
+
+
+
+ /**
+ * Set the date in the input element and hidden input
+ */
+ function setElementsValue( dateTargeted, updateCalendar ) {
+
+ // Set the target as the selection
+ DATE_SELECTED = dateTargeted
+
+ // Set the element value as the formatted date
+ ELEMENT.value = P.getDate()
+
+ // If there's a hidden input,
+ // set the value with the submit format
+ if ( ELEMENT_HIDDEN ) {
+ ELEMENT_HIDDEN.value = P.getDate( SETTINGS.formatSubmit )
+ }
+
+ // If the calendar should be updated, render a new one
+ if ( updateCalendar ) {
+ calendarRender()
+ }
+
+ // Trigger the onSelect method within scope of the picker
+ triggerFunction( SETTINGS.onSelect, P )
+ } //setElementsValue
+
+
+
+ /**
+ * Set the date that determines
+ * the month to show in focus
+ */
+ function setMonthFocused( month, year ) {
+
+ // Create and return the month focused
+ // * We set the date to first of month
+ // because date doesn't matter here
+ return ( MONTH_FOCUSED = createDate([ year, month, 1 ]) )
+ } //setMonthFocused
+
+
+ /**
+ * Find something within the calendar holder
+ */
+ function $findInHolder( klass ) {
+ return $HOLDER.find( '.' + klass )
+ } //$findInHolder
+
+
+ /**
+ * Show the month visible on the calendar
+ */
+ function showMonth( month, year ) {
+
+ // Ensure we have a year to work with
+ year = year || MONTH_FOCUSED.YEAR
+
+ // Get the month to be within
+ // the minimum and maximum date limits
+ month = getMonthInRange( month, year )
+
+ // Set the month to show in focus
+ setMonthFocused( month, year )
+
+ // Then render a new calendar
+ calendarRender()
+
+ triggerFunction( SETTINGS.onChangeMonth, P )
+ } //showMonth
+
+
+ /**
+ * Create a bounding date allowed on the calendar
+ * * A truthy second argument creates the upper boundary
+ */
+ function createBoundaryDate( limit, upper ) {
+
+ // If the limit is set to true, just return today
+ if ( limit === true ) {
+ return DATE_TODAY
+ }
+
+ // If the limit is an array, construct the date
+ // while fixing month 0index
+ if ( Array.isArray( limit ) ) {
+ --limit[ 1 ]
+ return createDate( limit )
+ }
+
+ // If there is a limit and its a number, create a
+ // calendar date relative to today by adding the limit
+ if ( limit && !isNaN( limit ) ) {
+ return createDate([ DATE_TODAY.YEAR, DATE_TODAY.MONTH, DATE_TODAY.DATE + limit ])
+ }
+
+ // Otherwise create an infinite date
+ return createDate( 0, upper ? Infinity : -Infinity )
+ } //createBoundaryDate
+
+
+ /**
+ * Create a validated date
+ */
+ function createValidatedDate( datePassed, direction ) {
+
+
+ // If the date passed isn't a date, create one
+ datePassed = !datePassed.TIME ? createDate( datePassed ) : datePassed
+
+
+ // If there are disabled dates
+ if ( DATES_TO_DISABLE ) {
+
+ // Create a reference to the original date passed
+ var originalDate = datePassed
+
+ // Check if this date is disabled. If it is,
+ // then keep adding the direction (or 1) to the date
+ // until we get to a date that's enabled.
+ while ( DATES_TO_DISABLE.filter( DISABLED_DATES, datePassed ).length ) {
+
+ // Create the next date based on the direction
+ datePassed = createDate([ datePassed.YEAR, datePassed.MONTH, datePassed.DATE + ( direction || 1 ) ])
+
+ // If we've looped through to another month,
+ // then increase/decrease the date by one and
+ // continue looping with the new original date
+ if ( datePassed.MONTH != originalDate.MONTH ) {
+ datePassed = createDate([ originalDate.YEAR, originalDate.MONTH, direction > 0 ? ++originalDate.DATE : --originalDate.DATE ])
+ originalDate = datePassed
+ }
+ }
+ }
+
+
+ // If it's less that min date, set it to min date
+ // by creating a validated date by adding one
+ // until we find an enabled date
+ if ( datePassed.TIME < DATE_MIN.TIME ) {
+ datePassed = createValidatedDate( DATE_MIN )
+ }
+
+
+ // If it's more than max date, set it to max date
+ // by creating a validated date by subtracting one
+ // until we find an enabled date
+ else if ( datePassed.TIME > DATE_MAX.TIME ) {
+ datePassed = createValidatedDate( DATE_MAX, -1 )
+ }
+
+
+ // Finally, return the date
+ return datePassed
+ } //createValidatedDate
+
+
+ /**
+ * Return a month by comparing with the date range.
+ * If outside the range, returns the value passed.
+ * Otherwise returns the in range value or the month itself.
+ */
+ function getMonthInRange( month, year, returnValue, inRangeValue ) {
+
+ // If the month is less than the min month,
+ // then return the return value or min month
+ if ( year <= DATE_MIN.YEAR && month < DATE_MIN.MONTH ) {
+ return returnValue || DATE_MIN.MONTH
+ }
+
+ // If the month is more than the max month,
+ // then return the return value or max month
+ if ( year >= DATE_MAX.YEAR && month > DATE_MAX.MONTH ) {
+ return returnValue || DATE_MAX.MONTH
+ }
+
+ // Otherwise return the in range return value
+ // or the month itself
+ return inRangeValue != null ? inRangeValue : month
+ } //getMonthInRange
+
+
+ /**
+ * Render a new calendar
+ */
+ function calendarRender() {
+
+ // Create a new wrapped calendar
+ // and place it within the holder
+ $HOLDER.html( createCalendarWrapped() )
+
+ // Do stuff after rendering the calendar
+ postRender()
+ } //calendarRender
+
+
+ /**
+ * Stuff to do after a calendar has been rendered
+ */
+ function postRender() {
+
+ // Find and store the month selector
+ CALENDAR.selectMonth = $findInHolder( CLASSES.monthSelector ).on({
+
+ // *** For iOS ***
+ click: function( event ) { event.stopPropagation() },
+
+ // Bind the change event
+ change: function() {
+
+ // Show the month based on the option selected
+ // while parsing as a float
+ showMonth( +this.value )
+
+ // Find the new month selector and focus back on it
+ $findInHolder( CLASSES.monthSelector ).focus()
+ }
+ })[ 0 ]
+
+ // Find and store the year selector
+ CALENDAR.selectYear = $findInHolder( CLASSES.yearSelector ).on({
+
+ // *** For iOS ***
+ click: function( event ) { event.stopPropagation() },
+
+ // Bind the change event
+ change: function() {
+
+ // Show the year based on the option selected
+ // and month currently in focus while parsing as a float
+ showMonth( MONTH_FOCUSED.MONTH, +this.value )
+
+ // Find the new year selector and focus back on it
+ $findInHolder( CLASSES.yearSelector ).focus()
+ }
+ })[ 0 ]
+ } //postRender
+
+
+
+
+
+
+ /**
+ * Handle all delegated click events on the calendar holder
+ */
+ function onClickCalendar( event ) {
+
+ var
+ // Get the jQuery target
+ $target = $( event.target ),
+
+ // Get the target data
+ targetData = $target.data()
+
+
+ // Stop the event from bubbling up to the document
+ event.stopPropagation()
+
+
+ // Put focus back onto the element
+ $ELEMENT.focus()
+
+
+ // If there's a date provided
+ if ( targetData.date ) {
+
+ // Split the target data into an array while parsing each as integer
+ var dateToSelect = targetData.date.split( '/' ).map( function( value ) { return +value })
+
+ // Create a date from the date to select and set the date as selected
+ // * Falsy second argument updates the element values
+ setDateSelected( createDate( dateToSelect ), false, $target )
+
+ // Close the calendar
+ P.close()
+ }
+
+
+ // If there's a navigator provided
+ if ( targetData.nav ) {
+
+ // Show the month according to the direction
+ showMonth( MONTH_FOCUSED.MONTH + targetData.nav )
+ }
+ } //onClickCalendar
+
+
+
+ /**
+ * Handle all document events when the calendar is open
+ */
+ function onDocumentEvent( event ) {
+
+ var
+ // Get the keycode
+ keycode = event.keyCode,
+
+ // Get the target
+ target = event.target
+
+
+ // If target is not the element, nor the select
+ // menus, close the calendar.
+ if ( target != ELEMENT && target != CALENDAR.selectMonth && target != CALENDAR.selectYear ) {
+ P.close()
+ return
+ }
+
+ // If the target is the select menu, remove the
+ // "focus" state from the input element
+ if ( target == CALENDAR.selectMonth || target == CALENDAR.selectYear ) {
+ $ELEMENT.removeClass( CLASSES.inputFocus )
+ return
+ }
+
+
+ // If theres a keycode and the target is the element
+ if ( keycode && target == ELEMENT ) {
+
+
+ // Prevent the default action if a "super" key
+ // is not held and the tab key isn't pressed,
+ // prevent the default action
+ if ( !event.metaKey && keycode != 9 ) {
+ event.preventDefault()
+ }
+
+
+ // On enter, set the element value as the highlighted date
+ // * Truthy second argument renders a new calendar
+ if ( keycode == 13 ) {
+ setElementsValue( DATE_HIGHLIGHTED, 1 )
+ P.close()
+ return
+ }
+
+
+ // On escape, close the calendar
+ if ( keycode == 27 ) {
+ P.close()
+ return
+ }
+
+
+ // If the keycode translates to a date change,
+ // set the date as superficially selected by
+ // creating new validated dates - incrementing by the date change.
+ // * Truthy second argument makes it a superficial selection
+ if ( KEYCODE_TO_DATE[ keycode ] ) {
+ setDateSelected( createValidatedDate( [ MONTH_FOCUSED.YEAR, MONTH_FOCUSED.MONTH, DATE_HIGHLIGHTED.DATE + KEYCODE_TO_DATE[ keycode ] ], KEYCODE_TO_DATE[ keycode ] ), 1 )
+ }
+
+ } //if ELEMENT
+ } //onDocumentEvent
+
+
+ // Return a new initialized picker
+ return new P.init()
+ } //Picker
+
+
+
+
+
+ /**
+ * Helper functions
+ */
+
+ // Check if a value is a function
+ // and trigger it, if that
+ function triggerFunction( callback, scope ) {
+ if ( typeof callback == 'function' ) {
+ return callback.call( scope )
+ }
+ }
+
+ // Return numbers below 10 with a leading zero
+ function leadZero( number ) {
+ return ( number < 10 ? '0': '' ) + number
+ }
+
+ // Create a dom node string
+ function createNode( wrapper, item, klass, attribute ) {
+
+ // If the item is an array, do a join
+ item = Array.isArray( item ) ? item.join( '' ) : item
+
+ // Check for the class
+ klass = klass ? ' class="' + klass + '"' : ''
+
+ // Check for any attributes
+ attribute = attribute ? ' ' + attribute : ''
+
+ // Return the wrapped item
+ return '<' + wrapper + klass + attribute + '>' + item + '</' + wrapper + '>'
+ } //createNode
+
+ // Create a calendar date
+ function createDate( datePassed, unlimited ) {
+
+ // If the date passed is an array
+ if ( Array.isArray( datePassed ) ) {
+
+ // Create the date
+ datePassed = new Date( datePassed[ 0 ], datePassed[ 1 ], datePassed[ 2 ] )
+ }
+
+ // If the date passed is a number
+ else if ( !isNaN( datePassed ) ) {
+
+ // Create the date
+ datePassed = new Date( datePassed )
+ }
+
+
+ // Otherwise if it's not unlimited
+ else if ( !unlimited ) {
+
+ // Set the date to today
+ datePassed = new Date()
+
+ // Set the time to midnight (for comparison purposes)
+ datePassed.setHours( 0, 0, 0, 0 )
+ }
+
+
+ // Return the calendar date object
+ return {
+ YEAR: unlimited || datePassed.getFullYear(),
+ MONTH: unlimited || datePassed.getMonth(),
+ DATE: unlimited || datePassed.getDate(),
+ DAY: unlimited || datePassed.getDay(),
+ TIME: unlimited || datePassed.getTime()
+ }
+ } //createDate
+
+
+
+
+ /**
+ * Extend jQuery
+ */
+ $.fn.pickadate = function( options ) {
+
+ var pickadate = 'pickadate'
+
+ // Merge the options with a deep copy
+ options = $.extend( true, {}, $.fn.pickadate.defaults, options )
+
+ // Check if it should be disabled
+ // for browsers that natively support `type=date`
+ if ( options.disablePicker ) { return this }
+
+ return this.each( function() {
+ var $this = $( this )
+ if ( this.nodeName == 'INPUT' && !$this.data( pickadate ) ) {
+ $this.data( pickadate, new Picker( $this, options ) )
+ }
+ })
+ } //$.fn.pickadate
+
+
+
+ /**
+ * Default options for the picker
+ */
+ $.fn.pickadate.defaults = {
+
+ monthsFull: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
+ monthsShort: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ],
+
+ weekdaysFull: [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ],
+ weekdaysShort: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ],
+
+ monthPrev: '&#9664;',
+ monthNext: '&#9654;',
+
+ // Display strings
+ showMonthsFull: true,
+ showWeekdaysShort: true,
+
+ // Date format to show on the input element
+ format: 'd mmmm, yyyy',
+
+ // Date format to send to the server
+ formatSubmit: false,
+
+ // Hidden element name suffix
+ hiddenSuffix: '_submit',
+
+ // First day of the week: 0 = Sunday, 1 = Monday
+ firstDay: 0,
+
+ // Month & year dropdown selectors
+ monthSelector: false,
+ yearSelector: false,
+
+ // Date ranges
+ dateMin: false,
+ dateMax: false,
+
+ // Dates to disable
+ datesDisabled: false,
+
+ // Disable for browsers with native date support
+ disablePicker: false,
+
+ // Events
+ onOpen: null,
+ onClose: null,
+ onSelect: null,
+ onChangeMonth: null,
+ onStart: null,
+
+
+ // Classes
+ klass: {
+
+ inputFocus: STRING_PREFIX_DATEPICKER + 'input--focused',
+
+ holder: STRING_PREFIX_DATEPICKER + 'holder',
+ open: STRING_PREFIX_DATEPICKER + 'holder--opened',
+
+ calendar: STRING_PREFIX_DATEPICKER + 'calendar',
+ calendarWrap: STRING_PREFIX_DATEPICKER + 'calendar--wrap',
+ calendarTable: STRING_PREFIX_DATEPICKER + 'calendar--table',
+ calendarBody: STRING_PREFIX_DATEPICKER + 'calendar--body',
+
+ year: STRING_PREFIX_DATEPICKER + 'year',
+ yearWrap: STRING_PREFIX_DATEPICKER + 'year--wrap',
+ yearSelector: STRING_PREFIX_DATEPICKER + 'year--selector',
+
+ month: STRING_PREFIX_DATEPICKER + 'month',
+ monthWrap: STRING_PREFIX_DATEPICKER + 'month--wrap',
+ monthSelector: STRING_PREFIX_DATEPICKER + 'month--selector',
+ monthNav: STRING_PREFIX_DATEPICKER + 'month--nav',
+ monthPrev: STRING_PREFIX_DATEPICKER + 'month--prev',
+ monthNext: STRING_PREFIX_DATEPICKER + 'month--next',
+
+ week: STRING_PREFIX_DATEPICKER + 'week',
+ weekdays: STRING_PREFIX_DATEPICKER + 'weekday',
+
+ day: STRING_PREFIX_DATEPICKER + 'day',
+ dayDisabled: STRING_PREFIX_DATEPICKER + 'day--disabled',
+ daySelected: STRING_PREFIX_DATEPICKER + 'day--selected',
+ dayHighlighted: STRING_PREFIX_DATEPICKER + 'day--highlighted',
+ dayToday: STRING_PREFIX_DATEPICKER + 'day--today',
+ dayInfocus: STRING_PREFIX_DATEPICKER + 'day--infocus',
+ dayOutfocus: STRING_PREFIX_DATEPICKER + 'day--outfocus'
+ }
+ } //$.fn.pickadate.defaults
+
+
+
+})( jQuery, window, document );
+
+
+
+
+
+