1/** 2 * ~ CLNDR v1.4.7 ~ 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(function (factory) { 22 // Multiple loading methods are supported depending on 23 // what is available globally. While moment is loaded 24 // here, the instance can be passed in at config time. 25 if (typeof define === 'function' && define.amd) { 26 // AMD. Register as an anonymous module. 27 define(['jquery', 'moment'], factory); 28 } 29 else if (typeof exports === 'object') { 30 // Node/CommonJS 31 factory(require('jquery'), require('moment')); 32 } 33 else { 34 // Browser globals 35 factory(jQuery, moment); 36 } 37}(function ($, moment) { 38 // Namespace 39 var pluginName = 'clndr'; 40 41 // This is the default calendar template. This can be overridden. 42 var clndrTemplate = 43 "<div class='clndr-controls'>" + 44 "<div class='clndr-control-button'>" + 45 "<span class='clndr-previous-button'>previous</span>" + 46 "</div>" + 47 "<div class='month'><%= month %> <%= year %></div>" + 48 "<div class='clndr-control-button rightalign'>" + 49 "<span class='clndr-next-button'>next</span>" + 50 "</div>" + 51 "</div>" + 52 "<table class='clndr-table' border='0' cellspacing='0' cellpadding='0'>" + 53 "<thead>" + 54 "<tr class='header-days'>" + 55 "<% for(var i = 0; i < daysOfTheWeek.length; i++) { %>" + 56 "<td class='header-day'><%= daysOfTheWeek[i] %></td>" + 57 "<% } %>" + 58 "</tr>" + 59 "</thead>" + 60 "<tbody>" + 61 "<% for(var i = 0; i < numberOfRows; i++){ %>" + 62 "<tr>" + 63 "<% for(var j = 0; j < 7; j++){ %>" + 64 "<% var d = j + i * 7; %>" + 65 "<td class='<%= days[d].classes %>'>" + 66 "<div class='day-contents'><%= days[d].day %></div>" + 67 "</td>" + 68 "<% } %>" + 69 "</tr>" + 70 "<% } %>" + 71 "</tbody>" + 72 "</table>"; 73 74 // Defaults used throughout the application, see docs. 75 var defaults = { 76 events: [], 77 ready: null, 78 extras: null, 79 render: null, 80 moment: null, 81 weekOffset: 0, 82 constraints: null, 83 forceSixRows: null, 84 selectedDate: null, 85 doneRendering: null, 86 daysOfTheWeek: null, 87 multiDayEvents: null, 88 startWithMonth: null, 89 dateParameter: 'date', 90 template: clndrTemplate, 91 showAdjacentMonths: true, 92 trackSelectedDate: false, 93 adjacentDaysChangeMonth: false, 94 ignoreInactiveDaysInSelection: null, 95 lengthOfTime: { 96 days: null, 97 interval: 1, 98 months: null 99 }, 100 clickEvents: { 101 click: null, 102 today: null, 103 nextYear: null, 104 nextMonth: null, 105 nextInterval: null, 106 previousYear: null, 107 onYearChange: null, 108 previousMonth: null, 109 onMonthChange: null, 110 previousInterval: null, 111 onIntervalChange: null 112 }, 113 targets: { 114 day: 'day', 115 empty: 'empty', 116 nextButton: 'clndr-next-button', 117 todayButton: 'clndr-today-button', 118 previousButton: 'clndr-previous-button', 119 nextYearButton: 'clndr-next-year-button', 120 previousYearButton: 'clndr-previous-year-button' 121 }, 122 classes: { 123 past: "past", 124 today: "today", 125 event: "event", 126 inactive: "inactive", 127 selected: "selected", 128 lastMonth: "last-month", 129 nextMonth: "next-month", 130 adjacentMonth: "adjacent-month" 131 }, 132 }; 133 134 /** 135 * The actual plugin constructor. 136 * Parses the events and lengthOfTime options to build a calendar of day 137 * objects containing event information from the events array. 138 */ 139 function Clndr(element, options) { 140 var dayDiff; 141 var constraintEnd; 142 var constraintStart; 143 144 this.element = element; 145
146 // Merge the default options with user-provided options 147 this.options = $.extend(true, {}, defaults, options); 148 149 // Check if moment was passed in as a dependency 150 if (this.options.moment) { 151 moment = this.options.moment; 152 } 153 154 // Boolean values used to log if any contraints are met 155 this.constraints = { 156 next: true, 157 today: true, 158 previous: true, 159 nextYear: true, 160 previousYear: true 161 }; 162 163 // If there are events, we should run them through our 164 // addMomentObjectToEvents function which will add a date object that 165 // we can use to make life easier. This is only necessarywhen events 166 // are provided on instantiation, since our setEvents function uses 167 // addMomentObjectToEvents. 168 if (this.options.events.length) { 169 if (this.options.multiDayEvents) { 170 this.options.events = 171 this.addMultiDayMomentObjectsToEvents(this.options.events); 172 } else { 173 this.options.events = 174 this.addMomentObjectToEvents(this.options.events); 175 } 176 } 177 178 // This used to be a place where we'd figure out the current month, 179 // but since we want to open up support for arbitrary lengths of time 180 // we're going to store the current range in addition to the current 181 // month. 182 if (this.options.lengthOfTime.months || this.options.lengthOfTime.days) { 183 // We want to establish intervalStart and intervalEnd, which will 184 // keep track of our boundaries. Let's look at the possibilities... 185 if (this.options.lengthOfTime.months) { 186 // Gonna go right ahead and annihilate any chance for bugs here 187 this.options.lengthOfTime.days = null; 188 189 // The length is specified in months. Is there a start date? 190 if (this.options.lengthOfTime.startDate) { 191 this.intervalStart = 192 moment(this.options.lengthOfTime.startDate) 193 .startOf('month'); 194 } else if (this.options.startWithMonth) { 195 this.intervalStart = 196 moment(this.options.startWithMonth) 197 .startOf('month'); 198 } else { 199 this.intervalStart = moment().startOf('month'); 200 } 201 202 // Subtract a day so that we are at the end of the interval. We 203 // always want intervalEnd to be inclusive. 204 this.intervalEnd = moment(this.intervalStart) 205 .add(this.options.lengthOfTime.months, 'months') 206 .subtract(1, 'days'); 207 this.month = this.intervalStart.clone(); 208 } 209 else if (this.options.lengthOfTime.days) { 210 // The length is specified in days. Start date? 211 if (this.options.lengthOfTime.startDate) { 212 this.intervalStart = 213 moment(this.options.lengthOfTime.startDate) 214 .startOf('day'); 215 } else { 216 this.intervalStart = moment().weekday(0).startOf('day'); 217 } 218 219 this.intervalEnd = moment(this.intervalStart) 220 .add(this.options.lengthOfTime.days - 1, 'days') 221 .endOf('day'); 222 this.month = this.intervalStart.clone(); 223 } 224 // No length of time specified so we're going to default into using the 225 // current month as the time period. 226 } else { 227 this.month = moment().startOf('month'); 228 this.intervalStart = moment(this.month); 229 this.intervalEnd = moment(this.month).endOf('month'); 230 } 231 232 if (this.options.startWithMonth) { 233 this.month = moment(this.options.startWithMonth).startOf('month'); 234 this.intervalStart = moment(this.month); 235 this.intervalEnd = (this.options.lengthOfTime.days) 236 ? moment(this.month) 237 .add(this.options.lengthOfTime.days - 1, 'days') 238 .endOf('day') 239 : moment(this.month).endOf('month'); 240 } 241 242 // If we've got constraints set, make sure the interval is within them. 243 if (this.options.constraints) { 244 // First check if the startDate exists & is later than now. 245 if (this.options.constraints.startDate) { 246 constraintStart = moment(this.options.constraints.startDate); 247 248 // We need to handle the constraints differently for weekly 249 // calendars vs. monthly calendars. 250 if (this.options.lengthOfTime.days) { 251 if (this.intervalStart.isBefore(constraintStart, 'week')) { 252 this.intervalStart = constraintStart.startOf('week'); 253 } 254
255 // If the new interval period is less than the desired length 256 // of time, or before the starting interval, then correct it. 257 dayDiff = this.intervalStart.diff(this.intervalEnd, 'days'); 258 259 if (dayDiff < this.options.lengthOfTime.days 260 || this.intervalEnd.isBefore(this.intervalStart)) 261 { 262 this.intervalEnd = moment(this.intervalStart) 263 .add(this.options.lengthOfTime.days - 1, 'days') 264 .endOf('day'); 265 this.month = this.intervalStart.clone(); 266 } 267 } 268 else { 269 if (this.intervalStart.isBefore(constraintStart, 'month')) { 270 // Try to preserve the date by moving only the month. 271 this.intervalStart 272 .set('month', constraintStart.month()) 273 .set('year', constraintStart.year()); 274 this.month 275 .set('month', constraintStart.month()) 276 .set('year', constraintStart.year()); 277 } 278 279 // Check if the ending interval is earlier than now. 280 if (this.intervalEnd.isBefore(constraintStart, 'month')) { 281 this.intervalEnd 282 .set('month', constraintStart.month()) 283 .set('year', constraintStart.year()); 284 } 285 } 286 } 287 288 // Make sure the intervalEnd is before the endDate. 289 if (this.options.constraints.endDate) { 290 constraintEnd = moment(this.options.constraints.endDate); 291 292 // We need to handle the constraints differently for weekly 293 // calendars vs. monthly calendars. 294 if (this.options.lengthOfTime.days) { 295 // The starting interval is after our ending constraint. 296 if (this.intervalStart.isAfter(constraintEnd, 'week')) { 297 this.intervalStart = moment(constraintEnd) 298 .endOf('week') 299 .subtract(this.options.lengthOfTime.days - 1, 'days') 300 .startOf('day'); 301 this.intervalEnd = moment(constraintEnd) 302 .endOf('week'); 303 this.month = this.intervalStart.clone(); 304 } 305 } 306 else { 307 if (this.intervalEnd.isAfter(constraintEnd, 'month')) { 308 this.intervalEnd 309 .set('month', constraintEnd.month()) 310 .set('year', constraintEnd.year()); 311 this.month 312 .set('month', constraintEnd.month()) 313 .set('year', constraintEnd.year()); 314 } 315 316 // Check if the starting interval is later than the ending. 317 if (this.intervalStart.isAfter(constraintEnd, 'month')) { 318 this.intervalStart 319 .set('month', constraintEnd.month()) 320 .set('year', constraintEnd.year()); 321 } 322 } 323 } 324 } 325 326 this._defaults = defaults; 327 this._name = pluginName; 328 329 // Some first-time initialization -> day of the week offset, template 330 // compiling, making and storing some elements we'll need later, and 331 // event handling for the controller. 332 this.init(); 333 } 334 335 /** 336 * Calendar initialization. 337 * Sets up the days of the week, the rendering function, binds all of the 338 * events to the rendered calendar, and then stores the node locally. 339 */ 340 Clndr.prototype.init = function () { 341 // Create the days of the week using moment's current language setting 342 this.daysOfTheWeek = this.options.daysOfTheWeek || []; 343 344 if (!this.options.daysOfTheWeek) { 345 this.daysOfTheWeek = []; 346 347 for (var i = 0; i < 7; i++) { 348 this.daysOfTheWeek.push( 349 moment().weekday(i).format('dd').charAt(0)); 350 } 351 } 352 353 // Shuffle the week if there's an offset 354 if (this.options.weekOffset) { 355 this.daysOfTheWeek = this.shiftWeekdayLabels(this.options.weekOffset); 356 } 357 358 // Quick and dirty test to make sure rendering is possible. 359 if (!$.isFunction(this.options.render)) { 360 this.options.render = null; 361 362 if (typeof _ === 'undefined') { 363 throw new Error( 364 "Underscore was not found. Please include underscore.js " + 365 "OR provide a custom render function."); 366 } else { 367 // We're just going ahead and using underscore here if no 368 // render method has been supplied.
369 this.compiledClndrTemplate = _.template(this.options.template); 370 } 371 } 372 373 // Create the parent element that will hold the plugin and save it 374 // for later 375 $(this.element).html("<div class='clndr'></div>"); 376 this.calendarContainer = $('.clndr', this.element); 377 378 // Attach event handlers for clicks on buttons/cells 379 this.bindEvents(); 380 381 // Do a normal render of the calendar template 382 this.render(); 383 384 // If a ready callback has been provided, call it. 385 if (this.options.ready) { 386 this.options.ready.apply(this, []); 387 } 388 }; 389 390 Clndr.prototype.shiftWeekdayLabels = function (offset) { 391 var days = this.daysOfTheWeek; 392 393 for (var i = 0; i < offset; i++) { 394 days.push(days.shift()); 395 } 396 397 return days; 398 }; 399 400 /** 401 * This is where the magic happens. Given a starting date and ending date, 402 * an array of calendarDay objects is constructed that contains appropriate 403 * events and classes depending on the circumstance. 404 */ 405 Clndr.prototype.createDaysObject = function (startDate, endDate) { 406 // This array will hold numbers for the entire grid (even the blank 407 // spaces). 408 var daysArray = [], 409 date = startDate.clone(), 410 lengthOfInterval = endDate.diff(startDate, 'days'), 411 startOfLastMonth, endOfLastMonth, startOfNextMonth, 412 endOfNextMonth, diff, dateIterator; 413 414 // This is a helper object so that days can resolve their classes 415 // correctly. Don't use it for anything please. 416 this._currentIntervalStart = startDate.clone(); 417 418 // Filter the events list (if it exists) to events that are happening 419 // last month, this month and next month (within the current grid view). 420 this.eventsLastMonth = []; 421 this.eventsNextMonth = []; 422 this.eventsThisInterval = []; 423 424 // Event parsing 425 if (this.options.events.length) { 426 // Here are the only two cases where we don't get an event in our 427 // interval: 428 // startDate | endDate | e.start | e.end 429 // e.start | e.end | startDate | endDate 430 this.eventsThisInterval = $(this.options.events).filter( 431 function () { 432 var afterEnd = this._clndrStartDateObject.isAfter(endDate), 433 beforeStart = this._clndrEndDateObject.isBefore(startDate); 434 435 if (beforeStart || afterEnd) { 436 return false; 437 } else { 438 return true; 439 } 440 }).toArray(); 441 442 if (this.options.showAdjacentMonths) { 443 startOfLastMonth = startDate.clone() 444 .subtract(1, 'months') 445 .startOf('month'); 446 endOfLastMonth = startOfLastMonth.clone().endOf('month'); 447 startOfNextMonth = endDate.clone() 448 .add(1, 'months') 449 .startOf('month'); 450 endOfNextMonth = startOfNextMonth.clone().endOf('month'); 451 452 this.eventsLastMonth = $(this.options.events).filter( 453 function () { 454 var beforeStart = this._clndrEndDateObject 455 .isBefore(startOfLastMonth); 456 var afterEnd = this._clndrStartDateObject 457 .isAfter(endOfLastMonth); 458 459 if (beforeStart || afterEnd) { 460 return false; 461 } else { 462 return true; 463 } 464 }).toArray(); 465 466 this.eventsNextMonth = $(this.options.events).filter( 467 function () { 468 var beforeStart = this._clndrEndDateObject 469 .isBefore(startOfNextMonth); 470 var afterEnd = this._clndrStartDateObject 471 .isAfter(endOfNextMonth); 472 473 if (beforeStart || afterEnd) { 474 return false;
475 } else { 476 return true; 477 } 478 }).toArray(); 479 } 480 } 481 482 // If diff is greater than 0, we'll have to fill in last days of the 483 // previous month to account for the empty boxes in the grid. We also 484 // need to take into account the weekOffset parameter. None of this 485 // needs to happen if the interval is being specified in days rather 486 // than months. 487 if (!this.options.lengthOfTime.days) { 488 diff = date.weekday() - this.options.weekOffset; 489 490 if (diff < 0) { 491 diff += 7; 492 } 493 494 if (this.options.showAdjacentMonths) { 495 for (var i = 1; i <= diff; i++) { 496 var day = moment([ 497 startDate.year(), 498 startDate.month(), 499 i 500 ]).subtract(diff, 'days'); 501 daysArray.push( 502 this.createDayObject( 503 day, 504 this.eventsLastMonth 505 )); 506 } 507 } else { 508 for (var i = 0; i < diff; i++) { 509 daysArray.push( 510 this.calendarDay({ 511 classes: this.options.targets.empty + 512 " " + this.options.classes.lastMonth 513 })); 514 } 515 } 516 } 517 518 // Now we push all of the days in the interval 519 dateIterator = startDate.clone(); 520 521 while (dateIterator.isBefore(endDate) || dateIterator.isSame(endDate, 'day')) { 522 daysArray.push( 523 this.createDayObject( 524 dateIterator.clone(), 525 this.eventsThisInterval 526 )); 527 dateIterator.add(1, 'days'); 528 } 529 530 // ...and if there are any trailing blank boxes, fill those in with the 531 // next month first days. Again, we can ignore this if the interval is 532 // specified in days. 533 if (!this.options.lengthOfTime.days) { 534 while (daysArray.length % 7 !== 0) { 535 if (this.options.showAdjacentMonths) { 536 daysArray.push( 537 this.createDayObject( 538 dateIterator.clone(), 539 this.eventsNextMonth 540 )); 541 } else { 542 daysArray.push( 543 this.calendarDay({ 544 classes: this.options.targets.empty + " " + 545 this.options.classes.nextMonth 546 })); 547 } 548 dateIterator.add(1, 'days'); 549 } 550 } 551 552 // If we want to force six rows of calendar, now's our Last Chance to 553 // add another row. If the 42 seems explicit it's because we're 554 // creating a 7-row grid and 6 rows of 7 is always 42! 555 if (this.options.forceSixRows && daysArray.length !== 42) { 556 while (daysArray.length < 42) { 557 if (this.options.showAdjacentMonths) { 558 daysArray.push( 559 this.createDayObject( 560 dateIterator.clone(), 561 this.eventsNextMonth 562 )); 563 dateIterator.add(1, 'days'); 564 } else { 565 daysArray.push( 566 this.calendarDay({ 567 classes: this.options.targets.empty + " " + 568 this.options.classes.nextMonth 569 })); 570 } 571 } 572 } 573 574 return daysArray; 575 }; 576 577 Clndr.prototype.createDayObject = function (day, monthEvents) { 578 var j = 0, 579 self = this, 580 now = moment(), 581 eventsToday = [], 582 extraClasses = "", 583 properties = { 584 isToday: false, 585 isInactive: false,
586 isAdjacentMonth: false 587 }, 588 startMoment, endMoment, selectedMoment; 589 590 // Validate moment date 591 if (!day.isValid() && day.hasOwnProperty('_d') && day._d != undefined) { 592 day = moment(day._d); 593 } 594 595 for (j; j < monthEvents.length; j++) { 596 // Keep in mind that the events here already passed the month/year 597 // test. Now all we have to compare is the moment.date(), which 598 // returns the day of the month. 599 var start = monthEvents[j]._clndrStartDateObject, 600 end = monthEvents[j]._clndrEndDateObject; 601 // If today is the same day as start or is after the start, and 602 // if today is the same day as the end or before the end ... 603 // woohoo semantics! 604 if ( (day.isSame(start, 'day') || day.isAfter(start, 'day')) 605 && (day.isSame(end, 'day') || day.isBefore(end, 'day')) ) 606 { 607 eventsToday.push( monthEvents[j] ); 608 } 609 } 610 611 if (now.format("YYYY-MM-DD") == day.format("YYYY-MM-DD")) { 612 extraClasses += (" " + this.options.classes.today); 613 properties.isToday = true; 614 } 615 616 if (day.isBefore(now, 'day')) { 617 extraClasses += (" " + this.options.classes.past); 618 } 619 620 if (eventsToday.length) { 621 extraClasses += (" " + this.options.classes.event); 622 } 623 624 if (!this.options.lengthOfTime.days) { 625 if (this._currentIntervalStart.month() > day.month()) { 626 extraClasses += (" " + this.options.classes.adjacentMonth); 627 properties.isAdjacentMonth = true; 628 629 this._currentIntervalStart.year() === day.year() 630 ? extraClasses += (" " + this.options.classes.lastMonth) 631 : extraClasses += (" " + this.options.classes.nextMonth); 632 } 633 else if (this._currentIntervalStart.month() < day.month()) { 634 extraClasses += (" " + this.options.classes.adjacentMonth); 635 properties.isAdjacentMonth = true; 636 637 this._currentIntervalStart.year() === day.year() 638 ? extraClasses += (" " + this.options.classes.nextMonth) 639 : extraClasses += (" " + this.options.classes.lastMonth); 640 } 641 } 642 643 // If there are constraints, we need to add the inactive class to the 644 // days outside of them 645 if (this.options.constraints) { 646 endMoment = moment(this.options.constraints.endDate); 647 startMoment = moment(this.options.constraints.startDate); 648 649 if (this.options.constraints.startDate && day.isBefore(startMoment)) { 650 extraClasses += (" " + this.options.classes.inactive); 651 properties.isInactive = true; 652 } 653 654 if (this.options.constraints.endDate && day.isAfter(endMoment)) { 655 extraClasses += (" " + this.options.classes.inactive); 656 properties.isInactive = true; 657 } 658 } 659 660 // Validate moment date 661 if (!day.isValid() && day.hasOwnProperty('_d') && day._d != undefined) { 662 day = moment(day._d); 663 } 664 665 // Check whether the day is "selected" 666 selectedMoment = moment(this.options.selectedDate); 667 668 if (this.options.selectedDate && day.isSame(selectedMoment, 'day')) { 669 extraClasses += (" " + this.options.classes.selected); 670 } 671 672 // We're moving away from using IDs in favor of classes, since when 673 // using multiple calendars on a page we are technically violating the 674 // uniqueness of IDs. 675 extraClasses += " calendar-day-" + day.format("YYYY-MM-DD"); 676 // Day of week 677 extraClasses += " calendar-dow-" + day.weekday(); 678 679 return this.calendarDay({ 680 date: day, 681 day: day.date(), 682 events: eventsToday, 683 properties: properties, 684 classes: this.options.targets.day + extraClasses 685 }); 686 }; 687 688 Clndr.prototype.render = function () { 689 // Get rid of the previous set of calendar parts. This should handle garbage 690 // collection according to jQuery's docs: 691 // http://api.jquery.com/empty/ 692 // To avoid memory leaks, jQuery removes other constructs such as 693 // data and event handlers from the child elements before removing 694 // the elements themselves. 695 var data = {}, 696 end = null, 697 start = null, 698 oneYearFromEnd = this.intervalEnd.clone().add(1, 'years'), 699 oneYearAgo = this.intervalStart.clone().subtract(1, 'years'), 700 days, months, currentMonth, eventsThisInterval, 701 numberOfRows; 702 this.calendarContainer.empty(); 703 704 if (this.options.lengthOfTime.days) { 705 days = this.createDaysObject( 706 this.intervalStart.clone(), 707 this.intervalEnd.clone()); 708 data = { 709 days: days, 710 months: [], 711 year: null, 712 month: null, 713 eventsLastMonth: [], 714 eventsNextMonth: [], 715 eventsThisMonth: [], 716 extras: this.options.extras, 717 daysOfTheWeek: this.daysOfTheWeek, 718 intervalEnd: this.intervalEnd.clone(), 719 numberOfRows: Math.ceil(days.length / 7), 720 intervalStart: this.intervalStart.clone(), 721 eventsThisInterval: this.eventsThisInterval 722 }; 723 } 724 else if (this.options.lengthOfTime.months) { 725 months = []; 726 numberOfRows = 0; 727 eventsThisInterval = []; 728 729 for (i = 0; i < this.options.lengthOfTime.months; i++) { 730 var currentIntervalStart = this.intervalStart 731 .clone() 732 .add(i, 'months'); 733 var currentIntervalEnd = currentIntervalStart 734 .clone() 735 .endOf('month'); 736 var days = this.createDaysObject( 737 currentIntervalStart, 738 currentIntervalEnd); 739 // Save events processed for each month into a master array of 740 // events for this interval 741 eventsThisInterval.push(this.eventsThisInterval); 742 months.push({ 743 days: days, 744 month: currentIntervalStart 745 }); 746 } 747 748 // Get the total number of rows across all months 749 for (i in months) { 750 numberOfRows += Math.ceil(months[i].days.length / 7); 751 } 752 753 data = { 754 days: [], 755 year: null, 756 month: null, 757 months: months, 758 eventsThisMonth: [], 759 numberOfRows: numberOfRows, 760 extras: this.options.extras, 761 intervalEnd: this.intervalEnd, 762 intervalStart: this.intervalStart, 763 daysOfTheWeek: this.daysOfTheWeek, 764 eventsLastMonth: this.eventsLastMonth, 765 eventsNextMonth: this.eventsNextMonth, 766 eventsThisInterval: eventsThisInterval, 767 }; 768 } 769 else { 770 // Get an array of days and blank spaces 771 days = this.createDaysObject( 772 this.month.clone().startOf('month'), 773 this.month.clone().endOf('month')); 774 // This is to prevent a scope/naming issue between this.month and 775 // data.month 776 currentMonth = this.month; 777 778 data = { 779 days: days, 780 months: [], 781 intervalEnd: null, 782 intervalStart: null, 783 year: this.month.year(), 784 eventsThisInterval: null, 785 extras: this.options.extras, 786 month: this.month.format('MMMM'), 787 daysOfTheWeek: this.daysOfTheWeek, 788 eventsLastMonth: this.eventsLastMonth, 789 eventsNextMonth: this.eventsNextMonth, 790 numberOfRows: Math.ceil(days.length / 7), 791 eventsThisMonth: this.eventsThisInterval 792 }; 793 } 794 795 // Render the calendar with the data above & bind events to its 796 // elements 797 if ( !this.options.render) { 798 this.calendarContainer.html( 799 this.compiledClndrTemplate(data)); 800 } else { 801 this.calendarContainer.html( 802 this.options.render.apply(this, [data])); 803 } 804 805 // If there are constraints, we need to add the 'inactive' class to 806 // the controls. 807 if (this.options.constraints) { 808 // In the interest of clarity we're just going to remove all 809 // inactive classes and re-apply them each render. 810 for (var target in this.options.targets) { 811 if (target != this.options.targets.day) { 812 this.element.find('.' + this.options.targets[target]) 813 .toggleClass( 814 this.options.classes.inactive, 815 false); 816 } 817 } 818 819 // Just like the classes we'll set this internal state to true and 820 // handle the disabling below.
821 for (var i in this.constraints) { 822 this.constraints[i] = true; 823 } 824 825 if (this.options.constraints.startDate) { 826 start = moment(this.options.constraints.startDate); 827 } 828 829 if (this.options.constraints.endDate) { 830 end = moment(this.options.constraints.endDate); 831 } 832 833 // Deal with the month controls first. Do we have room to go back? 834 if (start 835 && (start.isAfter(this.intervalStart) 836 || start.isSame(this.intervalStart, 'day'))) 837 { 838 this.element.find('.' + this.options.targets.previousButton) 839 .toggleClass(this.options.classes.inactive, true); 840 this.constraints.previous = !this.constraints.previous; 841 } 842 843 // Do we have room to go forward? 844 if (end 845 && (end.isBefore(this.intervalEnd) 846 || end.isSame(this.intervalEnd, 'day'))) 847 { 848 this.element.find('.' + this.options.targets.nextButton) 849 .toggleClass(this.options.classes.inactive, true); 850 this.constraints.next = !this.constraints.next; 851 } 852 853 // What's last year looking like? 854 if (start && start.isAfter(oneYearAgo)) { 855 this.element.find('.' + this.options.targets.previousYearButton) 856 .toggleClass(this.options.classes.inactive, true); 857 this.constraints.previousYear = !this.constraints.previousYear; 858 } 859 860 // How about next year? 861 if (end && end.isBefore(oneYearFromEnd)) { 862 this.element.find('.' + this.options.targets.nextYearButton) 863 .toggleClass(this.options.classes.inactive, true); 864 this.constraints.nextYear = !this.constraints.nextYear; 865 } 866 867 // Today? We could put this in init(), but we want to support the 868 // user changing the constraints on a living instance. 869 if ( (start && start.isAfter( moment(), 'month' )) 870 || (end && end.isBefore( moment(), 'month' )) ) 871 { 872 this.element.find('.' + this.options.targets.today) 873 .toggleClass(this.options.classes.inactive, true); 874 this.constraints.today = !this.constraints.today; 875 } 876 } 877 878 if (this.options.doneRendering) { 879 this.options.doneRendering.apply(this, []); 880 } 881 }; 882 883 Clndr.prototype.bindEvents = function () { 884 var data = {}, 885 self = this, 886 $container = $(this.element), 887 targets = this.options.targets, 888 classes = self.options.classes, 889 eventType = (this.options.useTouchEvents === true) 890 ? 'touchstart' 891 : 'click', 892 eventName = eventType + '.clndr'; 893 894 // Make sure we don't already have events 895 $container 896 .off(eventName, '.' + targets.day) 897 .off(eventName, '.' + targets.empty) 898 .off(eventName, '.' + targets.nextButton) 899 .off(eventName, '.' + targets.todayButton) 900 .off(eventName, '.' + targets.previousButton) 901 .off(eventName, '.' + targets.nextYearButton) 902 .off(eventName, '.' + targets.previousYearButton); 903 904 // Target the day elements and give them click events 905 $container.on(eventName, '.' + targets.day, function (event) { 906 var target, 907 $currentTarget = $(event.currentTarget); 908 909 if (self.options.clickEvents.click) { 910 target = self.buildTargetObject(event.currentTarget, true); 911 self.options.clickEvents.click.apply(self, [target]); 912 } 913 914 // If adjacentDaysChangeMonth is on, we need to change the 915 // month here. 916 if (self.options.adjacentDaysChangeMonth) { 917 if ($currentTarget.is('.' + classes.lastMonth)) { 918 self.backActionWithContext(self); 919 } 920 else if ($currentTarget.is('.' + classes.nextMonth)) { 921 self.forwardActionWithContext(self); 922 } 923 } 924 925 // if trackSelectedDate is on, we need to handle click on a new day 926 if (self.options.trackSelectedDate) { 927 if (self.options.ignoreInactiveDaysInSelection
928 && $currentTarget.hasClass(classes.inactive)) 929 { 930 return; 931 } 932 933 // Remember new selected date 934 self.options.selectedDate = 935 self.getTargetDateString(event.currentTarget); 936 // Handle "selected" class. This handles more complex templates 937 // that may have the selected elements nested. 938 $container.find('.' + classes.selected) 939 .removeClass(classes.selected); 940 $currentTarget.addClass(classes.selected); 941 } 942 }); 943 944 // Target the empty calendar boxes as well 945 $container.on(eventName, '.' + targets.empty, function (event) { 946 var target, 947 $eventTarget = $(event.currentTarget); 948 949 if (self.options.clickEvents.click) { 950 target = self.buildTargetObject(event.currentTarget, false); 951 self.options.clickEvents.click.apply(self, [target]); 952 } 953 954 if (self.options.adjacentDaysChangeMonth) { 955 if ($eventTarget.is('.' + classes.lastMonth)) { 956 self.backActionWithContext(self); 957 } 958 else if ($eventTarget.is('.' + classes.nextMonth)) { 959 self.forwardActionWithContext(self); 960 } 961 } 962 }); 963 964 // Bind the previous, next and today buttons. We pass the current 965 // context along with the event so that it can update this instance. 966 data = { 967 context: this 968 }; 969 970 $container 971 .on(eventName, '.' + targets.todayButton, data, this.todayAction) 972 .on(eventName, '.' + targets.nextButton, data, this.forwardAction) 973 .on(eventName, '.' + targets.previousButton, data, this.backAction) 974 .on(eventName, '.' + targets.nextYearButton, data, this.nextYearAction) 975 .on(eventName, '.' + targets.previousYearButton, data, this.previousYearAction); 976 }; 977 978 /** 979 * If the user provided a click callback we'd like to give them something 980 * nice to work with. buildTargetObject takes the DOM element that was 981 * clicked and returns an object with the DOM element, events, and the date 982 * (if the latter two exist). Currently it is based on the id, however it'd 983 * be nice to use a data- attribute in the future. 984 */ 985 Clndr.prototype.buildTargetObject = function (currentTarget, targetWasDay) { 986 // This is our default target object, assuming we hit an empty day 987 // with no events. 988 var target = { 989 date: null, 990 events: [], 991 element: currentTarget 992 }; 993 var dateString, filterFn; 994 995 // Did we click on a day or just an empty box? 996 if (targetWasDay) { 997 dateString = this.getTargetDateString(currentTarget); 998 target.date = (dateString) 999 ? moment(dateString) 1000 : null; 1001 1002 // Do we have events? 1003 if (this.options.events) { 1004 // Are any of the events happening today? 1005 if (this.options.multiDayEvents) { 1006 filterFn = function () { 1007 var isSameStart = target.date.isSame( 1008 this._clndrStartDateObject, 1009 'day'); 1010 var isAfterStart = target.date.isAfter( 1011 this._clndrStartDateObject, 1012 'day'); 1013 var isSameEnd = target.date.isSame( 1014 this._clndrEndDateObject, 1015 'day'); 1016 var isBeforeEnd = target.date.isBefore( 1017 this._clndrEndDateObject, 1018 'day'); 1019 return (isSameStart || isAfterStart) 1020 && (isSameEnd || isBeforeEnd); 1021 }; 1022 } 1023 else { 1024 filterFn = function () { 1025 var startString = this._clndrStartDateObject 1026 .format('YYYY-MM-DD'); 1027 return startString == dateString; 1028 }; 1029 } 1030 1031 // Filter the dates down to the ones that match. 1032 target.events = $.makeArray( 1033 $(this.options.events).filter(filterFn)); 1034 } 1035 } 1036 1037 return target; 1038 }; 1039 1040 /** 1041 * Get moment date object of the date associated with the given target. 1042 * This method is meant to be called on ".day" elements. 1043 */ 1044 Clndr.prototype.getTargetDateString = function (target) { 1045 // Our identifier is in the list of classNames. Find it! 1046 var classNameIndex = target.className.indexOf('calendar-day-'); 1047 1048 if (classNameIndex !== -1) { 1049 // Our unique identifier is always 23 characters long. 1050 // If this feels a little wonky, that's probably because it is.
1051 // Open to suggestions on how to improve this guy. 1052 return target.className.substring( 1053 classNameIndex + 13, 1054 classNameIndex + 23); 1055 } 1056 1057 return null; 1058 }; 1059 1060 /** 1061 * Triggers any applicable events given a change in the calendar's start 1062 * and end dates. ctx contains the current (changed) start and end date, 1063 * orig contains the original start and end dates. 1064 */ 1065 Clndr.prototype.triggerEvents = function (ctx, orig) { 1066 var timeOpt = ctx.options.lengthOfTime, 1067 eventsOpt = ctx.options.clickEvents, 1068 newInt = { 1069 end: ctx.intervalEnd, 1070 start: ctx.intervalStart 1071 }, 1072 intervalArg = [ 1073 moment(ctx.intervalStart), 1074 moment(ctx.intervalEnd) 1075 ], 1076 monthArg = [moment(ctx.month)], 1077 nextYear, prevYear, yearChanged, 1078 nextMonth, prevMonth, monthChanged, 1079 nextInterval, prevInterval, intervalChanged; 1080 1081 // We want to determine if any of the change conditions have been 1082 // hit and then trigger our events based off that. 1083 nextMonth = newInt.start.isAfter( orig.start ) 1084 && (Math.abs(newInt.start.month() - orig.start.month()) == 1 1085 || orig.start.month() === 11 && newInt.start.month() === 0); 1086 prevMonth = newInt.start.isBefore( orig.start ) 1087 && (Math.abs(orig.start.month() - newInt.start.month()) == 1 1088 || orig.start.month() === 0 && newInt.start.month() === 11); 1089 monthChanged = newInt.start.month() !== orig.start.month() 1090 || newInt.start.year() !== orig.start.year(); 1091 nextYear = newInt.start.year() - orig.start.year() === 1 1092 || newInt.end.year() - orig.end.year() === 1; 1093 prevYear = orig.start.year() - newInt.start.year() === 1 1094 || orig.end.year() - newInt.end.year() === 1; 1095 yearChanged = newInt.start.year() !== orig.start.year(); 1096 1097 // Only configs with a time period will get the interval change event 1098 if (timeOpt.days || timeOpt.months) { 1099 nextInterval = newInt.start.isAfter(orig.start); 1100 prevInterval = newInt.start.isBefore(orig.start); 1101 intervalChanged = nextInterval || prevInterval; 1102 1103 if (nextInterval && eventsOpt.nextInterval) { 1104 eventsOpt.nextInterval.apply(ctx, intervalArg); 1105 } 1106 1107 if (prevInterval && eventsOpt.previousInterval) { 1108 eventsOpt.previousInterval.apply(ctx, intervalArg); 1109 } 1110 1111 if (intervalChanged && eventsOpt.onIntervalChange) { 1112 eventsOpt.onIntervalChange.apply(ctx, intervalArg); 1113 } 1114 } 1115 // @V2-todo see https://github.com/kylestetz/CLNDR/issues/225 1116 else { 1117 if (nextMonth && eventsOpt.nextMonth) { 1118 eventsOpt.nextMonth.apply(ctx, monthArg); 1119 } 1120 1121 if (prevMonth && eventsOpt.previousMonth) { 1122 eventsOpt.previousMonth.apply(ctx, monthArg); 1123 } 1124 1125 if (monthChanged && eventsOpt.onMonthChange) { 1126 eventsOpt.onMonthChange.apply(ctx, monthArg); 1127 } 1128 1129 if (nextYear && eventsOpt.nextYear) { 1130 eventsOpt.nextYear.apply(ctx, monthArg); 1131 } 1132 1133 if (prevYear && eventsOpt.previousYear) { 1134 eventsOpt.previousYear.apply(ctx, monthArg); 1135 } 1136 1137 if (yearChanged && eventsOpt.onYearChange) { 1138 eventsOpt.onYearChange.apply(ctx, monthArg); 1139 } 1140 } 1141 }; 1142 1143 /** 1144 * Main action to go backward one period. Other methods call these, like 1145 * backAction which proxies jQuery events, and backActionWithContext which 1146 * is an internal method that this library uses. 1147 */ 1148 Clndr.prototype.back = function (options /*, ctx */) { 1149 var yearChanged = null, 1150 ctx = (arguments.length > 1) 1151 ? arguments[ 1 ] 1152 : this, 1153 timeOpt = ctx.options.lengthOfTime, 1154 defaults = { 1155 withCallbacks: false 1156 }, 1157 orig = { 1158 end: ctx.intervalEnd.clone(), 1159 start: ctx.intervalStart.clone() 1160 }; 1161 1162 // Extend any options 1163 options = $.extend(true, {}, defaults, options); 1164 1165 // Before we do anything, check if any constraints are limiting this 1166 if (!ctx.constraints.previous) { 1167 return ctx; 1168 } 1169 1170 if (!timeOpt.days) { 1171 // Shift the interval by a month (or several months) 1172 ctx.intervalStart 1173 .subtract(timeOpt.interval, 'months') 1174 .startOf('month'); 1175 ctx.intervalEnd = ctx.intervalStart.clone() 1176 .add(timeOpt.months || timeOpt.interval, 'months') 1177 .subtract(1, 'days') 1178 .endOf('month'); 1179 ctx.month = ctx.intervalStart.clone(); 1180 } 1181 else { 1182 // Shift the interval in days 1183 ctx.intervalStart 1184 .subtract(timeOpt.interval, 'days') 1185 .startOf('day'); 1186 ctx.intervalEnd = ctx.intervalStart.clone() 1187 .add(timeOpt.days - 1, 'days') 1188 .endOf('day'); 1189 // @V2-todo Useless, but consistent with API 1190 ctx.month = ctx.intervalStart.clone(); 1191 } 1192 1193 ctx.render(); 1194 1195 if (options.withCallbacks) { 1196 ctx.triggerEvents(ctx, orig); 1197 } 1198 1199 return ctx; 1200 }; 1201 1202 Clndr.prototype.backAction = function (event) { 1203 var ctx = event.data.context; 1204 ctx.backActionWithContext(ctx); 1205 }; 1206 1207 Clndr.prototype.backActionWithContext = function (ctx) { 1208 ctx.back({ 1209 withCallbacks: true 1210 }, ctx); 1211 }; 1212 1213 Clndr.prototype.previous = function (options) { 1214 // Alias 1215 return this.back(options); 1216 }; 1217 1218 /** 1219 * Main action to go forward one period. Other methods call these, like 1220 * forwardAction which proxies jQuery events, and backActionWithContext 1221 * which is an internal method that this library uses. 1222 */ 1223 Clndr.prototype.forward = function (options /*, ctx */) { 1224 var ctx = (arguments.length > 1) 1225 ? arguments[1] 1226 : this, 1227 timeOpt = ctx.options.lengthOfTime, 1228 defaults = { 1229 withCallbacks: false 1230 }, 1231 orig = { 1232 end: ctx.intervalEnd.clone(), 1233 start: ctx.intervalStart.clone() 1234 }; 1235 1236 // Extend any options 1237 options = $.extend(true, {}, defaults, options); 1238 1239 // Before we do anything, check if any constraints are limiting this 1240 if (!ctx.constraints.next) { 1241 return ctx; 1242 } 1243 1244 if (ctx.options.lengthOfTime.days) { 1245 // Shift the interval in days 1246 ctx.intervalStart 1247 .add(timeOpt.interval, 'days') 1248 .startOf('day'); 1249 ctx.intervalEnd = ctx.intervalStart.clone() 1250 .add(timeOpt.days - 1, 'days') 1251 .endOf('day'); 1252 // @V2-todo Useless, but consistent with API 1253 ctx.month = ctx.intervalStart.clone(); 1254 } 1255 else { 1256 // Shift the interval by a month (or several months) 1257 ctx.intervalStart 1258 .add(timeOpt.interval, 'months') 1259 .startOf('month'); 1260 ctx.intervalEnd = ctx.intervalStart.clone() 1261 .add(timeOpt.months || timeOpt.interval, 'months') 1262 .subtract(1, 'days') 1263 .endOf('month'); 1264 ctx.month = ctx.intervalStart.clone(); 1265 } 1266 1267 ctx.render(); 1268 1269 if (options.withCallbacks) { 1270 ctx.triggerEvents(ctx, orig); 1271 } 1272 1273 return ctx; 1274 }; 1275 1276 Clndr.prototype.forwardAction = function (event) { 1277 var ctx = event.data.context; 1278 ctx.forwardActionWithContext(ctx); 1279 }; 1280 1281 Clndr.prototype.forwardActionWithContext = function (ctx) { 1282 ctx.forward({ 1283 withCallbacks: true 1284 }, ctx); 1285 }; 1286 1287 Clndr.prototype.next = function (options) { 1288 // Alias 1289 return this.forward(options); 1290 }; 1291 1292 /** 1293 * Main action to go back one year. 1294 */ 1295 Clndr.prototype.previousYear = function (options /*, ctx */) { 1296 var ctx = (arguments.length > 1) 1297 ? arguments[1] 1298 : this, 1299 defaults = { 1300 withCallbacks: false 1301 }, 1302 orig = { 1303 end: ctx.intervalEnd.clone(), 1304 start: ctx.intervalStart.clone() 1305 }; 1306 1307 // Extend any options 1308 options = $.extend(true, {}, defaults, options); 1309 1310 // Before we do anything, check if any constraints are limiting this 1311 if (!ctx.constraints.previousYear) { 1312 return ctx; 1313 } 1314 1315 ctx.month.subtract(1, 'year'); 1316 ctx.intervalStart.subtract(1, 'year'); 1317 ctx.intervalEnd.subtract(1, 'year'); 1318 ctx.render(); 1319 1320 if (options.withCallbacks) { 1321 ctx.triggerEvents(ctx, orig); 1322 } 1323 1324 return ctx; 1325 }; 1326 1327 Clndr.prototype.previousYearAction = function (event) { 1328 var ctx = event.data.context; 1329 ctx.previousYear({ 1330 withCallbacks: true 1331 }, ctx); 1332 }; 1333 1334 /** 1335 * Main action to go forward one year. 1336 */ 1337 Clndr.prototype.nextYear = function (options /*, ctx */) { 1338 var ctx = (arguments.length > 1) 1339 ? arguments[1] 1340 : this, 1341 defaults = { 1342 withCallbacks: false 1343 }, 1344 orig = { 1345 end: ctx.intervalEnd.clone(), 1346 start: ctx.intervalStart.clone() 1347 }; 1348 1349 // Extend any options 1350 options = $.extend(true, {}, defaults, options); 1351 1352 // Before we do anything, check if any constraints are limiting this 1353 if (!ctx.constraints.nextYear) { 1354 return ctx; 1355 } 1356 1357 ctx.month.add(1, 'year'); 1358 ctx.intervalStart.add(1, 'year'); 1359 ctx.intervalEnd.add(1, 'year'); 1360 ctx.render(); 1361 1362 if (options.withCallbacks) { 1363 ctx.triggerEvents(ctx, orig); 1364 } 1365 1366 return ctx; 1367 }; 1368 1369 Clndr.prototype.nextYearAction = function (event) { 1370 var ctx = event.data.context; 1371 ctx.nextYear({ 1372 withCallbacks: true 1373 }, ctx); 1374 }; 1375 1376 Clndr.prototype.today = function (options /*, ctx */) { 1377 var ctx = (arguments.length > 1) 1378 ? arguments[1] 1379 : this, 1380 timeOpt = ctx.options.lengthOfTime, 1381 defaults = { 1382 withCallbacks: false 1383 }, 1384 orig = { 1385 end: ctx.intervalEnd.clone(), 1386 start: ctx.intervalStart.clone() 1387 }; 1388 1389 // Extend any options 1390 options = $.extend(true, {}, defaults, options); 1391 // @V2-todo Only used for legacy month view 1392 ctx.month = moment().startOf('month'); 1393 1394 if (timeOpt.days) { 1395 // If there was a startDate specified, we should figure out what 1396 // the weekday is and use that as the starting point of our 1397 // interval. If not, go to today.weekday(0). 1398 if (timeOpt.startDate) { 1399 ctx.intervalStart = moment() 1400 .weekday(timeOpt.startDate.weekday()) 1401 .startOf('day'); 1402 } else { 1403 ctx.intervalStart = moment().weekday(0).startOf('day'); 1404 } 1405 1406 ctx.intervalEnd = ctx.intervalStart.clone() 1407 .add(timeOpt.days - 1, 'days') 1408 .endOf('day'); 1409 } 1410 else { 1411 // Set the intervalStart to this month. 1412 ctx.intervalStart = moment().startOf('month'); 1413 ctx.intervalEnd = ctx.intervalStart.clone() 1414 .add(timeOpt.months || timeOpt.interval, 'months') 1415 .subtract(1, 'days') 1416 .endOf('month'); 1417 } 1418 1419 // No need to re-render if we didn't change months. 1420 if (!ctx.intervalStart.isSame(orig.start) 1421 || !ctx.intervalEnd.isSame(orig.end)) 1422 { 1423 ctx.render(); 1424 } 1425 1426 // Fire the today event handler regardless of any change 1427 if (options.withCallbacks) { 1428 if (ctx.options.clickEvents.today) { 1429 ctx.options.clickEvents.today.apply(ctx, [moment(ctx.month)]); 1430 } 1431 1432 ctx.triggerEvents(ctx, orig); 1433 } 1434 }; 1435 1436 Clndr.prototype.todayAction = function (event) { 1437 var ctx = event.data.context; 1438 ctx.today({ 1439 withCallbacks: true 1440 }, ctx); 1441 }; 1442 1443 /** 1444 * Changes the month. Accepts 0-11 or a full/partial month name e.g. "Jan", 1445 * "February", "Mar", etc. 1446 */ 1447 Clndr.prototype.setMonth = function (newMonth, options) { 1448 var timeOpt = this.options.lengthOfTime, 1449 orig = { 1450 end: this.intervalEnd.clone(), 1451 start: this.intervalStart.clone() 1452 }; 1453 1454 if (timeOpt.days || timeOpt.months) { 1455 console.log( 1456 'You are using a custom date interval. Use ' + 1457 'Clndr.setIntervalStart(startDate) instead.'); 1458 return this; 1459 } 1460 1461 this.month.month(newMonth); 1462 this.intervalStart = this.month.clone().startOf('month'); 1463 this.intervalEnd = this.intervalStart.clone().endOf('month'); 1464 this.render(); 1465 1466 if (options && options.withCallbacks) { 1467 this.triggerEvents(this, orig); 1468 } 1469 1470 return this; 1471 }; 1472 1473 Clndr.prototype.setYear = function (newYear, options) { 1474 var orig = { 1475 end: this.intervalEnd.clone(), 1476 start: this.intervalStart.clone() 1477 }; 1478 1479 this.month.year(newYear); 1480 this.intervalEnd.year(newYear); 1481 this.intervalStart.year(newYear); 1482 this.render(); 1483 1484 if (options && options.withCallbacks) { 1485 this.triggerEvents(this, orig); 1486 } 1487 1488 return this; 1489 }; 1490 1491 /** 1492 * Sets the start of the time period according to newDate. newDate can be 1493 * a string or a moment object. 1494 */ 1495 Clndr.prototype.setIntervalStart = function (newDate, options) { 1496 var timeOpt = this.options.lengthOfTime, 1497 orig = { 1498 end: this.intervalEnd.clone(), 1499 start: this.intervalStart.clone() 1500 }; 1501 1502 if (!timeOpt.days && !timeOpt.months) { 1503 console.log( 1504 'You are using a custom date interval. Use ' + 1505 'Clndr.setIntervalStart(startDate) instead.'); 1506 return this; 1507 } 1508 1509 if (timeOpt.days) { 1510 this.intervalStart = moment(newDate).startOf('day'); 1511 this.intervalEnd = this.intervalStart.clone() 1512 .add(timeOpt - 1, 'days') 1513 .endOf('day'); 1514 } else { 1515 this.intervalStart = moment(newDate).startOf('month'); 1516 this.intervalEnd = this.intervalStart.clone() 1517 .add(timeOpt.months || timeOpt.interval, 'months') 1518 .subtract(1, 'days') 1519 .endOf('month'); 1520 } 1521 1522 this.month = this.intervalStart.clone(); 1523 this.render(); 1524 1525 if (options && options.withCallbacks) { 1526 this.triggerEvents(this, orig); 1527 } 1528 1529 return this; 1530 }; 1531 1532 /** 1533 * Overwrites events in the calendar and triggers a render. 1534 */ 1535 Clndr.prototype.setEvents = function (events) { 1536 // Go through each event and add a moment object 1537 if (this.options.multiDayEvents) { 1538 this.options.events = this.addMultiDayMomentObjectsToEvents(events); 1539 } else { 1540 this.options.events = this.addMomentObjectToEvents(events); 1541 } 1542 1543 this.render(); 1544 return this; 1545 }; 1546 1547 /** 1548 * Adds additional events to the calendar and triggers a render. 1549 */
1550 Clndr.prototype.addEvents = function (events /*, reRender*/) { 1551 var reRender = (arguments.length > 1) 1552 ? arguments[1] 1553 : true; 1554 1555 // Go through each event and add a moment object 1556 if (this.options.multiDayEvents) { 1557 this.options.events = $.merge( 1558 this.options.events, 1559 this.addMultiDayMomentObjectsToEvents(events)); 1560 } else { 1561 this.options.events = $.merge( 1562 this.options.events, 1563 this.addMomentObjectToEvents(events)); 1564 } 1565 1566 if (reRender) { 1567 this.render(); 1568 } 1569 1570 return this; 1571 }; 1572 1573 /** 1574 * Passes all events through a matching function. Any that pass a truth 1575 * test will be removed from the calendar's events. This triggers a render. 1576 */ 1577 Clndr.prototype.removeEvents = function (matchingFn) { 1578 for (var i = this.options.events.length - 1; i >= 0; i--) { 1579 if (matchingFn(this.options.events[i]) == true) { 1580 this.options.events.splice(i, 1); 1581 } 1582 } 1583 1584 this.render(); 1585 return this; 1586 }; 1587 1588 Clndr.prototype.addMomentObjectToEvents = function (events) { 1589 var i = 0, 1590 self = this; 1591 1592 for (i; i < events.length; i++) { 1593 // Add the date as both start and end, since it's a single-day 1594 // event by default 1595 events[i]._clndrStartDateObject = 1596 moment(events[i][self.options.dateParameter]); 1597 events[i]._clndrEndDateObject = 1598 moment(events[i][self.options.dateParameter]); 1599 } 1600 1601 return events; 1602 }; 1603 1604 Clndr.prototype.addMultiDayMomentObjectsToEvents = function (events) { 1605 var i = 0, 1606 self = this, 1607 multiEvents = self.options.multiDayEvents; 1608 1609 for (i; i < events.length; i++) { 1610 var end = events[i][multiEvents.endDate], 1611 start = events[i][multiEvents.startDate]; 1612 // If we don't find the startDate OR endDate fields, look for 1613 // singleDay 1614 if (!end && !start) { 1615 events[i]._clndrEndDateObject = 1616 moment(events[i][multiEvents.singleDay]); 1617 events[i]._clndrStartDateObject = 1618 moment(events[i][multiEvents.singleDay]); 1619 } 1620 // Otherwise use startDate and endDate, or whichever one is present 1621 else { 1622 events[i]._clndrEndDateObject = moment(end || start); 1623 events[i]._clndrStartDateObject = moment(start || end); 1624 } 1625 } 1626 1627 return events; 1628 }; 1629 1630 Clndr.prototype.calendarDay = function (options) { 1631 var defaults = { 1632 day: "", 1633 date: null, 1634 events: [], 1635 classes: this.options.targets.empty 1636 }; 1637 1638 return $.extend({}, defaults, options); 1639 }; 1640 1641 Clndr.prototype.destroy = function () { 1642 var $container = $(this.calendarContainer); 1643 $container.parent().data('plugin_clndr', null); 1644 this.options = defaults; 1645 $container.empty().remove(); 1646 this.element = null; 1647 }; 1648 1649 $.fn.clndr = function (options) { 1650 var clndrInstance; 1651 1652 if (this.length > 1) { 1653 throw new Error( 1654 "CLNDR does not support multiple elements yet. Make sure " + 1655 "your clndr selector returns only one element."); 1656 } 1657 1658 if (!this.length) { 1659 throw new Error( 1660 "CLNDR cannot be instantiated on an empty selector."); 1661 } 1662 1663 if (!this.data('plugin_clndr')) { 1664 clndrInstance = new Clndr(this, options); 1665 this.data('plugin_clndr', clndrInstance); 1666 return clndrInstance; 1667 } 1668 1669 return this.data('plugin_clndr'); 1670 }; 1671}));
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.