diff --git a/javascripts/application.js b/javascripts/application.js index d34bb4c..6b43945 100644 --- a/javascripts/application.js +++ b/javascripts/application.js @@ -164,6 +164,7 @@ do{e=a.indexOf(c.charAt(l++));g=a.indexOf(c.charAt(l++));i=a.indexOf(c.charAt(l+ if ( isNaN(months) ) { throw new UtilsError( "addMonth requires an integer" ); } + // TODO: try to remove this if if ( ! this.isDate(date) ) { date = this.rightNow; } @@ -187,6 +188,19 @@ do{e=a.indexOf(c.charAt(l++));g=a.indexOf(c.charAt(l++));i=a.indexOf(c.charAt(l+ isDate: function( date ) { return typeof date === "object" && date instanceof Date; }, + validateDateIsInAcceptableRange: function( date ) { + minDate = new Date( 1806, 5, 30); // Andrew Jackson kills a man in a duel after the man had accused Jackson's wife of bigamy + maxDate = new Date( 2220, 1, 1); // Date that 'mind uploading' is perfected and used extenisvely in global rewilding efforts + + compareOne = (Utils.dateComparator( date, minDate ) >= 0); + compareTwo = (Utils.dateComparator( date, minDate ) >= 0); + + if (compareOne && compareTwo) { + return true; + } else { + return false; + } + }, dateTimeComparator: function( firstDate, secondDate ) { if ( ! this.isDate(firstDate) || ! this.isDate(secondDate) ) { throw new DayError( "Date comparator failed -- invalid date object" ); @@ -253,6 +267,21 @@ do{e=a.indexOf(c.charAt(l++));g=a.indexOf(c.charAt(l++));i=a.indexOf(c.charAt(l+ } return dateArray; }, + hashKeyByMonthDiff: function( date, monthDiff ) { + return Utils.generateMonthHashKey( + Utils.addMonth( date, monthDiff ) + ); + }, + generateMonthHashKey: function( date ) { + if (Utils.isDate( date )) { + var year = date.getFullYear(); + var month = date.getMonth(); + } else { + var year = date.year; + var month = date.month; + } + return (year.toString() + Utils.pad( month, 2, 0)).toString(); + }, toUTCArray: function( date, params ) { var dateArray = [ date.getUTCFullYear(), @@ -281,8 +310,7 @@ do{e=a.indexOf(c.charAt(l++));g=a.indexOf(c.charAt(l++));i=a.indexOf(c.charAt(l+ window.App = window.App || {}; window.App.Utils = Utils; -})(); -$(function(){ +})();$(function(){ var confs = { prefix: "calendeer", classes: { @@ -329,11 +357,11 @@ $(function(){ } else if ( typeof month === "string" && ~$.inArray(month.toLowerCase(), months) ) { this.month = $.inArray(month.toLowerCase(), months); } else { - throw new CalendarError( "Invalid month in constructor" ); + throw new CalendarError( "Invalid month in constructor" + month); } this.name = this._names.en[ this.month ]; this.daysInMonth = (function(){ - month = self.month === 11 ? 0 : self.month + 1; + month = (self.month + 1) % 11; year = self.month === 11 ? self.year + 1 : self.year; return ( new Date( year, month, 0 ) ).getDate(); })(); @@ -426,16 +454,18 @@ $(function(){ this.el = e.calendeer; $(".calendeers").append( e.calendeer ); - }, - attach: function( $el, sibling ) { + attach: function( $el, type ) { if ( ! this.el ) { return this; } - if ( sibling ) { - this.el.insertAfter( $el ); - } else { - this.el.prependTo( $el ); + var conditions = { + sibling: this.el.insertAfter, + prepend: this.el.prependTo, + append: this.el.appendTo + }; + if (conditions[type]) { + conditions[type].call( this.el, $el); } return this; }, @@ -535,9 +565,6 @@ $(function(){ "data-date": day, "data": { "calendeer": this } } ); - if ( self.isPast() ) { - self.el.addClass( confs.classes.past ); - } if ( self.isToday() ) { self.el.addClass( confs.classes.today ); } @@ -549,9 +576,6 @@ $(function(){ isToday: function() { return this.props.today; }, _setState: function( startDate, endDate ) { var flag; - if ( this.isPast() ) { - return this; - } if ( Utils.isDate(startDate) || Utils.isDate(endDate) ) { flag = Utils.rangeComparator( this.dateObject, startDate, endDate ); @@ -608,7 +632,6 @@ $(function(){ date = rightNow; this.el = $( "
", {"class": "calendeers"} ); this.show( date ); - this.toggleFocused( "start" ); this.setupHandlers(); this.handleOptions( this.options ); var noon = new Date(); @@ -618,6 +641,9 @@ $(function(){ noon.setMilliseconds(0); this.setTime("start", noon, false ); this.setTime("end", noon, false ); + this.preload(); + this.toggleFocused( "start" ); + this.removeInputHighlighting(); }, get: function( type ) { if ( Utils.isDate( this.dates[type] ) && @@ -626,7 +652,16 @@ $(function(){ } return; }, - set: function( type, date ) {}, + set: function( type, date ) { + if ( Utils.isDate(date) ) { + this.setDate(type, date); + if ( this.options.timeSupport ) { this.setTime(type, date); } + } else if( typeof date === "number") { + date = new Date( date ); + this.setDate(type, date); + if ( this.options.timeSupport ) { this.setTime(type, date); } + } + }, handleOptions: function( options ) { var self = this, actions = { @@ -667,9 +702,25 @@ $(function(){ self.setDate( self.focused, date ); } ); + + var that = this; + this.el.on( "mouseenter", function( event ) { + var clazzes = that.el.attr("class"); + + if (clazzes.indexOf( "start-focus" ) !== -1) { + $( that.options.startInput ).addClass( "calendeer-focused-input-start" ); + } else { + $( that.options.endInput ).addClass( "calendeer-focused-input-end" ); + } + } ); + this.el.on( "mouseleave", function( event ) { + $( that.options.startInput ).removeClass( "calendeer-focused-input-start" ); + $( that.options.endInput ).removeClass( "calendeer-focused-input-end" ); + } ); + this.el.on( "mouseenter", ".calendeer-day", function( event ) { var data = $( this ).data( "calendeer" ); - if ( ! data.isPast() ) data.el.addClass( "hovered" ); + data.el.addClass( "hovered" ); } ); this.el.on( "mouseleave", ".calendeer-day", function( event ) { $( this ).data( "calendeer" ).el.removeClass( "hovered" ); @@ -680,6 +731,24 @@ $(function(){ e.preventDefault(); } ); }, + preload: function() { + var $input, existingDate; + if ( this.options.startInput ) { + $input = $( this.options.startInput ); + existingDate = Date.create( $input.val() ); + if ( existingDate.isValid() ) { + this.set( "start", Date.parse(existingDate) ); + } + } + if ( this.options.endInput ) { + $input = $( this.options.endInput ); + existingDate = Date.parse( $input.val() ); + if (! isNaN(existingDate) ) { + existingDate = new Date( existingDate ); + this.set( "end", existingDate ); + } + } + }, inputHandler: function( event ) { var date, isValid, data = event.data, @@ -697,10 +766,10 @@ $(function(){ date = new Date( isValid ); } + isValid = isValid && Utils.validateDateIsInAcceptableRange( date ); + if ( isValid ) { data.scope.setDate( data.type, Utils.trimDate(date), true ); - } else { - data.scope.clearDates( data.type, true ); } }, inputTimeHandler: function( event ) { @@ -737,6 +806,7 @@ $(function(){ $input.val( date ); } } ); + $input.on( "keyup.calendeer." + type, { type: type, scope: this }, this.inputTimeHandler ); $input.on( "focus.calendeer." + type, { type: type, scope: this }, $.proxy( function(e) { this.toggleTimeFocused(e.data.type); @@ -752,67 +822,141 @@ $(function(){ this.el.bind( "setDate", { type: type }, function( e, type, date, suppress ) { if ( type === e.data.type && ! suppress ) { if ( useSugar && Utils.isDate(date) && date.isValid() ) { - date = date.short(); + date = date.format( "{Month} {date}, {year}" ); } $input.val( date ); } } ); + $input.on( "keyup.calendeer." + type, { type: type, scope: this }, this.inputHandler ); $input.on( "focus.calendeer." + type, { type: type, scope: this }, $.proxy( function(e) { - this.toggleFocused(e.data.type); + this.toggleFocused( e.data.type ); this.show( e.data.type ); }, this ) ); }, attach: function( $el ) { if ( this.el ) { this.el.appendTo( $el ); + this.clearEventQueue(); } return this; }, - getCalendar: function( date ) { - var diff; - if ( ! App.Utils.isDate(date) && typeof date === "number" ) { - diff = date; - date = App.Utils.addMonth( rightNow, diff ); + clearEventQueue: function() { + if ( this._eventQueue ) { + var currentEvent; + while ( currentEvent = this._eventQueue.shift() ) { + this.emitEvent( currentEvent[0], currentEvent[1] ); + } + delete this._eventQueue; + } + return this; + }, + emitEvent: function( eventType, args ) { + if ( this.el.parent().length ) { + this.emitEvent = function( _eventType, _args ) { + this.el.trigger( _eventType, _args ); + }; + this.emitEvent( eventType, args ); } else { - diff = App.Utils.monthDiff( rightNow, date ); + this._eventQueue = this._eventQueue || []; + this._eventQueue.push( [eventType, args] ); } - if ( diff >= 0 && - (this.options.maxCalendars === 0 || diff < this.options.maxCalendars) ) { - if ( this.Calendars[diff] ) { - return this.Calendars[ diff ]; - } else { - var calendar = this.Calendars[ diff ] = new App.Calendar(date); - calendar.attach.apply( calendar, this.attachmentPoint(diff) ); - return calendar; - } + }, + getCalendar: function( date ) { + var hashKey = ""; + if ( ! Utils.isDate(date) && typeof date === "number" ) { + date = Utils.addMonth( rightNow, date ); + hashKey = Utils.generateMonthHashKey( date ); + } else if( typeof date === "string" ) { + hashKey = date; + } + + var hashEntry = this.Calendars[ hashKey ]; + + // check if element exists in hash already, otherwise + // create it in the key value store + if ( hashEntry ) { + return hashEntry; + } else { + // create and insert into hash + hashYear = parseInt( hashKey.substring(0, 4), 10); + hashMonth = parseInt( hashKey.substring(4) , 10); + var calendar = this.Calendars[ hashKey ] = new App.Calendar(hashMonth, hashYear); + // insert into DOM + calendar.attach.apply(calendar, this.attachmentPoint( calendar.dateObject )); + return calendar; } throw new Error( "get fail" ); }, - attachmentPoint: function( diff ) { + attachmentPoint: function( date ) { var attachment; - while( diff-- && attachment === undefined ) { - attachment = this.Calendars[diff]; + var firstDOMMonth = this.el.children(":first"); + + // if no DOM element exists, we can just attach as child of calendeers div + if ( firstDOMMonth.length === 0 ){ + return [ this.el , "prepend" ]; } - if ( attachment === undefined ) { - return [ this.el ]; - } else { - return [ attachment.el, true ]; + + // if this month is before the first DOM element, insert before that element + var dateComparison = Utils.dateComparator( + date, firstDOMMonth.data('calendeer').dateObject + ); + if (dateComparison < 0) { + return [ this.el, "prepend" ]; + } else if ( dateComparison === 0) { + // don't attach; this is handled by Calendar.attach + return undefined; } + + var lastDOMMonth = this.el.children(":last"); + + dateComparison = Utils.dateComparator( + date, lastDOMMonth.data('calendeer').dateObject + ); + if ( dateComparison === 0) { + // this is a special case, because the element we are trying to place, + // when it is created, is placed at the end of the month elements. So + // if the elements are for the same month, we actually want to use the + // one before this element because it is the actual last month in the DOM + lastDOMMonth = lastDOMMonth.prev(); + } + + // if this month is after the last DOM element, insert after that element + dateComparison = Utils.dateComparator( + date, lastDOMMonth.data('calendeer').dateObject + ); + if (dateComparison > 0) { + return [ this.el , "append" ]; + } else if ( dateComparison === 0 ) { + // don't attach; this is handled by Calendar.attach + return undefined; + } + + // otherwise, find most recent month before the current month, + // and use that as an attachment point + while( attachment === undefined ) { + var currKey = Utils.hashKeyByMonthDiff( date, -1 ); + date = Utils.addMonth( date, -1 ); + attachment = this.Calendars[ currKey ] + } + + return [ attachment.el, "sibling" ]; }, toggleFocused: function( focused ) { if ( focused !== "start" && focused !== "end" ) { focused = this.focused === "start" ? "end" : "start"; } if ( focused === "end" ) { - $( this.options.endInput ).addClass( "calendeer-focused-input" ); $( this.options.startInput ).removeClass( "calendeer-focused-input" ); + $( this.options.endInput ).addClass( "calendeer-focused-input" ); + this.focused = "end"; this.el.addClass( "end-focus" ); this.el.removeClass( "start-focus" ); } else { - $( this.options.startInput ).addClass( "calendeer-focused-input" ); - $( this.options.endInput ).removeClass( "calendeer-focused-input" ); + $( this.options.startInput ).addClass( "calendeer-focused-input" ); + $( this.options.endInput ).removeClass( "calendeer-focused-input" ); + this.focused = "start"; this.el.addClass( "start-focus" ); this.el.removeClass( "end-focus" ); @@ -836,6 +980,23 @@ $(function(){ this.el.removeClass( "end-time-focus" ); } }, + toggleInputHighlighting: function( focused ) { + if ( focused !== "start" && focused !== "end" ) { + focused = this.focused === "start" ? "end" : "start"; + } + var notFocused = focused === "start" ? "end" : "start"; + + $( this.options[ focused + "Input" ] ).addClass( + "calendeer-focused-input-" + focused + ); + $( this.options[ notFocused + "Input" ] ).removeClass( + "calendeer-focused-input-" + notFocused + ); + }, + removeInputHighlighting: function() { + $( this.options.startInput ).removeClass( "calendeer-focused-input-start" ); + $( this.options.endInput ).removeClass( "calendeer-focused-input-end" ); + }, show: function( date, index ) { if ( typeof index !== "number" || index < 0 || @@ -854,34 +1015,20 @@ $(function(){ return this; } } - if ( App.Utils.isDate(date) ) { - var diff = App.Utils.monthDiff( rightNow, date ); - if ( diff < 0 || (this.options.maxCalendars !== 0 && diff >= this.options.maxCalendars) ) { - return this; - } else { - if ( diff < index ) { - index = diff; - } else { - if ( this.options.maxCalendars ) { - if ( this.options.numberOfCalendars > this.options.maxCalendars - diff ) { - index = this.options.numberOfCalendars - this.options.maxCalendars + diff; - } - } - } - } + if ( Utils.isDate(date) ) { this.hide(); var numCalendars = -1, showIndex; this.visibleIndexes = []; while( ++numCalendars < this.options.numberOfCalendars ) { - showIndex = diff - index + numCalendars; + showIndex = Utils.hashKeyByMonthDiff( date, numCalendars - index ); this.visibleIndexes.push( showIndex ); calendar = this.getCalendar( showIndex ); calendar.show(); - calendar.togglePreviousButton( numCalendars === 0 && - showIndex !== 0 ); + calendar.togglePreviousButton( + numCalendars === 0 + ); calendar.toggleNextButton( - numCalendars === this.options.numberOfCalendars - 1 && - showIndex !== this.options.maxCalendars - 1 + numCalendars === this.options.numberOfCalendars - 1 ); } this.drawState( this.dates.start, this.dates.end ); @@ -901,23 +1048,6 @@ $(function(){ } return this; }, - clearDates: function( type, fromHandler ) { - if ( typeof type === "string" && this.dates[type] !== undefined ) { - type = type.toLowerCase(); - this.dates[type] = null; - if ( type !== "start" && type !== "end" ) { - delete this.dates[type]; - } else { - this.el.removeClass( type + "-date" ); - this.el.trigger( "setDate", [type, null, fromHandler] ); - this.drawState( this.dates.start, this.dates.end ); - } - } else { - this.dates.start = null; - this.dates.end = null; - this.drawState( this.dates.start, this.dates.end ); - } - }, clearTimes: function( type, fromHandler ) { if ( typeof type === "string" && this.dates[type] !== undefined ) { type = type.toLowerCase(); @@ -926,7 +1056,7 @@ $(function(){ delete this.times[type]; } else { this.el.removeClass( type + "-time" ); - this.el.trigger( "setTime", [type, rightNow, fromHandler] ); + this.emitEvent( "setTime", [type, rightNow, fromHandler] ); } } else { this.dates.start = null; @@ -934,49 +1064,32 @@ $(function(){ this.drawState( this.dates.start, this.dates.end ); } }, - validateDate: function( date, type ) { - var dateConditions = Utils.isDate( date ), - futureConditions = ( this.options.maxCalendars !== 0 ? - Utils.monthDiff( this.dates.today, date ) < this.options.maxCalendars : - true ), - pastConditions = Utils.dateTimeComparator( date, this.dates.today ) + 1; - - return !! ( dateConditions && - futureConditions && - pastConditions ); - }, setDate: function( type, date, fromHandler ) { if ( typeof type !== "string" || ( ! Utils.isDate(date) && date != undefined ) ) { throw new Error( "setDate invalid arguments" ); } type = type.toLowerCase(); - if ( ! this.validateDate( date, type ) ) return this; this.dates[ type ] = date; this.el.addClass( type + "-date" ); if ( type === "start" ) { - if ( this.dates.end && Utils.dateComparator(date,this.dates.end)===1 ) { - this.clearDates( "end" ); - } if ( Utils.isDate(date) && ! this.isVisible(date) ) { this.show( date, 0 ); } this.drawState( this.dates.start, this.dates.end ); } else if ( type === "end" ) { - if ( this.dates.start && Utils.dateComparator(date,this.dates.start)===-1 ) { - this.clearDates( "start" ); - } if ( Utils.isDate(date) && ! this.isVisible(date) ) { this.show( date, this.options.numberOfCalendars - 1 ); } this.drawState( this.dates.start, this.dates.end ); } - this.el.trigger( "setDate", [type, date, fromHandler] ); + this.emitEvent( "setDate", [type, date, fromHandler] ); var dateTime = this.get( type ); if ( dateTime != undefined ) { - this.el.trigger( "setDateTime", [type, Utils.toISO(dateTime), dateTime] ); + this.emitEvent( "setDateTime", [type, Utils.toISO(dateTime), dateTime] ); } if ( ! fromHandler ) { + this.toggleInputHighlighting(); this.toggleFocused(); } return this; @@ -987,13 +1100,12 @@ $(function(){ throw new Error( "setTime invalid arguments" ); } type = type.toLowerCase(); - // if ( ! this.validateDate( date, type ) ) return this; this.times[ type ] = date; this.el.addClass( type + "-time" ); - this.el.trigger( "setTime", [type, date, fromHandler] ); + this.emitEvent( "setTime", [type, date, fromHandler] ); var dateTime = this.get( type ); if ( dateTime != undefined ) { - this.el.trigger( "setDateTime", [type, Utils.toISO(dateTime), dateTime] ); + this.emitEvent( "setDateTime", [type, Utils.toISO(dateTime), dateTime] ); } if ( ! fromHandler ) { this.toggleTimeFocused(); @@ -1005,7 +1117,9 @@ $(function(){ if ( isNaN(steps) ) { steps = 1; } - this.show( this.getCalendar( this.visibleIndexes[0] + steps ).dateObject, 0 ); + var thisDate = this.Calendars[ this.visibleIndexes[0] ].dateObject; + var nextMonthHashKey = Utils.hashKeyByMonthDiff( thisDate, steps ); + this.show( this.getCalendar( nextMonthHashKey ).dateObject, 0 ); }, nextPage: function() { this.next( this.options.numberOfCalendars ); @@ -1016,14 +1130,20 @@ $(function(){ steps = 1; } var last = this.visibleIndexes.length - 1; - this.show( this.getCalendar( this.visibleIndexes[ last ] - steps ).dateObject, last ); + var thisDate = this.Calendars[ this.visibleIndexes[last] ].dateObject; + var prevMonthHashKey = Utils.hashKeyByMonthDiff( thisDate, -steps ); + this.show( this.getCalendar( prevMonthHashKey ).dateObject, last ); }, previousPage: function() { this.previous( this.options.numberOfCalendars ); }, isVisible: function( date ) { - var diff = Utils.monthDiff( rightNow, date ); - return !!( ~$.inArray(diff, this.visibleIndexes) ); + // if date is a date object, must convert it to a hash key + if ( Utils.isDate(date) ) { + date = Utils.generateMonthHashKey( date ); + } + // otherwise just use the given hash key, assuming it is a string + return !!( ~$.inArray(date, this.visibleIndexes) ); }, callOnVisibles: function() { if ( ! arguments.length ) { @@ -1056,7 +1176,6 @@ $(function(){ endInput: null, eventDelegate: null, numberOfCalendars: 2, - maxCalendars: 0, timeSupport: true, useSugar: typeof (new Date()).isValid === "function" }; @@ -1064,66 +1183,3 @@ $(function(){ window.App = window.App || {}; window.App.Calendeer = Calendeer; }); -$(function(){ - - var init = function( startID, endID ) { - var $startInput = $("#" + startID), - $endInput = $("#" + endID), - $startTimeInput, - $endTimeInput, - startName = $startInput.attr( "name" ), - endName = $endInput.attr( "name" ), - $startHidden = $( '' ).attr( { - name: startName, - id: startID - } ), - $endHidden = $( '' ).attr( { - name: endName, - id: endID - } ); - - $startInput.attr( "id", startID + "_calendeer" ).removeAttr( "name" ); - $endInput.attr( "id", endID + "_calendeer" ).removeAttr( "name" ); - $startTimeInput = $startInput.clone().attr( "id", startID + "_time_calendeer" ); - $endTimeInput = $endInput.clone().attr( "id", endID + "_time_calendeer" ); - - $startInput.after( $startTimeInput ).after( $startHidden ); - $endInput.after( $endTimeInput ).after( $endHidden ); - - var calendeer = new App.Calendeer( { - startInput: $startInput, - startTimeInput: $startTimeInput, - endInput: $endInput, - endTimeInput: $endTimeInput - } ); - - $(document).bind( "setDateTime", function( e, type, isoDate, dateObject ) { - if ( type === "start" ) { - $startHidden.val( isoDate ); - } else if ( type === "end" ) { - $endHidden.val( isoDate ); - } - } ); - - return function() { - calendeer.el.insertAfter( $endTimeInput ); - }; - - }; - - var callback = function() {}; - if ( $('#offerStartDate_ID').length ) { - // Page 1 - callback = init( "offerStartDate_ID", "offerEndDate_ID" ); - } else if ( $('#startAdvTargetDate_ID').length ) { - // Page 3 - callback = init( "startAdvTargetDate_ID", "endAdvTargetDate_ID" ); - } - - $( "", { - href: "//paypal-moonwalk.s3.amazonaws.com/stylesheets/calendeer.css", - rel: "stylesheet", - type: "text/css" - } ).appendTo( $("head") ).bind( "load", callback ); - -}); \ No newline at end of file diff --git a/javascripts/calendeer.js b/javascripts/calendeer.js index 4c707d3..f5a3a73 100644 --- a/javascripts/calendeer.js +++ b/javascripts/calendeer.js @@ -27,7 +27,6 @@ $(function(){ date = rightNow; this.el = $( "", {"class": "calendeers"} ); this.show( date ); - this.toggleFocused( "start" ); this.setupHandlers(); this.handleOptions( this.options ); var noon = new Date(); @@ -38,6 +37,8 @@ $(function(){ this.setTime("start", noon, false ); this.setTime("end", noon, false ); this.preload(); + this.toggleFocused( "start" ); + this.removeInputHighlighting(); }, get: function( type ) { if ( Utils.isDate( this.dates[type] ) && @@ -47,7 +48,11 @@ $(function(){ return; }, set: function( type, date ) { - if ( Utils.isDate(date) && this.validateDate(date, type) ) { + if ( Utils.isDate(date) ) { + this.setDate(type, date); + if ( this.options.timeSupport ) { this.setTime(type, date); } + } else if( typeof date === "number") { + date = new Date( date ); this.setDate(type, date); if ( this.options.timeSupport ) { this.setTime(type, date); } } @@ -92,9 +97,25 @@ $(function(){ self.setDate( self.focused, date ); } ); + + var that = this; + this.el.on( "mouseenter", function( event ) { + var clazzes = that.el.attr("class"); + + if (clazzes.indexOf( "start-focus" ) !== -1) { + $( that.options.startInput ).addClass( "calendeer-focused-input-start" ); + } else { + $( that.options.endInput ).addClass( "calendeer-focused-input-end" ); + } + } ); + this.el.on( "mouseleave", function( event ) { + $( that.options.startInput ).removeClass( "calendeer-focused-input-start" ); + $( that.options.endInput ).removeClass( "calendeer-focused-input-end" ); + } ); + this.el.on( "mouseenter", ".calendeer-day", function( event ) { var data = $( this ).data( "calendeer" ); - if ( ! data.isPast() ) data.el.addClass( "hovered" ); + data.el.addClass( "hovered" ); } ); this.el.on( "mouseleave", ".calendeer-day", function( event ) { $( this ).data( "calendeer" ).el.removeClass( "hovered" ); @@ -109,10 +130,9 @@ $(function(){ var $input, existingDate; if ( this.options.startInput ) { $input = $( this.options.startInput ); - existingDate = Date.parse( $input.val() ); - if (! isNaN(existingDate) ) { - existingDate = new Date( existingDate ); - this.set( "start", existingDate ); + existingDate = Date.create( $input.val() ); + if ( existingDate.isValid() ) { + this.set( "start", Date.parse(existingDate) ); } } if ( this.options.endInput ) { @@ -141,10 +161,10 @@ $(function(){ date = new Date( isValid ); } + isValid = isValid && Utils.validateDateIsInAcceptableRange( date ); + if ( isValid ) { data.scope.setDate( data.type, Utils.trimDate(date), true ); - } else { - data.scope.clearDates( data.type, true ); } }, inputTimeHandler: function( event ) { @@ -181,6 +201,7 @@ $(function(){ $input.val( date ); } } ); + $input.on( "keyup.calendeer." + type, { type: type, scope: this }, this.inputTimeHandler ); $input.on( "focus.calendeer." + type, { type: type, scope: this }, $.proxy( function(e) { this.toggleTimeFocused(e.data.type); @@ -204,7 +225,7 @@ $(function(){ $input.on( "keyup.calendeer." + type, { type: type, scope: this }, this.inputHandler ); $input.on( "focus.calendeer." + type, { type: type, scope: this }, $.proxy( function(e) { - this.toggleFocused(e.data.type); + this.toggleFocused( e.data.type ); this.show( e.data.type ); }, this ) ); }, @@ -237,49 +258,100 @@ $(function(){ } }, getCalendar: function( date ) { - var diff; - if ( ! App.Utils.isDate(date) && typeof date === "number" ) { - diff = date; - date = App.Utils.addMonth( rightNow, diff ); - } else { - diff = App.Utils.monthDiff( rightNow, date ); + var hashKey = ""; + if ( ! Utils.isDate(date) && typeof date === "number" ) { + date = Utils.addMonth( rightNow, date ); + hashKey = Utils.generateMonthHashKey( date ); + } else if( typeof date === "string" ) { + hashKey = date; } - if ( diff >= 0 && - (this.options.maxCalendars === 0 || diff < this.options.maxCalendars) ) { - if ( this.Calendars[diff] ) { - return this.Calendars[ diff ]; - } else { - var calendar = this.Calendars[ diff ] = new App.Calendar(date); - calendar.attach.apply( calendar, this.attachmentPoint(diff) ); - return calendar; - } + + var hashEntry = this.Calendars[ hashKey ]; + + // check if element exists in hash already, otherwise + // create it in the key value store + if ( hashEntry ) { + return hashEntry; + } else { + // create and insert into hash + hashYear = parseInt( hashKey.substring(0, 4), 10); + hashMonth = parseInt( hashKey.substring(4) , 10); + var calendar = this.Calendars[ hashKey ] = new App.Calendar(hashMonth, hashYear); + // insert into DOM + calendar.attach.apply(calendar, this.attachmentPoint( calendar.dateObject )); + return calendar; } throw new Error( "get fail" ); }, - attachmentPoint: function( diff ) { + attachmentPoint: function( date ) { var attachment; - while( diff-- && attachment === undefined ) { - attachment = this.Calendars[diff]; + var firstDOMMonth = this.el.children(":first"); + + // if no DOM element exists, we can just attach as child of calendeers div + if ( firstDOMMonth.length === 0 ){ + return [ this.el , "prepend" ]; } - if ( attachment === undefined ) { - return [ this.el ]; - } else { - return [ attachment.el, true ]; + + // if this month is before the first DOM element, insert before that element + var dateComparison = Utils.dateComparator( + date, firstDOMMonth.data('calendeer').dateObject + ); + if (dateComparison < 0) { + return [ this.el, "prepend" ]; + } else if ( dateComparison === 0) { + // don't attach; this is handled by Calendar.attach + return undefined; + } + + var lastDOMMonth = this.el.children(":last"); + + dateComparison = Utils.dateComparator( + date, lastDOMMonth.data('calendeer').dateObject + ); + if ( dateComparison === 0) { + // this is a special case, because the element we are trying to place, + // when it is created, is placed at the end of the month elements. So + // if the elements are for the same month, we actually want to use the + // one before this element because it is the actual last month in the DOM + lastDOMMonth = lastDOMMonth.prev(); + } + + // if this month is after the last DOM element, insert after that element + dateComparison = Utils.dateComparator( + date, lastDOMMonth.data('calendeer').dateObject + ); + if (dateComparison > 0) { + return [ this.el , "append" ]; + } else if ( dateComparison === 0 ) { + // don't attach; this is handled by Calendar.attach + return undefined; + } + + // otherwise, find most recent month before the current month, + // and use that as an attachment point + while( attachment === undefined ) { + var currKey = Utils.hashKeyByMonthDiff( date, -1 ); + date = Utils.addMonth( date, -1 ); + attachment = this.Calendars[ currKey ] } + + return [ attachment.el, "sibling" ]; }, toggleFocused: function( focused ) { if ( focused !== "start" && focused !== "end" ) { focused = this.focused === "start" ? "end" : "start"; } if ( focused === "end" ) { - $( this.options.endInput ).addClass( "calendeer-focused-input" ); $( this.options.startInput ).removeClass( "calendeer-focused-input" ); + $( this.options.endInput ).addClass( "calendeer-focused-input" ); + this.focused = "end"; this.el.addClass( "end-focus" ); this.el.removeClass( "start-focus" ); } else { - $( this.options.startInput ).addClass( "calendeer-focused-input" ); - $( this.options.endInput ).removeClass( "calendeer-focused-input" ); + $( this.options.startInput ).addClass( "calendeer-focused-input" ); + $( this.options.endInput ).removeClass( "calendeer-focused-input" ); + this.focused = "start"; this.el.addClass( "start-focus" ); this.el.removeClass( "end-focus" ); @@ -303,6 +375,23 @@ $(function(){ this.el.removeClass( "end-time-focus" ); } }, + toggleInputHighlighting: function( focused ) { + if ( focused !== "start" && focused !== "end" ) { + focused = this.focused === "start" ? "end" : "start"; + } + var notFocused = focused === "start" ? "end" : "start"; + + $( this.options[ focused + "Input" ] ).addClass( + "calendeer-focused-input-" + focused + ); + $( this.options[ notFocused + "Input" ] ).removeClass( + "calendeer-focused-input-" + notFocused + ); + }, + removeInputHighlighting: function() { + $( this.options.startInput ).removeClass( "calendeer-focused-input-start" ); + $( this.options.endInput ).removeClass( "calendeer-focused-input-end" ); + }, show: function( date, index ) { if ( typeof index !== "number" || index < 0 || @@ -321,34 +410,20 @@ $(function(){ return this; } } - if ( App.Utils.isDate(date) ) { - var diff = App.Utils.monthDiff( rightNow, date ); - if ( diff < 0 || (this.options.maxCalendars !== 0 && diff >= this.options.maxCalendars) ) { - return this; - } else { - if ( diff < index ) { - index = diff; - } else { - if ( this.options.maxCalendars ) { - if ( this.options.numberOfCalendars > this.options.maxCalendars - diff ) { - index = this.options.numberOfCalendars - this.options.maxCalendars + diff; - } - } - } - } + if ( Utils.isDate(date) ) { this.hide(); var numCalendars = -1, showIndex; this.visibleIndexes = []; while( ++numCalendars < this.options.numberOfCalendars ) { - showIndex = diff - index + numCalendars; + showIndex = Utils.hashKeyByMonthDiff( date, numCalendars - index ); this.visibleIndexes.push( showIndex ); calendar = this.getCalendar( showIndex ); calendar.show(); - calendar.togglePreviousButton( numCalendars === 0 && - showIndex !== 0 ); + calendar.togglePreviousButton( + numCalendars === 0 + ); calendar.toggleNextButton( - numCalendars === this.options.numberOfCalendars - 1 && - showIndex !== this.options.maxCalendars - 1 + numCalendars === this.options.numberOfCalendars - 1 ); } this.drawState( this.dates.start, this.dates.end ); @@ -368,23 +443,6 @@ $(function(){ } return this; }, - clearDates: function( type, fromHandler ) { - if ( typeof type === "string" && this.dates[type] !== undefined ) { - type = type.toLowerCase(); - this.dates[type] = null; - if ( type !== "start" && type !== "end" ) { - delete this.dates[type]; - } else { - this.el.removeClass( type + "-date" ); - this.emitEvent( "setDate", [type, null, fromHandler] ); - this.drawState( this.dates.start, this.dates.end ); - } - } else { - this.dates.start = null; - this.dates.end = null; - this.drawState( this.dates.start, this.dates.end ); - } - }, clearTimes: function( type, fromHandler ) { if ( typeof type === "string" && this.dates[type] !== undefined ) { type = type.toLowerCase(); @@ -401,38 +459,20 @@ $(function(){ this.drawState( this.dates.start, this.dates.end ); } }, - validateDate: function( date, type ) { - var dateConditions = Utils.isDate( date ), - futureConditions = ( this.options.maxCalendars !== 0 ? - Utils.monthDiff( this.dates.today, date ) < this.options.maxCalendars : - true ), - pastConditions = Utils.dateTimeComparator( date, this.dates.today ) + 1; - - return !! ( dateConditions && - futureConditions && - pastConditions ); - }, setDate: function( type, date, fromHandler ) { if ( typeof type !== "string" || ( ! Utils.isDate(date) && date != undefined ) ) { throw new Error( "setDate invalid arguments" ); } type = type.toLowerCase(); - if ( ! this.validateDate( date, type ) ) return this; this.dates[ type ] = date; this.el.addClass( type + "-date" ); if ( type === "start" ) { - if ( this.dates.end && Utils.dateComparator(date,this.dates.end)===1 ) { - this.clearDates( "end" ); - } if ( Utils.isDate(date) && ! this.isVisible(date) ) { this.show( date, 0 ); } this.drawState( this.dates.start, this.dates.end ); } else if ( type === "end" ) { - if ( this.dates.start && Utils.dateComparator(date,this.dates.start)===-1 ) { - this.clearDates( "start" ); - } if ( Utils.isDate(date) && ! this.isVisible(date) ) { this.show( date, this.options.numberOfCalendars - 1 ); } @@ -444,6 +484,7 @@ $(function(){ this.emitEvent( "setDateTime", [type, Utils.toISO(dateTime), dateTime] ); } if ( ! fromHandler ) { + this.toggleInputHighlighting(); this.toggleFocused(); } return this; @@ -454,7 +495,6 @@ $(function(){ throw new Error( "setTime invalid arguments" ); } type = type.toLowerCase(); - // if ( ! this.validateDate( date, type ) ) return this; this.times[ type ] = date; this.el.addClass( type + "-time" ); this.emitEvent( "setTime", [type, date, fromHandler] ); @@ -472,7 +512,9 @@ $(function(){ if ( isNaN(steps) ) { steps = 1; } - this.show( this.getCalendar( this.visibleIndexes[0] + steps ).dateObject, 0 ); + var thisDate = this.Calendars[ this.visibleIndexes[0] ].dateObject; + var nextMonthHashKey = Utils.hashKeyByMonthDiff( thisDate, steps ); + this.show( this.getCalendar( nextMonthHashKey ).dateObject, 0 ); }, nextPage: function() { this.next( this.options.numberOfCalendars ); @@ -483,14 +525,20 @@ $(function(){ steps = 1; } var last = this.visibleIndexes.length - 1; - this.show( this.getCalendar( this.visibleIndexes[ last ] - steps ).dateObject, last ); + var thisDate = this.Calendars[ this.visibleIndexes[last] ].dateObject; + var prevMonthHashKey = Utils.hashKeyByMonthDiff( thisDate, -steps ); + this.show( this.getCalendar( prevMonthHashKey ).dateObject, last ); }, previousPage: function() { this.previous( this.options.numberOfCalendars ); }, isVisible: function( date ) { - var diff = Utils.monthDiff( rightNow, date ); - return !!( ~$.inArray(diff, this.visibleIndexes) ); + // if date is a date object, must convert it to a hash key + if ( Utils.isDate(date) ) { + date = Utils.generateMonthHashKey( date ); + } + // otherwise just use the given hash key, assuming it is a string + return !!( ~$.inArray(date, this.visibleIndexes) ); }, callOnVisibles: function() { if ( ! arguments.length ) { @@ -523,7 +571,6 @@ $(function(){ endInput: null, eventDelegate: null, numberOfCalendars: 2, - maxCalendars: 0, timeSupport: true, useSugar: typeof (new Date()).isValid === "function" }; diff --git a/javascripts/day.js b/javascripts/day.js index 295f7d5..1860770 100644 --- a/javascripts/day.js +++ b/javascripts/day.js @@ -56,9 +56,6 @@ $(function(){ "data-date": day, "data": { "calendeer": this } } ); - if ( self.isPast() ) { - self.el.addClass( confs.classes.past ); - } if ( self.isToday() ) { self.el.addClass( confs.classes.today ); } @@ -70,9 +67,6 @@ $(function(){ isToday: function() { return this.props.today; }, _setState: function( startDate, endDate ) { var flag; - if ( this.isPast() ) { - return this; - } if ( Utils.isDate(startDate) || Utils.isDate(endDate) ) { flag = Utils.rangeComparator( this.dateObject, startDate, endDate ); diff --git a/javascripts/jquery.calendeer.js b/javascripts/jquery.calendeer.js new file mode 100644 index 0000000..64eb1cf --- /dev/null +++ b/javascripts/jquery.calendeer.js @@ -0,0 +1,74 @@ +$(function() { + + $.fn.calendeerWidget = function( options ){ + return this.each( function(){ + var opts = $.extend( {}, options ); + init( "calendeer_start_input", "calendeer_end_input" ); + } ); + }; + + var init = function( startClass, endClass ){ + var $startInput = $("." + startClass), + $endInput = $("." + endClass), + $startTimeInput, + $endTimeInput, + startName = $startInput.attr( "name" ), + endName = $endInput.attr( "name" ), + $startHidden = $( '' ).attr( { + name: startName, + id: startClass + } ), + $endHidden = $( '' ).attr( { + name: endName, + id: endClass + } ); + + $startInput.attr( "class", startClass + "_calendeer" ).removeAttr( "name" ); + $endInput.attr( "class", endClass + "_calendeer" ).removeAttr( "name" ); + $startTimeInput = $startInput.clone().attr( "class", startClass + "_time_calendeer" ); + $endTimeInput = $endInput.clone().attr( "class", endClass + "_time_calendeer" ); + + $startInput.after( $startTimeInput ).after( $startHidden ); + $endInput.after( $endTimeInput ).after( $endHidden ); + + // pass inputs to Calendeer object + var calendeer = new App.Calendeer( { + startInput: $startInput, + startTimeInput: $startTimeInput, + endInput: $endInput, + endTimeInput: $endTimeInput + } ); + + // update the hidden input's time representation on every setDateTime call + $(document).bind( "setDateTime", function( e, type, isoDate, dateObject ) { + if ( type === "start" ) { + $startHidden.val( isoDate ); + } else if ( type === "end" ) { + $endHidden.val( isoDate ); + } + } ); + + calendeer.el.insertAfter( $endTimeInput ); + calendeer.clearEventQueue(); + + // hide Calendeer unless one of the four inputs is selected + $(document).delegate(':input', 'focus', function() { + var ae = document.activeElement; + var focusedClasses = ae.className; + var inputClasses = [ startClass + "_calendeer", + endClass + "_calendeer", + startClass + "_time_calendeer", + endClass + "_time_calendeer" ]; + var showCalendeer = false; + + $.each( inputClasses, function( i, cl ) { + showCalendeer = showCalendeer || ( focusedClasses.indexOf( cl ) !== -1 ); + }); + + showCalendeer ? calendeer.el.slideDown() : calendeer.el.slideUp(); + }); + + calendeer.el.hide(); + } + +}); diff --git a/javascripts/month.js b/javascripts/month.js index 1a43f24..59cb6b9 100644 --- a/javascripts/month.js +++ b/javascripts/month.js @@ -45,11 +45,11 @@ $(function(){ } else if ( typeof month === "string" && ~$.inArray(month.toLowerCase(), months) ) { this.month = $.inArray(month.toLowerCase(), months); } else { - throw new CalendarError( "Invalid month in constructor" ); + throw new CalendarError( "Invalid month in constructor" + month); } this.name = this._names.en[ this.month ]; this.daysInMonth = (function(){ - month = self.month === 11 ? 0 : self.month + 1; + month = (self.month + 1) % 11; year = self.month === 11 ? self.year + 1 : self.year; return ( new Date( year, month, 0 ) ).getDate(); })(); @@ -142,16 +142,18 @@ $(function(){ this.el = e.calendeer; $(".calendeers").append( e.calendeer ); - }, - attach: function( $el, sibling ) { + attach: function( $el, type ) { if ( ! this.el ) { return this; } - if ( sibling ) { - this.el.insertAfter( $el ); - } else { - this.el.prependTo( $el ); + var conditions = { + sibling: this.el.insertAfter, + prepend: this.el.prependTo, + append: this.el.appendTo + }; + if (conditions[type]) { + conditions[type].call( this.el, $el); } return this; }, diff --git a/javascripts/utils.js b/javascripts/utils.js index 7686e94..96fe293 100644 --- a/javascripts/utils.js +++ b/javascripts/utils.js @@ -54,6 +54,7 @@ if ( isNaN(months) ) { throw new UtilsError( "addMonth requires an integer" ); } + // TODO: try to remove this if if ( ! this.isDate(date) ) { date = this.rightNow; } @@ -77,6 +78,19 @@ isDate: function( date ) { return typeof date === "object" && date instanceof Date; }, + validateDateIsInAcceptableRange: function( date ) { + minDate = new Date( 1806, 5, 30); // Andrew Jackson kills a man in a duel after the man had accused Jackson's wife of bigamy + maxDate = new Date( 2220, 1, 1); // Date that 'mind uploading' is perfected and used extenisvely in global rewilding efforts + + compareOne = (Utils.dateComparator( date, minDate ) >= 0); + compareTwo = (Utils.dateComparator( date, minDate ) >= 0); + + if (compareOne && compareTwo) { + return true; + } else { + return false; + } + }, dateTimeComparator: function( firstDate, secondDate ) { if ( ! this.isDate(firstDate) || ! this.isDate(secondDate) ) { throw new DayError( "Date comparator failed -- invalid date object" ); @@ -143,6 +157,21 @@ } return dateArray; }, + hashKeyByMonthDiff: function( date, monthDiff ) { + return Utils.generateMonthHashKey( + Utils.addMonth( date, monthDiff ) + ); + }, + generateMonthHashKey: function( date ) { + if (Utils.isDate( date )) { + var year = date.getFullYear(); + var month = date.getMonth(); + } else { + var year = date.year; + var month = date.month; + } + return (year.toString() + Utils.pad( month, 2, 0)).toString(); + }, toUTCArray: function( date, params ) { var dateArray = [ date.getUTCFullYear(), @@ -171,4 +200,4 @@ window.App = window.App || {}; window.App.Utils = Utils; -})(); +})(); \ No newline at end of file diff --git a/stylesheets/calendeer.css b/stylesheets/calendeer.css index f932b23..e94f8fc 100644 --- a/stylesheets/calendeer.css +++ b/stylesheets/calendeer.css @@ -4,6 +4,8 @@ text-align: center; overflow: hidden; width: 410px; + margin-left: 180px; + margin-top: 15px; } .calendeer { @@ -77,21 +79,21 @@ .calendeer-day.start-date, .calendeers.start-focus .calendeer-day.hovered, .calendeers.start-focus.end-date .after-date.hovered { - background-color: #e39937; + background-color: #3bd521; color: white; } .calendeer-day.end-date, .calendeers.end-focus .calendeer-day.hovered, .calendeers.end-focus.start-date .before-date.hovered { - background-color: #3bd521; + background-color: #e39937; color: white; } .calendeer-day.start-end-date, .calendeers.start-focus .calendeer-day.end-date.hovered, .calendeers.end-focus .calendeer-day.start-date.hovered { - background: url(../images/start-end-date.gif) 50% 50% no-repeat; + background: url(data:image/gif;base64,R0lGODdhFwAQAIQBAOKZN+OZN+CaN9edNeGaN96bNtycNtGgNb6mMsijM8WkM7OqMZ6xLqKwL6avL4+3LJC3LIG8Kni/KWzEJ1nKJVPMJF7JJlLNJEnQI0PSIkrQIz7UITvVIeOZN+OZN+OZNywAAAAAFwAQAAAFUyAnjmRpUoqpqg4grDBXHUHwxiYE1AGBk5oEr+b7cSKCIfGXQSh5ANzE8ITCNovqMKqyDLRbFWMHtpIutHJ49CCrzRjhWxmVFOZPQAPA7/v/gAQhADs=) 50% 50% no-repeat; color: white; } @@ -104,4 +106,34 @@ .calendeers.start-focus.end-date .after-date { background-color: #dedede; color: gray; -} \ No newline at end of file +} + +.calendeer_start_input_calendeer { + min-width: auto !important; + width: 150px !important; + margin-right: 5px; +} + +.calendeer_end_input_calendeer { + min-width: auto !important; + width: 150px !important; + margin-right: 5px; +} + +.calendeer_start_input_time_calendeer { + min-width: auto !important; + width: 150px !important; +} + +.calendeer_end_input_time_calendeer { + min-width: auto !important; + width: 150px !important; +} + +.calendeer-focused-input-start { + border: 1px solid #3bd521 !important; +} + +.calendeer-focused-input-end { + border: 1px solid #e39937 !important; +}