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 } 861 862 // Do we have room to go forward? 863 if (end && 864 (end.isBefore(this.intervalEnd) || 865 end.isSame(this.intervalEnd, 'day')) 866 ) { 867 this.element.find('.' + this.options.targets.nextButton) 868 .toggleClass(this.options.classes.inactive, true); 869 this.constraints.next = !this.constraints.next; 870 } 871 872 // What's last year looking like? 873 if (start && start.isAfter(oneYearAgo)) { 874 this.element 875 .find('.' + this.options.targets.previousYearButton) 876 .toggleClass(this.options.classes.inactive, true); 877 this.constraints.previousYear = !this.constraints.previousYear; 878 } 879 880 // How about next year? 881 if (end && end.isBefore(oneYearFromEnd)) { 882 this.element 883 .find('.' + this.options.targets.nextYearButton) 884 .toggleClass(this.options.classes.inactive, true); 885 this.constraints.nextYear = !this.constraints.nextYear; 886 } 887 888 // Today? We could put this in init(), but we want to support the 889 // user changing the constraints on a living instance. 890 if ((start && start.isAfter(moment(), 'month')) || 891 (end && end.isBefore(moment(), 'month')) 892 ) { 893 this.element.find('.' + this.options.targets.today) 894 .toggleClass(this.options.classes.inactive, true); 895 this.constraints.today = !this.constraints.today; 896 } 897 } 898 899 if (this.options.doneRendering) { 900 this.options.doneRendering.apply(this, []); 901 } 902 }; 903 904 Clndr.prototype.bindEvents = function () { 905 var data = {}; 906 var self = this; 907 var $container = $(this.element); 908 var targets = this.options.targets; 909 var classes = self.options.classes; 910 var eventType = this.options.useTouchEvents === true 911 ? 'touchstart' 912 : 'click'; 913 var eventName = eventType + '.clndr'; 914 915 // Make sure we don't already have events 916 $container 917 .off(eventName, '.' + targets.day) 918 .off(eventName, '.' + targets.empty) 919 .off(eventName, '.' + targets.nextButton) 920 .off(eventName, '.' + targets.todayButton) 921 .off(eventName, '.' + targets.previousButton) 922 .off(eventName, '.' + targets.nextYearButton) 923 .off(eventName, '.' + targets.previousYearButton); 924 925 // Target the day elements and give them click events 926 $container.on(eventName, '.' + targets.day, function (event) { 927 var target; 928 var $currentTarget = $(event.currentTarget); 929 930 // If adjacentDaysChangeMonth is on, we need to change the 931 // month here. Forward and Back trigger render() to be called. 932 // This is a callback because it can be triggered in two places. 933 var handleAdjacentDay = function () { 934 if (self.options.adjacentDaysChangeMonth) { 935 if ($currentTarget.is('.' + classes.lastMonth)) { 936 self.backActionWithContext(self); 937 return true; 938 } else if ($currentTarget.is('.' + classes.nextMonth)) { 939 self.forwardActionWithContext(self); 940 return true; 941 } 942 } 943 }; 944 945 // If setting is enabled, we want to store the selected date 946 // as a string. When render() is called, the selected date will 947 // get the additional classes added. If there is no re-render, 948 // then just add the classes manually. 949 if (self.options.trackSelectedDate && 950 !(self.options.ignoreInactiveDaysInSelection &&
951 $currentTarget.hasClass(classes.inactive)) 952 ) { 953 // If there was no re-render, manually update classes 954 if (handleAdjacentDay() !== true) { 955 // Remember new selected date 956 self.options.selectedDate = 957 self.getTargetDateString(event.currentTarget); 958 $container.find('.' + classes.selected) 959 .removeClass(classes.selected); 960 $currentTarget.addClass(classes.selected); 961 } 962 } else { 963 handleAdjacentDay(); 964 } 965 966 // Trigger click events after any selected date updates 967 if (self.options.clickEvents.click) { 968 target = self.buildTargetObject(event.currentTarget, true); 969 self.options.clickEvents.click.apply(self, [target]); 970 } 971 }); 972 973 // Target the empty calendar boxes as well 974 $container.on(eventName, '.' + targets.empty, function (event) { 975 var target; 976 var $eventTarget = $(event.currentTarget); 977 978 if (self.options.clickEvents.click) { 979 target = self.buildTargetObject(event.currentTarget, false); 980 self.options.clickEvents.click.apply(self, [target]); 981 } 982 983 if (self.options.adjacentDaysChangeMonth) { 984 if ($eventTarget.is('.' + classes.lastMonth)) { 985 self.backActionWithContext(self); 986 } else if ($eventTarget.is('.' + classes.nextMonth)) { 987 self.forwardActionWithContext(self); 988 } 989 } 990 }); 991 992 // Bind the previous, next and today buttons. We pass the current 993 // context along with the event so that it can update this instance. 994 data = { 995 context: this 996 }; 997 998 $container 999 .on(eventName, '.' + targets.todayButton, data, this.todayAction) 1000 .on(eventName, '.' + targets.nextButton, data, this.forwardAction) 1001 .on(eventName, '.' + targets.previousButton, data, this.backAction) 1002 .on(eventName, '.' + targets.nextYearButton, data, this.nextYearAction) 1003 .on(eventName, '.' + targets.previousYearButton, data, this.previousYearAction); 1004 }; 1005 1006 /** 1007 * If the user provided a click callback we'd like to give them something 1008 * nice to work with. buildTargetObject takes the DOM element that was 1009 * clicked and returns an object with the DOM element, events, and the date 1010 * (if the latter two exist). Currently it is based on the id, however it'd 1011 * be nice to use a data- attribute in the future. 1012 */ 1013 Clndr.prototype.buildTargetObject = function (currentTarget, targetWasDay) { 1014 // This is our default target object, assuming we hit an empty day 1015 // with no events. 1016 var target = { 1017 date: null, 1018 events: [], 1019 element: currentTarget 1020 }; 1021 var filterFn; 1022 var dateString; 1023 var targetEndDate; 1024 1025 // Did we click on a day or just an empty box? 1026 if (targetWasDay) { 1027 dateString = this.getTargetDateString(currentTarget); 1028 target.date = dateString 1029 ? moment(dateString) 1030 : null; 1031 1032 // Do we have events? 1033 if (this.options.events) { 1034 // Are any of the events happening today? 1035 if (this.options.multiDayEvents) { 1036 targetEndDate = target.date.clone().endOf('day'); 1037 filterFn = function () { 1038 return this._clndrStartDateObject <= targetEndDate && 1039 target.date <= this._clndrEndDateObject; 1040 }; 1041 } else { 1042 filterFn = function () { 1043 return dateString === this._clndrStartDateObject.format('YYYY-MM-DD'); 1044 }; 1045 } 1046 1047 // Filter the dates down to the ones that match. 1048 target.events = $.makeArray($(this.options.events).filter(filterFn)); 1049 } 1050 } 1051 1052 return target; 1053 }; 1054 1055 /** 1056 * Get moment date object of the date associated with the given target. 1057 * This method is meant to be called on ".day" elements. 1058 */ 1059 Clndr.prototype.getTargetDateString = function (target) { 1060 // Our identifier is in the list of classNames. Find it! 1061 var index = target.className.indexOf('calendar-day-'); 1062 1063 if (index !== -1) { 1064 // Our unique identifier is always 23 characters long. 1065 // If this feels a little wonky, that's probably because it is.
1066 // Open to suggestions on how to improve this guy. 1067 return target.className.substring(index + 13, index + 23); 1068 } 1069 1070 return null; 1071 }; 1072 1073 /** 1074 * Triggers any applicable events given a change in the calendar's start 1075 * and end dates. ctx contains the current (changed) start and end date, 1076 * orig contains the original start and end dates. 1077 */ 1078 Clndr.prototype.triggerEvents = function (ctx, orig) { 1079 var nextYear; 1080 var prevYear; 1081 var nextMonth; 1082 var prevMonth; 1083 var yearChanged; 1084 var monthChanged; 1085 var nextInterval; 1086 var prevInterval; 1087 var intervalChanged; 1088 var monthArg = [moment(ctx.month)]; 1089 var timeOpt = ctx.options.lengthOfTime; 1090 var eventsOpt = ctx.options.clickEvents; 1091 var newInt = { 1092 end: ctx.intervalEnd, 1093 start: ctx.intervalStart 1094 }; 1095 var intervalArg = [ 1096 moment(ctx.intervalStart), 1097 moment(ctx.intervalEnd) 1098 ]; 1099 1100 // We want to determine if any of the change conditions have been 1101 // hit and then trigger our events based off that. 1102 nextMonth = newInt.start.isAfter(orig.start) && 1103 (Math.abs(newInt.start.month() - orig.start.month()) === 1 || 1104 (orig.start.month() === 11 && newInt.start.month() === 0)); 1105 prevMonth = newInt.start.isBefore(orig.start) && 1106 (Math.abs(orig.start.month() - newInt.start.month()) === 1 || 1107 (orig.start.month() === 0 && newInt.start.month() === 11)); 1108 monthChanged = newInt.start.month() !== orig.start.month() || 1109 newInt.start.year() !== orig.start.year(); 1110 nextYear = newInt.start.year() - orig.start.year() === 1 || 1111 newInt.end.year() - orig.end.year() === 1; 1112 prevYear = orig.start.year() - newInt.start.year() === 1 || 1113 orig.end.year() - newInt.end.year() === 1; 1114 yearChanged = newInt.start.year() !== orig.start.year(); 1115 1116 // Only configs with a time period will get the interval change event 1117 if (timeOpt.days || timeOpt.months) { 1118 nextInterval = newInt.start.isAfter(orig.start); 1119 prevInterval = newInt.start.isBefore(orig.start); 1120 intervalChanged = nextInterval || prevInterval; 1121 1122 if (nextInterval && eventsOpt.nextInterval) { 1123 eventsOpt.nextInterval.apply(ctx, intervalArg); 1124 } 1125 1126 if (prevInterval && eventsOpt.previousInterval) { 1127 eventsOpt.previousInterval.apply(ctx, intervalArg); 1128 } 1129 1130 if (intervalChanged && eventsOpt.onIntervalChange) { 1131 eventsOpt.onIntervalChange.apply(ctx, intervalArg); 1132 } 1133 } else { 1134 // @V2-todo see https://github.com/kylestetz/CLNDR/issues/225 1135 if (nextMonth && eventsOpt.nextMonth) { 1136 eventsOpt.nextMonth.apply(ctx, monthArg); 1137 } 1138 1139 if (prevMonth && eventsOpt.previousMonth) { 1140 eventsOpt.previousMonth.apply(ctx, monthArg); 1141 } 1142 1143 if (monthChanged && eventsOpt.onMonthChange) { 1144 eventsOpt.onMonthChange.apply(ctx, monthArg); 1145 } 1146 1147 if (nextYear && eventsOpt.nextYear) { 1148 eventsOpt.nextYear.apply(ctx, monthArg); 1149 } 1150 1151 if (prevYear && eventsOpt.previousYear) { 1152 eventsOpt.previousYear.apply(ctx, monthArg); 1153 } 1154 1155 if (yearChanged && eventsOpt.onYearChange) { 1156 eventsOpt.onYearChange.apply(ctx, monthArg); 1157 } 1158 } 1159 }; 1160 1161 /** 1162 * Main action to go backward one period. Other methods call these, like 1163 * backAction which proxies jQuery events, and backActionWithContext which 1164 * is an internal method that this library uses. 1165 */ 1166 Clndr.prototype.back = function (options /*, ctx */) { 1167 var ctx = arguments[ 1 ] || this; 1168 var timeOpt = ctx.options.lengthOfTime; 1169 var defaults = { 1170 withCallbacks: false 1171 }; 1172 var orig = { 1173 end: ctx.intervalEnd.clone(), 1174 start: ctx.intervalStart.clone() 1175 }; 1176 1177 // Extend any options 1178 options = $.extend(true, {}, defaults, options); 1179 1180 // Before we do anything, check if any constraints are limiting this 1181 if (!ctx.constraints.previous) { 1182 return ctx; 1183 } 1184 1185 if (timeOpt.days) { 1186 // Shift the interval in days 1187 ctx.intervalStart 1188 .subtract(timeOpt.interval, 'days') 1189 .startOf('day'); 1190 ctx.intervalEnd = ctx.intervalStart.clone() 1191 .add(timeOpt.days - 1, 'days') 1192 .endOf('day'); 1193 // @V2-todo Useless, but consistent with API 1194 ctx.month = ctx.intervalStart.clone(); 1195 } else { 1196 // Shift the interval by a month (or several months) 1197 ctx.intervalStart 1198 .subtract(timeOpt.interval, 'months') 1199 .startOf('month'); 1200 ctx.intervalEnd = ctx.intervalStart.clone() 1201 .add(timeOpt.months || timeOpt.interval, 'months') 1202 .subtract(1, 'days') 1203 .endOf('month'); 1204 ctx.month = ctx.intervalStart.clone(); 1205 } 1206 1207 ctx.render(); 1208 1209 if (options.withCallbacks) { 1210 ctx.triggerEvents(ctx, orig); 1211 } 1212 1213 return ctx; 1214 }; 1215 1216 Clndr.prototype.backAction = function (event) { 1217 var ctx = event.data.context; 1218 1219 ctx.backActionWithContext(ctx); 1220 }; 1221 1222 Clndr.prototype.backActionWithContext = function (ctx) { 1223 ctx.back({ 1224 withCallbacks: true 1225 }, ctx); 1226 }; 1227 1228 Clndr.prototype.previous = function (options) { 1229 // Alias 1230 return this.back(options); 1231 }; 1232 1233 /** 1234 * Main action to go forward one period. Other methods call these, like 1235 * forwardAction which proxies jQuery events, and backActionWithContext 1236 * which is an internal method that this library uses. 1237 */ 1238 Clndr.prototype.forward = function (options /*, ctx */) { 1239 var ctx = arguments[1] || this; 1240 var timeOpt = ctx.options.lengthOfTime; 1241 var defaults = { 1242 withCallbacks: false 1243 }; 1244 var orig = { 1245 end: ctx.intervalEnd.clone(), 1246 start: ctx.intervalStart.clone() 1247 }; 1248 1249 // Extend any options 1250 options = $.extend(true, {}, defaults, options); 1251 1252 // Before we do anything, check if any constraints are limiting this 1253 if (!ctx.constraints.next) { 1254 return ctx; 1255 } 1256 1257 if (ctx.options.lengthOfTime.days) { 1258 // Shift the interval in days 1259 ctx.intervalStart 1260 .add(timeOpt.interval, 'days') 1261 .startOf('day'); 1262 ctx.intervalEnd = ctx.intervalStart.clone() 1263 .add(timeOpt.days - 1, 'days') 1264 .endOf('day'); 1265 // @V2-todo Useless, but consistent with API 1266 ctx.month = ctx.intervalStart.clone(); 1267 } else { 1268 // Shift the interval by a month (or several months) 1269 ctx.intervalStart 1270 .add(timeOpt.interval, 'months') 1271 .startOf('month'); 1272 ctx.intervalEnd = ctx.intervalStart.clone() 1273 .add(timeOpt.months || timeOpt.interval, 'months') 1274 .subtract(1, 'days') 1275 .endOf('month'); 1276 ctx.month = ctx.intervalStart.clone(); 1277 } 1278 1279 ctx.render(); 1280 1281 if (options.withCallbacks) { 1282 ctx.triggerEvents(ctx, orig); 1283 } 1284 1285 return ctx; 1286 }; 1287 1288 Clndr.prototype.forwardAction = function (event) { 1289 var ctx = event.data.context; 1290 1291 ctx.forwardActionWithContext(ctx); 1292 }; 1293 1294 Clndr.prototype.forwardActionWithContext = function (ctx) { 1295 ctx.forward({ 1296 withCallbacks: true 1297 }, ctx); 1298 }; 1299 1300 Clndr.prototype.next = function (options) { 1301 // Alias 1302 return this.forward(options); 1303 }; 1304 1305 /** 1306 * Main action to go back one year. 1307 */ 1308 Clndr.prototype.previousYear = function (options /*, ctx */) { 1309 var ctx = arguments[1] || this; 1310 var defaults = { 1311 withCallbacks: false 1312 }; 1313 var orig = { 1314 end: ctx.intervalEnd.clone(), 1315 start: ctx.intervalStart.clone() 1316 }; 1317 1318 // Extend any options 1319 options = $.extend(true, {}, defaults, options); 1320 1321 // Before we do anything, check if any constraints are limiting this 1322 if (!ctx.constraints.previousYear) { 1323 return ctx; 1324 } 1325 1326 ctx.month.subtract(1, 'year'); 1327 ctx.intervalStart.subtract(1, 'year'); 1328 ctx.intervalEnd.subtract(1, 'year'); 1329 ctx.render(); 1330 1331 if (options.withCallbacks) { 1332 ctx.triggerEvents(ctx, orig); 1333 } 1334 1335 return ctx; 1336 }; 1337 1338 Clndr.prototype.previousYearAction = function (event) { 1339 event.data.context.previousYear({ 1340 withCallbacks: true 1341 }, event.data.context); 1342 }; 1343 1344 /** 1345 * Main action to go forward one year. 1346 */ 1347 Clndr.prototype.nextYear = function (options /*, ctx */) { 1348 var ctx = arguments[1] || this; 1349 var defaults = { 1350 withCallbacks: false 1351 }; 1352 var orig = { 1353 end: ctx.intervalEnd.clone(), 1354 start: ctx.intervalStart.clone() 1355 }; 1356 1357 // Extend any options 1358 options = $.extend(true, {}, defaults, options); 1359 1360 // Before we do anything, check if any constraints are limiting this 1361 if (!ctx.constraints.nextYear) { 1362 return ctx; 1363 } 1364 1365 ctx.month.add(1, 'year'); 1366 ctx.intervalStart.add(1, 'year'); 1367 ctx.intervalEnd.add(1, 'year'); 1368 ctx.render(); 1369 1370 if (options.withCallbacks) { 1371 ctx.triggerEvents(ctx, orig); 1372 } 1373 1374 return ctx; 1375 }; 1376 1377 Clndr.prototype.nextYearAction = function (event) { 1378 event.data.context.nextYear({ 1379 withCallbacks: true 1380 }, event.data.context); 1381 }; 1382 1383 Clndr.prototype.today = function (options /*, ctx */) { 1384 var ctx = arguments[1] || this; 1385 var timeOpt = ctx.options.lengthOfTime; 1386 var defaults = { 1387 withCallbacks: false 1388 }; 1389 var orig = { 1390 end: ctx.intervalEnd.clone(), 1391 start: ctx.intervalStart.clone() 1392 }; 1393 1394 // Extend any options 1395 options = $.extend(true, {}, defaults, options); 1396 // @V2-todo Only used for legacy month view 1397 ctx.month = moment().startOf('month'); 1398 1399 if (timeOpt.days) { 1400 // If there was a startDate specified, we should figure out what 1401 // the weekday is and use that as the starting point of our 1402 // interval. If not, go to today.weekday(0). 1403 if (timeOpt.startDate) { 1404 ctx.intervalStart = moment() 1405 .weekday(timeOpt.startDate.weekday()) 1406 .startOf('day'); 1407 } else { 1408 ctx.intervalStart = moment().weekday(0).startOf('day'); 1409 } 1410 1411 ctx.intervalEnd = ctx.intervalStart.clone() 1412 .add(timeOpt.days - 1, 'days') 1413 .endOf('day'); 1414 } else { 1415 // Set the intervalStart to this month. 1416 ctx.intervalStart = moment().startOf('month'); 1417 ctx.intervalEnd = ctx.intervalStart.clone() 1418 .add(timeOpt.months || timeOpt.interval, 'months') 1419 .subtract(1, 'days') 1420 .endOf('month'); 1421 } 1422 1423 // No need to re-render if we didn't change months. 1424 if (!ctx.intervalStart.isSame(orig.start) || 1425 !ctx.intervalEnd.isSame(orig.end) 1426 ) { 1427 ctx.render(); 1428 } 1429 1430 // Fire the today event handler regardless of any change 1431 if (options.withCallbacks) { 1432 if (ctx.options.clickEvents.today) { 1433 ctx.options.clickEvents.today.apply(ctx, [moment(ctx.month)]); 1434 } 1435 1436 ctx.triggerEvents(ctx, orig); 1437 } 1438 }; 1439 1440 Clndr.prototype.todayAction = function (event) { 1441 event.data.context.today({ 1442 withCallbacks: true 1443 }, event.data.context); 1444 }; 1445 1446 /** 1447 * Changes the month. Accepts 0-11 or a full/partial month name. 1448 * e.g. "Jan", "February", "Mar", etc. 1449 */ 1450 Clndr.prototype.setMonth = function (newMonth, options) { 1451 var timeOpt = this.options.lengthOfTime; 1452 var orig = { 1453 end: this.intervalEnd.clone(), 1454 start: this.intervalStart.clone() 1455 }; 1456 1457 if (timeOpt.days || timeOpt.months) { 1458 console.warn( 1459 'clndr.js: You are using a custom date interval. ' + 1460 'Use Clndr.setIntervalStart(startDate) instead.'); 1461 return this; 1462 } 1463 1464 this.month.month(newMonth); 1465 this.intervalStart = this.month.clone().startOf('month'); 1466 this.intervalEnd = this.intervalStart.clone().endOf('month'); 1467 this.render(); 1468 1469 if (options && options.withCallbacks) { 1470 this.triggerEvents(this, orig); 1471 } 1472 1473 return this; 1474 }; 1475 1476 Clndr.prototype.setYear = function (newYear, options) { 1477 var orig = { 1478 end: this.intervalEnd.clone(), 1479 start: this.intervalStart.clone() 1480 }; 1481 1482 this.month.year(newYear); 1483 this.intervalEnd.year(newYear); 1484 this.intervalStart.year(newYear); 1485 this.render(); 1486 1487 if (options && options.withCallbacks) { 1488 this.triggerEvents(this, orig); 1489 } 1490 1491 return this; 1492 }; 1493 1494 /** 1495 * Sets the start of the time period according to newDate. 1496 * newDate can be a string or a moment object. 1497 */ 1498 Clndr.prototype.setIntervalStart = function (newDate, options) { 1499 var timeOpt = this.options.lengthOfTime; 1500 var orig = { 1501 end: this.intervalEnd.clone(), 1502 start: this.intervalStart.clone() 1503 }; 1504 1505 if (!timeOpt.days && !timeOpt.months) { 1506 console.warn( 1507 'clndr.js: You are using a custom date interval. ' + 1508 'Use Clndr.setIntervalStart(startDate) instead.'); 1509 return this; 1510 } 1511 1512 if (timeOpt.days) { 1513 this.intervalStart = moment(newDate).startOf('day'); 1514 this.intervalEnd = this.intervalStart.clone() 1515 .add(timeOpt.days - 1, 'days') 1516 .endOf('day'); 1517 } else { 1518 this.intervalStart = moment(newDate).startOf('month'); 1519 this.intervalEnd = this.intervalStart.clone() 1520 .add(timeOpt.months || timeOpt.interval, 'months') 1521 .subtract(1, 'days') 1522 .endOf('month'); 1523 } 1524 1525 this.month = this.intervalStart.clone(); 1526 this.render(); 1527 1528 if (options && options.withCallbacks) { 1529 this.triggerEvents(this, orig); 1530 } 1531 1532 return this; 1533 }; 1534 1535 /** 1536 * Overwrites extras in the calendar and triggers a render. 1537 */ 1538 Clndr.prototype.setExtras = function (extras) { 1539 this.options.extras = extras; 1540 this.render(); 1541 1542 return this; 1543 }; 1544 1545 /** 1546 * Overwrites events in the calendar and triggers a render. 1547 */ 1548 Clndr.prototype.setEvents = function (events) { 1549 // Go through each event and add a moment object 1550 if (this.options.multiDayEvents) { 1551 this.options.events = this.addMultiDayMomentObjectsToEvents(events); 1552 } else { 1553 this.options.events = this.addMomentObjectToEvents(events); 1554 } 1555 1556 this.render(); 1557 1558 return this; 1559 }; 1560 1561 /** 1562 * Adds additional events to the calendar and triggers a render. 1563 */
1564 Clndr.prototype.addEvents = function (events /*, reRender */) { 1565 var reRender = arguments.length > 1 1566 ? arguments[1] 1567 : true; 1568 1569 // Go through each event and add a moment object 1570 if (this.options.multiDayEvents) { 1571 this.options.events = $.merge( 1572 this.options.events, 1573 this.addMultiDayMomentObjectsToEvents(events)); 1574 } else { 1575 this.options.events = $.merge( 1576 this.options.events, 1577 this.addMomentObjectToEvents(events)); 1578 } 1579 1580 if (reRender) { 1581 this.render(); 1582 } 1583 1584 return this; 1585 }; 1586 1587 /** 1588 * Passes all events through a matching function. Any that pass a truth 1589 * test will be removed from the calendar's events. This triggers a render. 1590 */ 1591 Clndr.prototype.removeEvents = function (matchingFn) { 1592 var i; 1593 1594 for (i = this.options.events.length - 1; i >= 0; i--) { 1595 if (matchingFn(this.options.events[i]) === true) { 1596 this.options.events.splice(i, 1); 1597 } 1598 } 1599 1600 this.render(); 1601 1602 return this; 1603 }; 1604 1605 Clndr.prototype.addMomentObjectToEvents = function (events) { 1606 var i = 0; 1607 var self = this; 1608 1609 for (i; i < events.length; i++) { 1610 // Add the date as both start and end, since it's a single-day 1611 // event by default 1612 events[i]._clndrStartDateObject = 1613 moment(events[i][self.options.dateParameter]); 1614 events[i]._clndrEndDateObject = 1615 moment(events[i][self.options.dateParameter]); 1616 } 1617 1618 return events; 1619 }; 1620 1621 Clndr.prototype.addMultiDayMomentObjectsToEvents = function (events) { 1622 var end; 1623 var start; 1624 var i = 0; 1625 var self = this; 1626 var multiEvents = self.options.multiDayEvents; 1627 1628 for (i; i < events.length; i++) { 1629 end = events[i][multiEvents.endDate]; 1630 start = events[i][multiEvents.startDate]; 1631 1632 // If we don't find the startDate OR endDate fields, look for singleDay 1633 if (!end && !start) { 1634 events[i]._clndrEndDateObject = 1635 moment(events[i][multiEvents.singleDay]); 1636 events[i]._clndrStartDateObject = 1637 moment(events[i][multiEvents.singleDay]); 1638 } else { 1639 // Otherwise use startDate and endDate, or whichever one is present 1640 events[i]._clndrEndDateObject = moment(end || start); 1641 events[i]._clndrStartDateObject = moment(start || end); 1642 } 1643 } 1644 1645 return events; 1646 }; 1647 1648 Clndr.prototype.calendarDay = function (options) { 1649 var defaults = { 1650 day: '', 1651 date: null, 1652 events: [], 1653 classes: this.options.targets.empty 1654 }; 1655 1656 return $.extend({}, defaults, options); 1657 }; 1658 1659 Clndr.prototype.destroy = function () { 1660 var $container = $(this.calendarContainer); 1661 1662 $container.parent().data('plugin_clndr', null); 1663 $container.empty().remove(); 1664 1665 this.options = defaults; 1666 this.element = null; 1667 }; 1668 1669 $.fn.clndr = function (options) { 1670 var clndrInstance; 1671 1672 if (this.length > 1) { 1673 throw new Error( 1674 'CLNDR does not support multiple elements yet. Make sure ' + 1675 'your clndr selector returns only one element.' 1676 ); 1677 } 1678 1679 if (!this.length) { 1680 throw new Error('CLNDR cannot be instantiated on an empty selector.'); 1681 } 1682 1683 if (!this.data('plugin_clndr')) { 1684 clndrInstance = new Clndr(this, options); 1685 1686 this.data('plugin_clndr', clndrInstance); 1687 1688 return clndrInstance; 1689 } 1690 1691 return this.data('plugin_clndr'); 1692 }; 1693}));
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.