diff options
Diffstat (limited to 'pickadate.js')
| -rwxr-xr-x | pickadate.js | 1576 |
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: '◀', + monthNext: '▶', + + // 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 ); + + + + + + |
