Dashboard sipadu mbip
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.

clndr.js 61KB

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