PageSourceSearch

https://plb.lt/wp-content/themes/nowoTheme/modules/clndr/js/clndr.js?ver=1.0.0

js plb.lt collected 2026-10-02 22:27:30 UTC 53,910 bytes, 1,693 lines download raw bytes

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.