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-previous-button'><</div>" + 45 "<div class='clndr-next-button'>></div>" + 46 "<div class='month no_mobile clndr-previous-button regular-btn' ><%= monthP %> <%= year %></div>" + 47 "<div class='month active'><%= month %> <%= year %></div>" + 48 "<div class='month no_mobile clndr-next-button regular-btn' ><%= monthN %> <%= year %></div>" + 49 "</div>" + 50 "<table class='clndr-table' border='0' cellspacing='0' cellpadding='0'>" + 51 "<thead>" + 52 "<tr class='header-days'>" + 53 "<% for(var i = 0; i < daysOfTheWeek.length; i++) { %>" + 54 "<td class='header-day'><%= daysOfTheWeek[i] %></td>" + 55 "<% } %>" + 56 "</tr>" + 57 "</thead>" + 58 "<tbody>" + 59 "<% for(var i = 0; i < numberOfRows; i++){ %>" + 60 "<tr>" + 61 "<% for(var j = 0; j < 7; j++){ %>" + 62 "<% var d = j + i * 7; %>" + 63 "<td class='<%= days[d].classes %>'>" + 64 "<div class='day-contents'><%= days[d].day %></div>" + 65 "</td>" + 66 "<% } %>" + 67 "</tr>" + 68 "<% } %>" + 69 "</tbody>" + 70 "</table>"; 71 72 // Defaults used throughout the application, see docs. 73 var defaults = { 74 events: [], 75 ready: null, 76 extras: null, 77 render: null, 78 moment: null, 79 weekOffset: 0, 80 constraints: null, 81 forceSixRows: null, 82 selectedDate: null, 83 doneRendering: null, 84 daysOfTheWeek: null, 85 multiDayEvents: null, 86 startWithMonth: null, 87 dateParameter: 'date', 88 template: clndrTemplate, 89 showAdjacentMonths: true, 90 trackSelectedDate: false, 91 adjacentDaysChangeMonth: false, 92 ignoreInactiveDaysInSelection: null, 93 lengthOfTime: { 94 days: null, 95 interval: 1, 96 months: null 97 }, 98 clickEvents: { 99 click: null, 100 today: null, 101 nextYear: null, 102 nextMonth: null, 103 nextInterval: null, 104 previousYear: null, 105 onYearChange: null, 106 previousMonth: null, 107 onMonthChange: null, 108 previousInterval: null, 109 onIntervalChange: null 110 }, 111 targets: { 112 day: 'day', 113 empty: 'empty', 114 nextButton: 'clndr-next-button', 115 todayButton: 'clndr-today-button', 116 previousButton: 'clndr-previous-button', 117 nextYearButton: 'clndr-next-year-button', 118 previousYearButton: 'clndr-previous-year-button' 119 }, 120 classes: { 121 past: "past", 122 today: "today", 123 event: "event", 124 inactive: "inactive", 125 selected: "selected", 126 lastMonth: "last-month", 127 nextMonth: "next-month", 128 adjacentMonth: "adjacent-month" 129 }, 130 }; 131 132 /** 133 * The actual plugin constructor. 134 * Parses the events and lengthOfTime options to build a calendar of day 135 * objects containing event information from the events array. 136 */ 137 function Clndr(element, options) { 138 var dayDiff; 139 var constraintEnd; 140 var constraintStart; 141 142 this.element = element; 143
144 // Merge the default options with user-provided options 145 this.options = $.extend(true, {}, defaults, options); 146 147 // Check if moment was passed in as a dependency 148 if (this.options.moment) { 149 moment = this.options.moment; 150 } 151 152 // Boolean values used to log if any contraints are met 153 this.constraints = { 154 next: true, 155 today: true, 156 previous: true, 157 nextYear: true, 158 previousYear: true 159 }; 160 161 // If there are events, we should run them through our 162 // addMomentObjectToEvents function which will add a date object that 163 // we can use to make life easier. This is only necessarywhen events 164 // are provided on instantiation, since our setEvents function uses 165 // addMomentObjectToEvents. 166 if (this.options.events.length) { 167 if (this.options.multiDayEvents) { 168 this.options.events = 169 this.addMultiDayMomentObjectsToEvents(this.options.events); 170 } else { 171 this.options.events = 172 this.addMomentObjectToEvents(this.options.events); 173 } 174 } 175 176 // This used to be a place where we'd figure out the current month, 177 // but since we want to open up support for arbitrary lengths of time 178 // we're going to store the current range in addition to the current 179 // month. 180 if (this.options.lengthOfTime.months || this.options.lengthOfTime.days) { 181 // We want to establish intervalStart and intervalEnd, which will 182 // keep track of our boundaries. Let's look at the possibilities... 183 if (this.options.lengthOfTime.months) { 184 // Gonna go right ahead and annihilate any chance for bugs here 185 this.options.lengthOfTime.days = null; 186 187 // The length is specified in months. Is there a start date? 188 if (this.options.lengthOfTime.startDate) { 189 this.intervalStart = 190 moment(this.options.lengthOfTime.startDate) 191 .startOf('month'); 192 } else if (this.options.startWithMonth) { 193 this.intervalStart = 194 moment(this.options.startWithMonth) 195 .startOf('month'); 196 } else { 197 this.intervalStart = moment().startOf('month'); 198 } 199 200 // Subtract a day so that we are at the end of the interval. We 201 // always want intervalEnd to be inclusive. 202 this.intervalEnd = moment(this.intervalStart) 203 .add(this.options.lengthOfTime.months, 'months') 204 .subtract(1, 'days'); 205 this.month = this.intervalStart.clone(); 206 } 207 else if (this.options.lengthOfTime.days) { 208 // The length is specified in days. Start date? 209 if (this.options.lengthOfTime.startDate) { 210 this.intervalStart = 211 moment(this.options.lengthOfTime.startDate) 212 .startOf('day'); 213 } else { 214 this.intervalStart = moment().weekday(0).startOf('day'); 215 } 216 217 this.intervalEnd = moment(this.intervalStart) 218 .add(this.options.lengthOfTime.days - 1, 'days') 219 .endOf('day'); 220 this.month = this.intervalStart.clone(); 221 } 222 // No length of time specified so we're going to default into using the 223 // current month as the time period. 224 } else { 225 this.month = moment().startOf('month'); 226 this.intervalStart = moment(this.month); 227 this.intervalEnd = moment(this.month).endOf('month'); 228 } 229 230 if (this.options.startWithMonth) { 231 this.month = moment(this.options.startWithMonth).startOf('month'); 232 this.intervalStart = moment(this.month); 233 this.intervalEnd = (this.options.lengthOfTime.days) 234 ? moment(this.month) 235 .add(this.options.lengthOfTime.days - 1, 'days') 236 .endOf('day') 237 : moment(this.month).endOf('month'); 238 } 239 240 // If we've got constraints set, make sure the interval is within them. 241 if (this.options.constraints) { 242 // First check if the startDate exists & is later than now. 243 if (this.options.constraints.startDate) { 244 constraintStart = moment(this.options.constraints.startDate); 245 246 // We need to handle the constraints differently for weekly 247 // calendars vs. monthly calendars. 248 if (this.options.lengthOfTime.days) { 249 if (this.intervalStart.isBefore(constraintStart, 'week')) { 250 this.intervalStart = constraintStart.startOf('week'); 251 } 252
253 // If the new interval period is less than the desired length 254 // of time, or before the starting interval, then correct it. 255 dayDiff = this.intervalStart.diff(this.intervalEnd, 'days'); 256 257 if (dayDiff < this.options.lengthOfTime.days 258 || this.intervalEnd.isBefore(this.intervalStart)) 259 { 260 this.intervalEnd = moment(this.intervalStart) 261 .add(this.options.lengthOfTime.days - 1, 'days') 262 .endOf('day'); 263 this.month = this.intervalStart.clone(); 264 } 265 } 266 else { 267 if (this.intervalStart.isBefore(constraintStart, 'month')) { 268 // Try to preserve the date by moving only the month. 269 this.intervalStart 270 .set('month', constraintStart.month()) 271 .set('year', constraintStart.year()); 272 this.month 273 .set('month', constraintStart.month()) 274 .set('year', constraintStart.year()); 275 } 276 277 // Check if the ending interval is earlier than now. 278 if (this.intervalEnd.isBefore(constraintStart, 'month')) { 279 this.intervalEnd 280 .set('month', constraintStart.month()) 281 .set('year', constraintStart.year()); 282 } 283 } 284 } 285 286 // Make sure the intervalEnd is before the endDate. 287 if (this.options.constraints.endDate) { 288 constraintEnd = moment(this.options.constraints.endDate); 289 290 // We need to handle the constraints differently for weekly 291 // calendars vs. monthly calendars. 292 if (this.options.lengthOfTime.days) { 293 // The starting interval is after our ending constraint. 294 if (this.intervalStart.isAfter(constraintEnd, 'week')) { 295 this.intervalStart = moment(constraintEnd) 296 .endOf('week') 297 .subtract(this.options.lengthOfTime.days - 1, 'days') 298 .startOf('day'); 299 this.intervalEnd = moment(constraintEnd) 300 .endOf('week'); 301 this.month = this.intervalStart.clone(); 302 } 303 } 304 else { 305 if (this.intervalEnd.isAfter(constraintEnd, 'month')) { 306 this.intervalEnd 307 .set('month', constraintEnd.month()) 308 .set('year', constraintEnd.year()); 309 this.month 310 .set('month', constraintEnd.month()) 311 .set('year', constraintEnd.year()); 312 } 313 314 // Check if the starting interval is later than the ending. 315 if (this.intervalStart.isAfter(constraintEnd, 'month')) { 316 this.intervalStart 317 .set('month', constraintEnd.month()) 318 .set('year', constraintEnd.year()); 319 } 320 } 321 } 322 } 323 324 this._defaults = defaults; 325 this._name = pluginName; 326 327 // Some first-time initialization -> day of the week offset, template 328 // compiling, making and storing some elements we'll need later, and 329 // event handling for the controller. 330 this.init(); 331 } 332 333 /** 334 * Calendar initialization. 335 * Sets up the days of the week, the rendering function, binds all of the 336 * events to the rendered calendar, and then stores the node locally. 337 */ 338 Clndr.prototype.init = function () { 339 // Create the days of the week using moment's current language setting 340 this.daysOfTheWeek = this.options.daysOfTheWeek || []; 341 342 if (!this.options.daysOfTheWeek) { 343 this.daysOfTheWeek = []; 344 345 for (var i = 0; i < 7; i++) { 346 this.daysOfTheWeek.push( 347 moment().weekday(i).format('dd').charAt(0)); 348 } 349 } 350 351 // Shuffle the week if there's an offset 352 if (this.options.weekOffset) { 353 this.daysOfTheWeek = this.shiftWeekdayLabels(this.options.weekOffset); 354 } 355 356 // Quick and dirty test to make sure rendering is possible. 357 if (!$.isFunction(this.options.render)) { 358 this.options.render = null; 359 360 if (typeof _ === 'undefined') { 361 throw new Error( 362 "Underscore was not found. Please include underscore.js " + 363 "OR provide a custom render function."); 364 } else { 365 // We're just going ahead and using underscore here if no 366 // render method has been supplied.
367 this.compiledClndrTemplate = _.template(this.options.template); 368 } 369 } 370 371 // Create the parent element that will hold the plugin and save it 372 // for later 373 $(this.element).html("<div class='clndr'></div>"); 374 this.calendarContainer = $('.clndr', this.element); 375 376 // Attach event handlers for clicks on buttons/cells 377 this.bindEvents(); 378 379 // Do a normal render of the calendar template 380 this.render(); 381 382 // If a ready callback has been provided, call it. 383 if (this.options.ready) { 384 this.options.ready.apply(this, []); 385 } 386 }; 387 388 Clndr.prototype.shiftWeekdayLabels = function (offset) { 389 var days = this.daysOfTheWeek; 390 391 for (var i = 0; i < offset; i++) { 392 days.push(days.shift()); 393 } 394 395 return days; 396 }; 397 398 /** 399 * This is where the magic happens. Given a starting date and ending date, 400 * an array of calendarDay objects is constructed that contains appropriate 401 * events and classes depending on the circumstance. 402 */ 403 Clndr.prototype.createDaysObject = function (startDate, endDate) { 404 // This array will hold numbers for the entire grid (even the blank 405 // spaces). 406 var daysArray = [], 407 date = startDate.clone(), 408 lengthOfInterval = endDate.diff(startDate, 'days'), 409 startOfLastMonth, endOfLastMonth, startOfNextMonth, 410 endOfNextMonth, diff, dateIterator; 411 412 // This is a helper object so that days can resolve their classes 413 // correctly. Don't use it for anything please. 414 this._currentIntervalStart = startDate.clone(); 415 416 // Filter the events list (if it exists) to events that are happening 417 // last month, this month and next month (within the current grid view). 418 this.eventsLastMonth = []; 419 this.eventsNextMonth = []; 420 this.eventsThisInterval = []; 421 422 // Event parsing 423 if (this.options.events.length) { 424 // Here are the only two cases where we don't get an event in our 425 // interval: 426 // startDate | endDate | e.start | e.end 427 // e.start | e.end | startDate | endDate 428 this.eventsThisInterval = $(this.options.events).filter( 429 function () { 430 var afterEnd = this._clndrStartDateObject.isAfter(endDate), 431 beforeStart = this._clndrEndDateObject.isBefore(startDate); 432 433 if (beforeStart || afterEnd) { 434 return false; 435 } else { 436 return true; 437 } 438 }).toArray(); 439 440 if (this.options.showAdjacentMonths) { 441 startOfLastMonth = startDate.clone() 442 .subtract(1, 'months') 443 .startOf('month'); 444 endOfLastMonth = startOfLastMonth.clone().endOf('month'); 445 startOfNextMonth = endDate.clone() 446 .add(1, 'months') 447 .startOf('month'); 448 endOfNextMonth = startOfNextMonth.clone().endOf('month'); 449 450 this.eventsLastMonth = $(this.options.events).filter( 451 function () { 452 var beforeStart = this._clndrEndDateObject 453 .isBefore(startOfLastMonth); 454 var afterEnd = this._clndrStartDateObject 455 .isAfter(endOfLastMonth); 456 457 if (beforeStart || afterEnd) { 458 return false; 459 } else { 460 return true; 461 } 462 }).toArray(); 463 464 this.eventsNextMonth = $(this.options.events).filter( 465 function () { 466 var beforeStart = this._clndrEndDateObject 467 .isBefore(startOfNextMonth); 468 var afterEnd = this._clndrStartDateObject 469 .isAfter(endOfNextMonth); 470 471 if (beforeStart || afterEnd) { 472 return false;
473 } else { 474 return true; 475 } 476 }).toArray(); 477 } 478 } 479 480 // If diff is greater than 0, we'll have to fill in last days of the 481 // previous month to account for the empty boxes in the grid. We also 482 // need to take into account the weekOffset parameter. None of this 483 // needs to happen if the interval is being specified in days rather 484 // than months. 485 if (!this.options.lengthOfTime.days) { 486 diff = date.weekday() - this.options.weekOffset; 487 488 if (diff < 0) { 489 diff += 7; 490 } 491 492 if (this.options.showAdjacentMonths) { 493 for (var i = 1; i <= diff; i++) { 494 var day = moment([ 495 startDate.year(), 496 startDate.month(), 497 i 498 ]).subtract(diff, 'days'); 499 daysArray.push( 500 this.createDayObject( 501 day, 502 this.eventsLastMonth 503 )); 504 } 505 } else { 506 for (var i = 0; i < diff; i++) { 507 daysArray.push( 508 this.calendarDay({ 509 classes: this.options.targets.empty + 510 " " + this.options.classes.lastMonth 511 })); 512 } 513 } 514 } 515 516 // Now we push all of the days in the interval 517 dateIterator = startDate.clone(); 518 519 while (dateIterator.isBefore(endDate) || dateIterator.isSame(endDate, 'day')) { 520 daysArray.push( 521 this.createDayObject( 522 dateIterator.clone(), 523 this.eventsThisInterval 524 )); 525 dateIterator.add(1, 'days'); 526 } 527 528 // ...and if there are any trailing blank boxes, fill those in with the 529 // next month first days. Again, we can ignore this if the interval is 530 // specified in days. 531 if (!this.options.lengthOfTime.days) { 532 while (daysArray.length % 7 !== 0) { 533 if (this.options.showAdjacentMonths) { 534 daysArray.push( 535 this.createDayObject( 536 dateIterator.clone(), 537 this.eventsNextMonth 538 )); 539 } else { 540 daysArray.push( 541 this.calendarDay({ 542 classes: this.options.targets.empty + " " + 543 this.options.classes.nextMonth 544 })); 545 } 546 dateIterator.add(1, 'days'); 547 } 548 } 549 550 // If we want to force six rows of calendar, now's our Last Chance to 551 // add another row. If the 42 seems explicit it's because we're 552 // creating a 7-row grid and 6 rows of 7 is always 42! 553 if (this.options.forceSixRows && daysArray.length !== 42) { 554 while (daysArray.length < 42) { 555 if (this.options.showAdjacentMonths) { 556 daysArray.push( 557 this.createDayObject( 558 dateIterator.clone(), 559 this.eventsNextMonth 560 )); 561 dateIterator.add(1, 'days'); 562 } else { 563 daysArray.push( 564 this.calendarDay({ 565 classes: this.options.targets.empty + " " + 566 this.options.classes.nextMonth 567 })); 568 } 569 } 570 } 571 572 return daysArray; 573 }; 574 575 Clndr.prototype.createDayObject = function (day, monthEvents) { 576 var j = 0, 577 self = this, 578 now = moment(), 579 eventsToday = [], 580 extraClasses = "", 581 properties = { 582 isToday: false, 583 isInactive: false,
584 isAdjacentMonth: false 585 }, 586 startMoment, endMoment, selectedMoment; 587 588 // Validate moment date 589 if (!day.isValid() && day.hasOwnProperty('_d') && day._d != undefined) { 590 day = moment(day._d); 591 } 592 593 for (j; j < monthEvents.length; j++) { 594 // Keep in mind that the events here already passed the month/year 595 // test. Now all we have to compare is the moment.date(), which 596 // returns the day of the month. 597 var start = monthEvents[j]._clndrStartDateObject, 598 end = monthEvents[j]._clndrEndDateObject; 599 // If today is the same day as start or is after the start, and 600 // if today is the same day as the end or before the end ... 601 // woohoo semantics! 602 if ( (day.isSame(start, 'day') || day.isAfter(start, 'day')) 603 && (day.isSame(end, 'day') || day.isBefore(end, 'day')) ) 604 { 605 eventsToday.push( monthEvents[j] ); 606 } 607 } 608 609 if (now.format("YYYY-MM-DD") == day.format("YYYY-MM-DD")) { 610 extraClasses += (" " + this.options.classes.today); 611 properties.isToday = true; 612 } 613 614 if (day.isBefore(now, 'day')) { 615 extraClasses += (" " + this.options.classes.past); 616 } 617 618 if (eventsToday.length) { 619 extraClasses += (" " + this.options.classes.event); 620 } 621 622 if (!this.options.lengthOfTime.days) { 623 if (this._currentIntervalStart.month() > day.month()) { 624 extraClasses += (" " + this.options.classes.adjacentMonth); 625 properties.isAdjacentMonth = true; 626 627 this._currentIntervalStart.year() === day.year() 628 ? extraClasses += (" " + this.options.classes.lastMonth) 629 : extraClasses += (" " + this.options.classes.nextMonth); 630 } 631 else if (this._currentIntervalStart.month() < day.month()) { 632 extraClasses += (" " + this.options.classes.adjacentMonth); 633 properties.isAdjacentMonth = true; 634 635 this._currentIntervalStart.year() === day.year() 636 ? extraClasses += (" " + this.options.classes.nextMonth) 637 : extraClasses += (" " + this.options.classes.lastMonth); 638 } 639 } 640 641 // If there are constraints, we need to add the inactive class to the 642 // days outside of them 643 if (this.options.constraints) { 644 endMoment = moment(this.options.constraints.endDate); 645 startMoment = moment(this.options.constraints.startDate); 646 647 if (this.options.constraints.startDate && day.isBefore(startMoment)) { 648 extraClasses += (" " + this.options.classes.inactive); 649 properties.isInactive = true; 650 } 651 652 if (this.options.constraints.endDate && day.isAfter(endMoment)) { 653 extraClasses += (" " + this.options.classes.inactive); 654 properties.isInactive = true; 655 } 656 } 657 658 // Validate moment date 659 if (!day.isValid() && day.hasOwnProperty('_d') && day._d != undefined) { 660 day = moment(day._d); 661 } 662 663 // Check whether the day is "selected" 664 selectedMoment = moment(this.options.selectedDate); 665 666 if (this.options.selectedDate && day.isSame(selectedMoment, 'day')) { 667 extraClasses += (" " + this.options.classes.selected); 668 } 669 670 // We're moving away from using IDs in favor of classes, since when 671 // using multiple calendars on a page we are technically violating the 672 // uniqueness of IDs. 673 extraClasses += " calendar-day-" + day.format("YYYY-MM-DD"); 674 // Day of week 675 extraClasses += " calendar-dow-" + day.weekday(); 676 677 return this.calendarDay({ 678 date: day, 679 day: day.date(), 680 events: eventsToday, 681 properties: properties, 682 classes: this.options.targets.day + extraClasses 683 }); 684 }; 685 686 Clndr.prototype.render = function () { 687 // Get rid of the previous set of calendar parts. This should handle garbage 688 // collection according to jQuery's docs: 689 // http://api.jquery.com/empty/ 690 // To avoid memory leaks, jQuery removes other constructs such as 691 // data and event handlers from the child elements before removing 692 // the elements themselves. 693 var data = {}, 694 end = null, 695 start = null, 696 oneYearFromEnd = this.intervalEnd.clone().add(1, 'years'), 697 oneYearAgo = this.intervalStart.clone().subtract(1, 'years'), 698 days, months, currentMonth, eventsThisInterval, 699 numberOfRows; 700 this.calendarContainer.empty(); 701 702 if (this.options.lengthOfTime.days) { 703 days = this.createDaysObject( 704 this.intervalStart.clone(), 705 this.intervalEnd.clone()); 706 data = { 707 days: days, 708 months: [], 709 year: null, 710 month: null, 711 eventsLastMonth: [], 712 eventsNextMonth: [], 713 eventsThisMonth: [], 714 extras: this.options.extras, 715 daysOfTheWeek: this.daysOfTheWeek, 716 intervalEnd: this.intervalEnd.clone(), 717 numberOfRows: Math.ceil(days.length / 7), 718 intervalStart: this.intervalStart.clone(), 719 eventsThisInterval: this.eventsThisInterval 720 }; 721 } 722 else if (this.options.lengthOfTime.months) { 723 months = []; 724 numberOfRows = 0; 725 eventsThisInterval = []; 726 727 for (i = 0; i < this.options.lengthOfTime.months; i++) { 728 var currentIntervalStart = this.intervalStart 729 .clone() 730 .add(i, 'months'); 731 var currentIntervalEnd = currentIntervalStart 732 .clone() 733 .endOf('month'); 734 var days = this.createDaysObject( 735 currentIntervalStart, 736 currentIntervalEnd); 737 // Save events processed for each month into a master array of 738 // events for this interval 739 eventsThisInterval.push(this.eventsThisInterval); 740 months.push({ 741 days: days, 742 month: currentIntervalStart 743 }); 744 } 745 746 // Get the total number of rows across all months 747 for (i = 0; i < months.length; i++) { 748 numberOfRows += Math.ceil(months[i].days.length / 7); 749 } 750 751 data = { 752 days: [], 753 year: null, 754 month: null, 755 months: months, 756 eventsThisMonth: [], 757 numberOfRows: numberOfRows, 758 extras: this.options.extras, 759 intervalEnd: this.intervalEnd, 760 intervalStart: this.intervalStart, 761 daysOfTheWeek: this.daysOfTheWeek, 762 eventsLastMonth: this.eventsLastMonth, 763 eventsNextMonth: this.eventsNextMonth, 764 eventsThisInterval: eventsThisInterval, 765 }; 766 } 767 else { 768 // Get an array of days and blank spaces 769 days = this.createDaysObject( 770 this.month.clone().startOf('month'), 771 this.month.clone().endOf('month')); 772 // This is to prevent a scope/naming issue between this.month and 773 // data.month 774 currentMonth = this.month; 775 776 data = { 777 days: days, 778 months: [], 779 intervalEnd: null, 780 intervalStart: null, 781 year: this.month.year(), 782 eventsThisInterval: null, 783 extras: this.options.extras, 784 month: this.month.format('MMMM'), 785 monthP: this.month.clone().subtract(1, 'months').format('MMMM'), 786 monthN: this.month.clone().add(1, '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 extras in the calendar and triggers a render. 1534 */ 1535 Clndr.prototype.setExtras = function (extras) { 1536 this.options.extras = extras; 1537 this.render(); 1538 return this; 1539 }; 1540 1541 /** 1542 * Overwrites events in the calendar and triggers a render. 1543 */ 1544 Clndr.prototype.setEvents = function (events) { 1545 // Go through each event and add a moment object 1546 if (this.options.multiDayEvents) { 1547 this.options.events = this.addMultiDayMomentObjectsToEvents(events); 1548 } else { 1549 this.options.events = this.addMomentObjectToEvents(events); 1550 } 1551 1552 this.render(); 1553 return this; 1554 }; 1555 1556 /** 1557 * Adds additional events to the calendar and triggers a render. 1558 */
1559 Clndr.prototype.addEvents = function (events /*, reRender*/) { 1560 var reRender = (arguments.length > 1) 1561 ? arguments[1] 1562 : true; 1563 1564 // Go through each event and add a moment object 1565 if (this.options.multiDayEvents) { 1566 this.options.events = $.merge( 1567 this.options.events, 1568 this.addMultiDayMomentObjectsToEvents(events)); 1569 } else { 1570 this.options.events = $.merge( 1571 this.options.events, 1572 this.addMomentObjectToEvents(events)); 1573 } 1574 1575 if (reRender) { 1576 this.render(); 1577 } 1578 1579 return this; 1580 }; 1581 1582 /** 1583 * Passes all events through a matching function. Any that pass a truth 1584 * test will be removed from the calendar's events. This triggers a render. 1585 */ 1586 Clndr.prototype.removeEvents = function (matchingFn) { 1587 for (var i = this.options.events.length - 1; i >= 0; i--) { 1588 if (matchingFn(this.options.events[i]) == true) { 1589 this.options.events.splice(i, 1); 1590 } 1591 } 1592 1593 this.render(); 1594 return this; 1595 }; 1596 1597 Clndr.prototype.addMomentObjectToEvents = function (events) { 1598 var i = 0, 1599 self = this; 1600 1601 for (i; i < events.length; i++) { 1602 // Add the date as both start and end, since it's a single-day 1603 // event by default 1604 events[i]._clndrStartDateObject = 1605 moment(events[i][self.options.dateParameter]); 1606 events[i]._clndrEndDateObject = 1607 moment(events[i][self.options.dateParameter]); 1608 } 1609 1610 return events; 1611 }; 1612 1613 Clndr.prototype.addMultiDayMomentObjectsToEvents = function (events) { 1614 var i = 0, 1615 self = this, 1616 multiEvents = self.options.multiDayEvents; 1617 1618 for (i; i < events.length; i++) { 1619 var end = events[i][multiEvents.endDate], 1620 start = events[i][multiEvents.startDate]; 1621 // If we don't find the startDate OR endDate fields, look for 1622 // singleDay 1623 if (!end && !start) { 1624 events[i]._clndrEndDateObject = 1625 moment(events[i][multiEvents.singleDay]); 1626 events[i]._clndrStartDateObject = 1627 moment(events[i][multiEvents.singleDay]); 1628 } 1629 // Otherwise use startDate and endDate, or whichever one is present 1630 else { 1631 events[i]._clndrEndDateObject = moment(end || start); 1632 events[i]._clndrStartDateObject = moment(start || end); 1633 } 1634 } 1635 1636 return events; 1637 }; 1638 1639 Clndr.prototype.calendarDay = function (options) { 1640 var defaults = { 1641 day: "", 1642 date: null, 1643 events: [], 1644 classes: this.options.targets.empty 1645 }; 1646 1647 return $.extend({}, defaults, options); 1648 }; 1649 1650 Clndr.prototype.destroy = function () { 1651 var $container = $(this.calendarContainer); 1652 $container.parent().data('plugin_clndr', null); 1653 this.options = defaults; 1654 $container.empty().remove(); 1655 this.element = null; 1656 }; 1657 1658 $.fn.clndr = function (options) { 1659 var clndrInstance; 1660 1661 if (this.length > 1) { 1662 throw new Error( 1663 "CLNDR does not support multiple elements yet. Make sure " + 1664 "your clndr selector returns only one element."); 1665 } 1666 1667 if (!this.length) { 1668 throw new Error( 1669 "CLNDR cannot be instantiated on an empty selector."); 1670 } 1671 1672 if (!this.data('plugin_clndr')) { 1673 clndrInstance = new Clndr(this, options); 1674 this.data('plugin_clndr', clndrInstance); 1675 return clndrInstance; 1676 } 1677 1678 return this.data('plugin_clndr'); 1679 }; 1680}));
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.