You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1673 lines
61 KiB

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