1/* 2 * ~ CLNDR v1.2.4 ~ 3 * ============================================== 4 * https://github.com/kylestetz/CLNDR 5 * ============================================== 6 * created by kyle stetz (github.com/kylestetz) 7 * &available under the MIT license 8 * http://opensource.org/licenses/mit-license.php 9 * ============================================== 10 * 11 * This is the fully-commented development version of CLNDR. 12 * For the production version, check out clndr.min.js 13 * at https://github.com/kylestetz/CLNDR 14 * 15 * This work is based on the 16 * jQuery lightweight plugin boilerplate 17 * Original author: @ajpiano 18 * Further changes, comments: @addyosmani 19 * Licensed under the MIT license 20 */ 21 22(function (factory) { 23 24 if (typeof define === 'function' && define.amd) { 25 26 // AMD. Register as an anonymous module. 27 define(['jquery', 'moment'], factory); 28 } else if (typeof exports === 'object') { 29 30 // Node/CommonJS 31 factory(require('jquery'), require('moment')); 32 } else { 33 34 // Browser globals 35 factory(jQuery, moment); 36 } 37 38}(function ($, moment) { 39 40 // This is the default calendar template. This can be overridden. 41 var clndrTemplate = "<div class='clndr-controls'>" + 42 "<div class='clndr-control-button'><span class='clndr-previous-button'>previous</span></div><div class='month'><%= month %> <%= year %></div><div class='clndr-control-button rightalign'><span class='clndr-next-button'>next</span></div>" + 43 "</div>" + 44 "<table class='clndr-table' border='0' cellspacing='0' cellpadding='0'>" + 45 "<thead>" + 46 "<tr class='header-days'>" + 47 "<% for(var i = 0; i < daysOfTheWeek.length; i++) { %>" + 48 "<td class='header-day'><%= daysOfTheWeek[i] %></td>" + 49 "<% } %>" + 50 "</tr>" + 51 "</thead>" + 52 "<tbody>" + 53 "<% for(var i = 0; i < numberOfRows; i++){ %>" + 54 "<tr>" + 55 "<% for(var j = 0; j < 7; j++){ %>" + 56 "<% var d = j + i * 7; %>" + 57 "<td class='<%= days[d].classes %>'><div class='day-contents'><%= days[d].day %>" + 58 "</div></td>" + 59 "<% } %>" + 60 "</tr>" + 61 "<% } %>" + 62 "</tbody>" + 63 "</table>"; 64 65 var pluginName = 'clndr'; 66 67 var defaults = { 68 template: clndrTemplate, 69 weekOffset: 0, 70 startWithMonth: null, 71 clickEvents: { 72 click: null, 73 nextMonth: null, 74 previousMonth: null, 75 nextYear: null, 76 previousYear: null, 77 today: null, 78 onMonthChange: null, 79 onYearChange: null 80 }, 81 targets: { 82 nextButton: 'clndr-next-button', 83 previousButton: 'clndr-previous-button', 84 nextYearButton: 'clndr-next-year-button', 85 previousYearButton: 'clndr-previous-year-button', 86 todayButton: 'clndr-today-button', 87 day: 'day', 88 empty: 'empty' 89 }, 90 events: [], 91 extras: null, 92 dateParameter: 'date', 93 multiDayEvents: null, 94 doneRendering: null, 95 render: null, 96 daysOfTheWeek: null, 97 showAdjacentMonths: true, 98 adjacentDaysChangeMonth: false, 99 ready: null, 100 constraints: null, 101 forceSixRows: null 102 }; 103 104 // The actual plugin constructor 105 function Clndr( element, options ) { 106 this.element = element; 107 108 // merge the default options with user-provided options 109 this.options = $.extend(true, {}, defaults, options); 110 111 // if there are events, we should run them through our addMomentObjectToEvents function 112 // which will add a date object that we can use to make life easier. This is only necessary 113 // when events are provided on instantiation, since our setEvents function uses addMomentObjectToEvents. 114 if(this.options.events.length) { 115 if(this.options.multiDayEvents) { 116 this.options.events = this.addMultiDayMomentObjectsToEvents(this.options.events); 117 } else { 118 this.options.events = this.addMomentObjectToEvents(this.options.events); 119 } 120 } 121 122 // this object will store a reference to the current month. 123 // it's a moment object, which allows us to poke at it a little if we need to. 124 // this will serve as the basis for switching between months & is the go-to 125 // internally if we want to know which month we're currently at. 126 if(this.options.startWithMonth) { 127 this.month = moment(this.options.startWithMonth).startOf('month'); 128 } else { 129 this.month = moment().startOf('month'); 130 } 131 132 // if we've got constraints set, make sure the month is within them. 133 if(this.options.constraints) { 134 // first check if the start date exists & is later than now. 135 if(this.options.constraints.startDate) { 136 var startMoment = moment(this.options.constraints.startDate); 137 if(this.month.isBefore(startMoment, 'month')) { 138 this.month.set('month', startMoment.month()); 139 this.month.set('year', startMoment.year()); 140 } 141 } 142 // make sure the month (whether modified or not) is before the endDate 143 if(this.options.constraints.endDate) { 144 var endMoment = moment(this.options.constraints.endDate); 145 if(this.month.isAfter(endMoment, 'month')) { 146 this.month.set('month', endMoment.month()).set('year', endMoment.year()); 147 } 148 } 149 } 150 151 this._defaults = defaults; 152 this._name = pluginName; 153 154 // Some first-time initialization -> day of the week offset, 155 // template compiling, making and storing some elements we'll need later, 156 // & event handling for the controller. 157 this.init(); 158 } 159 160 Clndr.prototype.init = function () { 161 // create the days of the week using moment's current language setting 162 this.daysOfTheWeek = this.options.daysOfTheWeek || []; 163 if(!this.options.daysOfTheWeek) { 164 this.daysOfTheWeek = []; 165 for(var i = 0; i < 7; i++) { 166 this.daysOfTheWeek.push( moment().weekday(i).format('ddd') ); 167 } 168 } 169 // shuffle the week if there's an offset 170 if(this.options.weekOffset) { 171 this.daysOfTheWeek = this.shiftWeekdayLabels(this.options.weekOffset); 172 } 173 174 // quick & dirty test to make sure rendering is possible. 175 if( !$.isFunction(this.options.render) ) { 176 this.options.render = null; 177 if (typeof _ === 'undefined') { 178 throw new Error("Underscore was not found. Please include underscore.js OR provide a custom render function."); 179 } 180 else { 181 // we're just going ahead and using underscore here if no render method has been supplied.
182 this.compiledClndrTemplate = _.template(this.options.template); 183 } 184 } 185 186 // create the parent element that will hold the plugin & save it for later 187 $(this.element).html("<div class='clndr'></div>"); 188 this.calendarContainer = $('.clndr', this.element); 189 190 // attach event handlers for clicks on buttons/cells 191 this.bindEvents(); 192 193 // do a normal render of the calendar template 194 this.render(); 195 196 // if a ready callback has been provided, call it. 197 if(this.options.ready) { 198 this.options.ready.apply(this, []); 199 } 200 }; 201 202 Clndr.prototype.shiftWeekdayLabels = function(offset) { 203 var days = this.daysOfTheWeek; 204 for(var i = 0; i < offset; i++) { 205 days.push( days.shift() ); 206 } 207 return days; 208 }; 209 210 // This is where the magic happens. Given a moment object representing the current month, 211 // an array of calendarDay objects is constructed that contains appropriate events and 212 // classes depending on the circumstance. 213 Clndr.prototype.createDaysObject = function(currentMonth) { 214 // this array will hold numbers for the entire grid (even the blank spaces) 215 daysArray = []; 216 var date = currentMonth.startOf('month'); 217 218 // filter the events list (if it exists) to events that are happening last month, this month and next month (within the current grid view) 219 this.eventsLastMonth = []; 220 this.eventsThisMonth = []; 221 this.eventsNextMonth = []; 222 223 if(this.options.events.length) { 224 225 // MULTI-DAY EVENT PARSING 226 // if we're using multi-day events, the start or end must be in the current month 227 if(this.options.multiDayEvents) { 228 this.eventsThisMonth = $(this.options.events).filter( function() { 229// return this._clndrStartDateObject.format("YYYY-MM") <= currentMonth.format("YYYY-MM") 230// || currentMonth.format("YYYY-MM") <= this._clndrEndDateObject.format("YYYY-MM"); 231 if ( this._clndrStartDateObject.format("YYYY-MM") === currentMonth.format("YYYY-MM") 232 || this._clndrEndDateObject.format("YYYY-MM") === currentMonth.format("YYYY-MM") ) { 233 return true; 234 } 235 if ( this._clndrStartDateObject.format("YYYY-MM") <= currentMonth.format("YYYY-MM") 236 && this._clndrEndDateObject.format("YYYY-MM") >= currentMonth.format("YYYY-MM") ) { 237 return true; 238 } 239 240 return false; 241 }).toArray(); 242 243 if(this.options.showAdjacentMonths) { 244 var lastMonth = currentMonth.clone().subtract(1, 'months'); 245 var nextMonth = currentMonth.clone().add(1, 'months'); 246 this.eventsLastMonth = $(this.options.events).filter( function() { 247// return this._clndrStartDateObject.format("YYYY-MM") <= lastMonth.format("YYYY-MM") 248// || lastMonth.format("YYYY-MM") <= this._clndrEndDateObject.format("YYYY-MM"); 249 if ( this._clndrStartDateObject.format("YYYY-MM") === lastMonth.format("YYYY-MM") 250 || this._clndrEndDateObject.format("YYYY-MM") === lastMonth.format("YYYY-MM") ) { 251 return true; 252 } 253 if ( this._clndrStartDateObject.format("YYYY-MM") <= lastMonth.format("YYYY-MM") 254 && this._clndrEndDateObject.format("YYYY-MM") >= lastMonth.format("YYYY-MM") ) { 255 return true; 256 } 257 258 return false; 259 }).toArray(); 260 261 this.eventsNextMonth = $(this.options.events).filter( function() { 262// return this._clndrStartDateObject.format("YYYY-MM") <= nextMonth.format("YYYY-MM") 263// || nextMonth.format("YYYY-MM") <= this._clndrEndDateObject.format("YYYY-MM"); 264 if ( this._clndrStartDateObject.format("YYYY-MM") === nextMonth.format("YYYY-MM") 265 || this._clndrEndDateObject.format("YYYY-MM") === nextMonth.format("YYYY-MM") ) { 266 return true; 267 } 268 if ( this._clndrStartDateObject.format("YYYY-MM") <= nextMonth.format("YYYY-MM") 269 && this._clndrEndDateObject.format("YYYY-MM") >= nextMonth.format("YYYY-MM") ) { 270 return true; 271 } 272 273 return false;
274 }).toArray(); 275 } 276 } 277 278 // SINGLE-DAY EVENT PARSING 279 // if we're using single-day events, use _clndrDateObject 280 else { 281 this.eventsThisMonth = $(this.options.events).filter( function() { 282 return this._clndrDateObject.format("YYYY-MM") == currentMonth.format("YYYY-MM"); 283 }).toArray(); 284 285 // filter the adjacent months as well, if the option is true 286 if(this.options.showAdjacentMonths) { 287 var lastMonth = currentMonth.clone().subtract(1, 'months'); 288 var nextMonth = currentMonth.clone().add(1, 'months'); 289 this.eventsLastMonth = $(this.options.events).filter( function() { 290 return this._clndrDateObject.format("YYYY-MM") == lastMonth.format("YYYY-MM"); 291 }).toArray(); 292 293 this.eventsNextMonth = $(this.options.events).filter( function() { 294 return this._clndrDateObject.format("YYYY-MM") == nextMonth.format("YYYY-MM"); 295 }).toArray(); 296 } 297 } 298 } 299 300 // if diff is greater than 0, we'll have to fill in last days of the previous month 301 // to account for the empty boxes in the grid. 302 // we also need to take into account the weekOffset parameter 303 var diff = date.weekday() - this.options.weekOffset; 304 if(diff < 0) diff += 7; 305 306 if(this.options.showAdjacentMonths) { 307 for(var i = 0; i < diff; i++) { 308 var day = moment([currentMonth.year(), currentMonth.month(), i - diff + 1]); 309 daysArray.push( this.createDayObject(day, this.eventsLastMonth) ); 310 } 311 } else { 312 for(var i = 0; i < diff; i++) { 313 daysArray.push( this.calendarDay({ classes: this.options.targets.empty + " last-month" }) ); 314 } 315 } 316 317 // now we push all of the days in a month 318 var numOfDays = date.daysInMonth(); 319 for(var i = 1; i <= numOfDays; i++) { 320 var day = moment.utc([currentMonth.year(), currentMonth.month(), i]); 321 daysArray.push(this.createDayObject(day, this.eventsThisMonth) ) 322 } 323 324 // ...and if there are any trailing blank boxes, fill those in 325 // with the next month first days 326 var i = 1; 327 while(daysArray.length % 7 !== 0) { 328 if(this.options.showAdjacentMonths) { 329 var day = moment.utc([currentMonth.year(), currentMonth.month(), numOfDays + i]); 330 daysArray.push( this.createDayObject(day, this.eventsNextMonth) ); 331 } else { 332 daysArray.push( this.calendarDay({ classes: this.options.targets.empty + " next-month" }) ); 333 } 334 i++; 335 } 336 337 // if we want to force six rows of calendar, now's our Last Chance to add another row. 338 // if the 42 seems explicit it's because we're creating a 7-row grid and 6 rows of 7 is always 42! 339 if(this.options.forceSixRows && daysArray.length !== 42 ) { 340 var start = moment(daysArray[daysArray.length - 1].date).add(1, 'days'); 341 while(daysArray.length < 42) { 342 if(this.options.showAdjacentMonths) { 343 daysArray.push( this.createDayObject(moment(start), this.eventsNextMonth) ); 344 start.add(1, 'days'); 345 } else { 346 daysArray.push( this.calendarDay({ classes: this.options.targets.empty + " next-month" }) ); 347 } 348 } 349 } 350 351 return daysArray; 352 }; 353 354 Clndr.prototype.createDayObject = function(day, monthEvents) { 355 var eventsToday = []; 356 var now = moment(); 357 var self = this; 358 359 var j = 0, l = monthEvents.length; 360 for(j; j < l; j++) { 361 // keep in mind that the events here already passed the month/year test. 362 // now all we have to compare is the moment.date(), which returns the day of the month. 363 if(self.options.multiDayEvents) { 364 var start = monthEvents[j]._clndrStartDateObject; 365 var end = monthEvents[j]._clndrEndDateObject; 366 // if today is the same day as start or is after the start, and 367 // if today is the same day as the end or before the end ... 368 // woohoo semantics! 369 if( ( day.isSame(start, 'day') || day.isAfter(start, 'day') ) && 370 ( day.isSame(end, 'day') || day.isBefore(end, 'day') ) ) { 371 eventsToday.push( monthEvents[j] ); 372 } 373 } else { 374 if( monthEvents[j]._clndrDateObject.date() == day.date() ) { 375 eventsToday.push( monthEvents[j] ); 376 } 377 } 378 } 379
380 var extraClasses = ""; 381 382 if(now.format("YYYY-MM-DD") == day.format("YYYY-MM-DD")) { 383 extraClasses += " today"; 384 } 385 if(day.isBefore(now, 'day')) { 386 extraClasses += " past"; 387 } 388 if(eventsToday.length) { 389 extraClasses += " event"; 390 } 391 if(this.month.month() > day.month()) { 392 extraClasses += " adjacent-month"; 393 394 this.month.year() === day.year() 395 ? extraClasses += " last-month" 396 : extraClasses += " next-month"; 397 398 } else if(this.month.month() < day.month()) { 399 extraClasses += " adjacent-month"; 400 401 this.month.year() === day.year() 402 ? extraClasses += " next-month" 403 : extraClasses += " last-month"; 404 } 405 406 // if there are constraints, we need to add the inactive class to the days outside of them 407 if(this.options.constraints) { 408 if(this.options.constraints.startDate && day.isBefore(moment( this.options.constraints.startDate ))) { 409 extraClasses += " inactive"; 410 } 411 if(this.options.constraints.endDate && day.isAfter(moment( this.options.constraints.endDate ))) { 412 extraClasses += " inactive"; 413 } 414 } 415 416 // validate moment date 417 if (!day.isValid() && day.hasOwnProperty('_d') && day._d != undefined) { 418 day = moment(day._d); 419 } 420 421 // we're moving away from using IDs in favor of classes, since when 422 // using multiple calendars on a page we are technically violating the 423 // uniqueness of IDs. 424 extraClasses += " calendar-day-" + day.format("YYYY-MM-DD"); 425 426 // day of week 427 extraClasses += " calendar-dow-" + day.weekday(); 428 429 return this.calendarDay({ 430 day: day.date(), 431 classes: this.options.targets.day + extraClasses, 432 events: eventsToday, 433 date: day 434 }); 435 }; 436 437 Clndr.prototype.render = function() { 438 // get rid of the previous set of calendar parts.
439 // TODO: figure out if this is the right way to ensure proper garbage collection? 440 this.calendarContainer.children().remove(); 441 // get an array of days and blank spaces 442 var days = this.createDaysObject(this.month); 443 // this is to prevent a scope/naming issue between this.month and data.month 444 var currentMonth = this.month; 445 446 var data = { 447 daysOfTheWeek: this.daysOfTheWeek, 448 numberOfRows: Math.ceil(days.length / 7), 449 days: days, 450 month: this.month.format('MMMM'), 451 year: this.month.year(), 452 eventsThisMonth: this.eventsThisMonth, 453 eventsLastMonth: this.eventsLastMonth, 454 eventsNextMonth: this.eventsNextMonth, 455 extras: this.options.extras 456 }; 457 458 // render the calendar with the data above & bind events to its elements 459 if(!this.options.render) { 460 this.calendarContainer.html(this.compiledClndrTemplate(data)); 461 } else { 462 this.calendarContainer.html(this.options.render.apply(this, [data])); 463 } 464 465 // if there are constraints, we need to add the 'inactive' class to the controls 466 if(this.options.constraints) { 467 // in the interest of clarity we're just going to remove all inactive classes and re-apply them each render. 468 for(var target in this.options.targets) { 469 if(target != this.options.targets.day) { 470 this.element.find('.' + this.options.targets[target]).toggleClass('inactive', false); 471 } 472 } 473 474 var start = null; 475 var end = null; 476 477 if(this.options.constraints.startDate) { 478 start = moment(this.options.constraints.startDate); 479 } 480 if(this.options.constraints.endDate) { 481 end = moment(this.options.constraints.endDate); 482 } 483 // deal with the month controls first. 484 // are we at the start month? 485 if(start && this.month.isSame( start, 'month' )) { 486 this.element.find('.' + this.options.targets.previousButton).toggleClass('inactive', true); 487 } 488 // are we at the end month? 489 if(end && this.month.isSame( end, 'month' )) { 490 this.element.find('.' + this.options.targets.nextButton).toggleClass('inactive', true); 491 } 492 // what's last year looking like? 493 if(start && moment(start).subtract(1, 'years').isBefore(moment(this.month).subtract(1, 'years')) ) { 494 this.element.find('.' + this.options.targets.previousYearButton).toggleClass('inactive', true); 495 } 496 // how about next year? 497 if(end && moment(end).add(1, 'years').isAfter(moment(this.month).add(1, 'years')) ) { 498 this.element.find('.' + this.options.targets.nextYearButton).toggleClass('inactive', true); 499 } 500 // today? we could put this in init(), but we want to support the user changing the constraints on a living instance. 501 if(( start && start.isAfter( moment(), 'month' ) ) || ( end && end.isBefore( moment(), 'month' ) )) { 502 this.element.find('.' + this.options.targets.today).toggleClass('inactive', true); 503 } 504 } 505 506 507 if(this.options.doneRendering) { 508 this.options.doneRendering.apply(this, []); 509 } 510 }; 511 512 Clndr.prototype.bindEvents = function() { 513 var $container = $(this.element); 514 var self = this; 515 516 // target the day elements and give them click events 517 $container.on('click', '.'+this.options.targets.day, function(event) { 518 if(self.options.clickEvents.click) { 519 var target = self.buildTargetObject(event.currentTarget, true); 520 self.options.clickEvents.click.apply(self, [target]); 521 } 522 // if adjacentDaysChangeMonth is on, we need to change the month here. 523 if(self.options.adjacentDaysChangeMonth) { 524 if($(event.currentTarget).is(".last-month")) { 525 self.backActionWithContext(self); 526 } else if($(event.currentTarget).is(".next-month")) { 527 self.forwardActionWithContext(self); 528 } 529 } 530 }); 531 // target the empty calendar boxes as well 532 $container.on('click', '.'+this.options.targets.empty, function(event) { 533 if(self.options.clickEvents.click) { 534 var target = self.buildTargetObject(event.currentTarget, false); 535 self.options.clickEvents.click.apply(self, [target]); 536 } 537 if(self.options.adjacentDaysChangeMonth) { 538 if($(event.currentTarget).is(".last-month")) { 539 self.backActionWithContext(self); 540 } else if($(event.currentTarget).is(".next-month")) { 541 self.forwardActionWithContext(self); 542 } 543 } 544 }); 545 546 // bind the previous, next and today buttons 547 $container 548 .on('click', '.'+this.options.targets.previousButton, { context: this }, this.backAction) 549 .on('click', '.'+this.options.targets.nextButton, { context: this }, this.forwardAction) 550 .on('click', '.'+this.options.targets.todayButton, { context: this }, this.todayAction) 551 .on('click', '.'+this.options.targets.nextYearButton, { context: this }, this.nextYearAction) 552 .on('click', '.'+this.options.targets.previousYearButton, { context: this }, this.previousYearAction); 553 } 554 555 // If the user provided a click callback we'd like to give them something nice to work with. 556 // buildTargetObject takes the DOM element that was clicked and returns an object with 557 // the DOM element, events, and the date (if the latter two exist). Currently it is based on the id, 558 // however it'd be nice to use a data- attribute in the future. 559 Clndr.prototype.buildTargetObject = function(currentTarget, targetWasDay) { 560 // This is our default target object, assuming we hit an empty day with no events. 561 var target = { 562 element: currentTarget, 563 events: [], 564 date: null 565 }; 566 // did we click on a day or just an empty box? 567 if(targetWasDay) { 568 var dateString; 569 570 // Our identifier is in the list of classNames. Find it! 571 var classNameIndex = currentTarget.className.indexOf('calendar-day-'); 572 if(classNameIndex !== 0) { 573 // our unique identifier is always 23 characters long. 574 // If this feels a little wonky, that's probably because it is.
575 // Open to suggestions on how to improve this guy. 576 dateString = currentTarget.className.substring(classNameIndex + 13, classNameIndex + 23); 577 target.date = moment(dateString); 578 } else { 579 target.date = null; 580 } 581 582 // do we have events? 583 if(this.options.events) { 584 // are any of the events happening today? 585 if(this.options.multiDayEvents) { 586 target.events = $.makeArray( $(this.options.events).filter( function() { 587 // filter the dates down to the ones that match. 588 return ( ( target.date.isSame(this._clndrStartDateObject, 'day') || target.date.isAfter(this._clndrStartDateObject, 'day') ) && 589 ( target.date.isSame(this._clndrEndDateObject, 'day') || target.date.isBefore(this._clndrEndDateObject, 'day') ) ); 590 }) ); 591 } else { 592 target.events = $.makeArray( $(this.options.events).filter( function() { 593 // filter the dates down to the ones that match. 594 return this._clndrDateObject.format('YYYY-MM-DD') == dateString; 595 }) ); 596 } 597 } 598 } 599 600 return target; 601 } 602 603 // the click handlers in bindEvents need a context, so these are wrappers 604 // to the actual functions. Todo: better way to handle this? 605 Clndr.prototype.forwardAction = function(event) { 606 var self = event.data.context; 607 self.forwardActionWithContext(self); 608 }; 609 610 Clndr.prototype.backAction = function(event) { 611 var self = event.data.context; 612 self.backActionWithContext(self); 613 }; 614 615 // These are called directly, except for in the bindEvent click handlers, 616 // where forwardAction and backAction proxy to these guys. 617 Clndr.prototype.backActionWithContext = function(self) { 618 // before we do anything, check if there is an inactive class on the month control. 619 // if it does, we want to return and take no action. 620 if(self.element.find('.' + self.options.targets.previousButton).hasClass('inactive')) { 621 return; 622 } 623 624 // is subtracting one month going to switch the year? 625 var yearChanged = !self.month.isSame( moment(self.month).subtract(1, 'months'), 'year'); 626 self.month.subtract(1, 'months'); 627 628 self.render(); 629 630 if(self.options.clickEvents.previousMonth) { 631 self.options.clickEvents.previousMonth.apply( self, [moment(self.month)] ); 632 } 633 if(self.options.clickEvents.onMonthChange) { 634 self.options.clickEvents.onMonthChange.apply( self, [moment(self.month)] ); 635 } 636 if(yearChanged) { 637 if(self.options.clickEvents.onYearChange) { 638 self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] ); 639 } 640 } 641 }; 642 643 Clndr.prototype.forwardActionWithContext = function(self) { 644 // before we do anything, check if there is an inactive class on the month control. 645 // if it does, we want to return and take no action. 646 if(self.element.find('.' + self.options.targets.nextButton).hasClass('inactive')) { 647 return; 648 } 649 650 // is adding one month going to switch the year? 651 var yearChanged = !self.month.isSame( moment(self.month).add(1, 'months'), 'year'); 652 self.month.add(1, 'months'); 653 654 self.render(); 655 656 if(self.options.clickEvents.nextMonth) { 657 self.options.clickEvents.nextMonth.apply(self, [moment(self.month)]); 658 } 659 if(self.options.clickEvents.onMonthChange) { 660 self.options.clickEvents.onMonthChange.apply(self, [moment(self.month)]); 661 } 662 if(yearChanged) { 663 if(self.options.clickEvents.onYearChange) { 664 self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] ); 665 } 666 } 667 }; 668 669 Clndr.prototype.todayAction = function(event) { 670 var self = event.data.context; 671 672 // did we switch months when the today button was hit? 673 var monthChanged = !self.month.isSame(moment(), 'month'); 674 var yearChanged = !self.month.isSame(moment(), 'year'); 675 676 self.month = moment().startOf('month'); 677 678 // fire the today event handler regardless of whether the month changed. 679 if(self.options.clickEvents.today) { 680 self.options.clickEvents.today.apply( self, [moment(self.month)] ); 681 } 682
683 if(monthChanged) { 684 // no need to re-render if we didn't change months. 685 self.render(); 686 687 self.month = moment(); 688 // fire the onMonthChange callback 689 if(self.options.clickEvents.onMonthChange) { 690 self.options.clickEvents.onMonthChange.apply( self, [moment(self.month)] ); 691 } 692 // maybe fire the onYearChange callback? 693 if(yearChanged) { 694 if(self.options.clickEvents.onYearChange) { 695 self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] ); 696 } 697 } 698 } 699 }; 700 701 Clndr.prototype.nextYearAction = function(event) { 702 var self = event.data.context; 703 // before we do anything, check if there is an inactive class on the month control. 704 // if it does, we want to return and take no action. 705 if(self.element.find('.' + self.options.targets.nextYearButton).hasClass('inactive')) { 706 return; 707 } 708 709 self.month.add(1, 'years'); 710 self.render(); 711 712 if(self.options.clickEvents.nextYear) { 713 self.options.clickEvents.nextYear.apply( self, [moment(self.month)] ); 714 } 715 if(self.options.clickEvents.onMonthChange) { 716 self.options.clickEvents.onMonthChange.apply( self, [moment(self.month)] ); 717 } 718 if(self.options.clickEvents.onYearChange) { 719 self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] ); 720 } 721 }; 722 723 Clndr.prototype.previousYearAction = function(event) { 724 var self = event.data.context; 725 // before we do anything, check if there is an inactive class on the month control. 726 // if it does, we want to return and take no action. 727 if(self.element.find('.' + self.options.targets.previousYear).hasClass('inactive')) { 728 return; 729 } 730 731 self.month.subtract(1, 'years'); 732 self.render(); 733 734 if(self.options.clickEvents.previousYear) { 735 self.options.clickEvents.previousYear.apply( self, [moment(self.month)] ); 736 } 737 if(self.options.clickEvents.onMonthChange) { 738 self.options.clickEvents.onMonthChange.apply( self, [moment(self.month)] ); 739 } 740 if(self.options.clickEvents.onYearChange) { 741 self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] ); 742 } 743 }; 744 745 Clndr.prototype.forward = function(options) { 746 this.month.add(1, 'months'); 747 this.render(); 748 if(options && options.withCallbacks) { 749 if(this.options.clickEvents.onMonthChange) { 750 this.options.clickEvents.onMonthChange.apply( this, [moment(this.month)] ); 751 } 752 753 // We entered a new year 754 if (this.month.month() === 0 && this.options.clickEvents.onYearChange) { 755 this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] ); 756 } 757 } 758 759 return this; 760 } 761 762 Clndr.prototype.back = function(options) { 763 this.month.subtract(1, 'months'); 764 this.render(); 765 if(options && options.withCallbacks) { 766 if(this.options.clickEvents.onMonthChange) { 767 this.options.clickEvents.onMonthChange.apply( this, [moment(this.month)] ); 768 } 769 770 // We went all the way back to previous year 771 if (this.month.month() === 11 && this.options.clickEvents.onYearChange) { 772 this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] ); 773 } 774 } 775 776 return this; 777 } 778 779 // alternate names for convenience 780 Clndr.prototype.next = function(options) { 781 this.forward(options); 782 return this; 783 } 784 785 Clndr.prototype.previous = function(options) { 786 this.back(options); 787 return this; 788 } 789 790 Clndr.prototype.setMonth = function(newMonth, options) { 791 // accepts 0 - 11 or a full/partial month name e.g. "Jan", "February", "Mar" 792 this.month.month(newMonth); 793 this.render(); 794 if(options && options.withCallbacks) { 795 if(this.options.clickEvents.onMonthChange) { 796 this.options.clickEvents.onMonthChange.apply( this, [moment(this.month)] ); 797 } 798 } 799 return this; 800 } 801 802 Clndr.prototype.nextYear = function(options) { 803 this.month.add(1, 'year'); 804 this.render(); 805 if(options && options.withCallbacks) { 806 if(this.options.clickEvents.onYearChange) { 807 this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] ); 808 } 809 } 810 return this; 811 } 812 813 Clndr.prototype.previousYear = function(options) { 814 this.month.subtract(1, 'year'); 815 this.render(); 816 if(options && options.withCallbacks) { 817 if(this.options.clickEvents.onYearChange) { 818 this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] ); 819 } 820 } 821 return this; 822 } 823 824 Clndr.prototype.setYear = function(newYear, options) { 825 this.month.year(newYear); 826 this.render(); 827 if(options && options.withCallbacks) { 828 if(this.options.clickEvents.onYearChange) { 829 this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] ); 830 } 831 } 832 return this; 833 } 834 835 Clndr.prototype.setEvents = function(events) { 836 // go through each event and add a moment object 837 if(this.options.multiDayEvents) { 838 this.options.events = this.addMultiDayMomentObjectsToEvents(events); 839 } else { 840 this.options.events = this.addMomentObjectToEvents(events); 841 } 842 843 this.render(); 844 return this; 845 }; 846 847 Clndr.prototype.addEvents = function(events) { 848 // go through each event and add a moment object 849 if(this.options.multiDayEvents) { 850 this.options.events = $.merge(this.options.events, this.addMultiDayMomentObjectsToEvents(events)); 851 } else { 852 this.options.events = $.merge(this.options.events, this.addMomentObjectToEvents(events)); 853 } 854 855 this.render(); 856 return this; 857 }; 858 859 Clndr.prototype.removeEvents = function(matchingFunction) { 860 for (var i = this.options.events.length-1; i >= 0; i--) { 861 if(matchingFunction(this.options.events[i]) == true) { 862 this.options.events.splice(i, 1); 863 } 864 } 865 866 this.render(); 867 return this; 868 }; 869 870 Clndr.prototype.addMomentObjectToEvents = function(events) { 871 var self = this; 872 var i = 0, l = events.length; 873 for(i; i < l; i++) { 874 // stuff a _clndrDateObject in each event, which really, REALLY should not be 875 // overriding any existing object... Man that would be weird. 876 events[i]._clndrDateObject = moment( events[i][self.options.dateParameter] ); 877 } 878 return events; 879 } 880 881 Clndr.prototype.addMultiDayMomentObjectsToEvents = function(events) { 882 var self = this; 883 var i = 0, l = events.length; 884 for(i; i < l; i++) { 885 events[i]._clndrStartDateObject = moment( events[i][self.options.multiDayEvents.startDate] ); 886 events[i]._clndrEndDateObject = moment( events[i][self.options.multiDayEvents.endDate] ); 887 } 888 return events; 889 } 890 891 Clndr.prototype.calendarDay = function(options) { 892 var defaults = { day: "", classes: this.options.targets.empty, events: [], date: null }; 893 return $.extend({}, defaults, options); 894 } 895 896 $.fn.clndr = function(options) { 897 if(this.length === 1) { 898 if(!this.data('plugin_clndr')) { 899 var clndr_instance = new Clndr(this, options); 900 this.data('plugin_clndr', clndr_instance); 901 return clndr_instance; 902 } 903 } else if(this.length > 1) { 904 throw new Error("CLNDR does not support multiple elements yet. Make sure your clndr selector returns only one element."); 905 } 906 } 907 908}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.