calendar.js 56 KB

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