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