clndr.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. /*
  2. * ~ CLNDR v1.1.0 ~
  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 ( $, window, document, undefined ) {
  22. // This is the default calendar template. This can be overridden.
  23. var clndrTemplate = "<div class='clndr-controls'>" +
  24. "<div class='clndr-control-button'><p class='clndr-previous-button'>previous</p></div><div class='month'><%= month %> <%= year %></div><div class='clndr-control-button rightalign'><p class='clndr-next-button'>next</p></div>" +
  25. "</div>" +
  26. "<table class='clndr-table' border='0' cellspacing='0' cellpadding='0'>" +
  27. "<thead>" +
  28. "<tr class='header-days'>" +
  29. "<% for(var i = 0; i < daysOfTheWeek.length; i++) { %>" +
  30. "<td class='header-day'><%= daysOfTheWeek[i] %></td>" +
  31. "<% } %>" +
  32. "</tr>" +
  33. "</thead>" +
  34. "<tbody>" +
  35. "<% for(var i = 0; i < numberOfRows; i++){ %>" +
  36. "<tr>" +
  37. "<% for(var j = 0; j < 7; j++){ %>" +
  38. "<% var d = j + i * 7; %>" +
  39. "<td class='<%= days[d].classes %>'><div class='day-contents'><%= days[d].day %>" +
  40. "</div></td>" +
  41. "<% } %>" +
  42. "</tr>" +
  43. "<% } %>" +
  44. "</tbody>" +
  45. "</table>";
  46. var pluginName = 'clndr';
  47. var defaults = {
  48. template: clndrTemplate,
  49. weekOffset: 0,
  50. startWithMonth: null,
  51. clickEvents: {
  52. click: null,
  53. nextMonth: null,
  54. previousMonth: null,
  55. nextYear: null,
  56. previousYear: null,
  57. today: null,
  58. onMonthChange: null,
  59. onYearChange: null
  60. },
  61. targets: {
  62. nextButton: 'clndr-next-button',
  63. previousButton: 'clndr-previous-button',
  64. nextYearButton: 'clndr-next-year-button',
  65. previousYearButton: 'clndr-previous-year-button',
  66. todayButton: 'clndr-today-button',
  67. day: 'day',
  68. empty: 'empty'
  69. },
  70. events: [],
  71. extras: null,
  72. dateParameter: 'date',
  73. multiDayEvents: null,
  74. doneRendering: null,
  75. render: null,
  76. daysOfTheWeek: null,
  77. showAdjacentMonths: true,
  78. adjacentDaysChangeMonth: false,
  79. ready: null,
  80. constraints: null
  81. };
  82. // The actual plugin constructor
  83. function Clndr( element, options ) {
  84. this.element = element;
  85. // merge the default options with user-provided options
  86. this.options = $.extend(true, {}, defaults, options);
  87. // if there are events, we should run them through our addMomentObjectToEvents function
  88. // which will add a date object that we can use to make life easier. This is only necessary
  89. // when events are provided on instantiation, since our setEvents function uses addMomentObjectToEvents.
  90. if(this.options.events.length) {
  91. if(this.options.multiDayEvents) {
  92. this.options.events = this.addMultiDayMomentObjectsToEvents(this.options.events);
  93. } else {
  94. this.options.events = this.addMomentObjectToEvents(this.options.events);
  95. }
  96. }
  97. // this object will store a reference to the current month.
  98. // it's a moment object, which allows us to poke at it a little if we need to.
  99. // this will serve as the basis for switching between months & is the go-to
  100. // internally if we want to know which month we're currently at.
  101. if(this.options.startWithMonth) {
  102. this.month = moment(this.options.startWithMonth).startOf('month');
  103. } else {
  104. this.month = moment().startOf('month');
  105. }
  106. // if we've got constraints set, make sure the month is within them.
  107. if(this.options.constraints) {
  108. // first check if the start date exists & is later than now.
  109. if(this.options.constraints.startDate) {
  110. var startMoment = moment(this.options.constraints.startDate);
  111. if(this.month.isBefore(startMoment, 'month')) {
  112. this.month.set('month', startMoment.month());
  113. this.month.set('year', startMoment.year());
  114. }
  115. }
  116. // make sure the month (whether modified or not) is before the endDate
  117. if(this.options.constraints.endDate) {
  118. var endMoment = moment(this.options.constraints.endDate);
  119. if(this.month.isAfter(endMoment, 'month')) {
  120. this.month.set('month', endMoment.month()).set('year', endMoment.year());
  121. }
  122. }
  123. }
  124. this._defaults = defaults;
  125. this._name = pluginName;
  126. // Some first-time initialization -> day of the week offset,
  127. // template compiling, making and storing some elements we'll need later,
  128. // & event handling for the controller.
  129. this.init();
  130. }
  131. Clndr.prototype.init = function () {
  132. // create the days of the week using moment's current language setting
  133. this.daysOfTheWeek = this.options.daysOfTheWeek || [];
  134. if(!this.options.daysOfTheWeek) {
  135. this.daysOfTheWeek = [];
  136. for(var i = 0; i < 7; i++) {
  137. this.daysOfTheWeek.push( moment().weekday(i).format('dd').charAt(0) );
  138. }
  139. }
  140. // shuffle the week if there's an offset
  141. if(this.options.weekOffset) {
  142. this.daysOfTheWeek = this.shiftWeekdayLabels(this.options.weekOffset);
  143. }
  144. // quick & dirty test to make sure rendering is possible.
  145. if( !$.isFunction(this.options.render) ) {
  146. this.options.render = null;
  147. if (typeof _ === 'undefined') {
  148. throw new Error("Underscore was not found. Please include underscore.js OR provide a custom render function.");
  149. }
  150. else {
  151. // we're just going ahead and using underscore here if no render method has been supplied.
  152. this.compiledClndrTemplate = _.template(this.options.template);
  153. }
  154. }
  155. // create the parent element that will hold the plugin & save it for later
  156. $(this.element).html("<div class='clndr'></div>");
  157. this.calendarContainer = $('.clndr', this.element);
  158. // attach event handlers for clicks on buttons/cells
  159. this.bindEvents();
  160. // do a normal render of the calendar template
  161. this.render();
  162. // if a ready callback has been provided, call it.
  163. if(this.options.ready) {
  164. this.options.ready.apply(this, []);
  165. }
  166. };
  167. Clndr.prototype.shiftWeekdayLabels = function(offset) {
  168. var days = this.daysOfTheWeek;
  169. for(var i = 0; i < offset; i++) {
  170. days.push( days.shift() );
  171. }
  172. return days;
  173. };
  174. // This is where the magic happens. Given a moment object representing the current month,
  175. // an array of calendarDay objects is constructed that contains appropriate events and
  176. // classes depending on the circumstance.
  177. Clndr.prototype.createDaysObject = function(currentMonth) {
  178. // this array will hold numbers for the entire grid (even the blank spaces)
  179. daysArray = [];
  180. var date = currentMonth.startOf('month');
  181. // filter the events list (if it exists) to events that are happening last month, this month and next month (within the current grid view)
  182. this.eventsLastMonth = [];
  183. this.eventsThisMonth = [];
  184. this.eventsNextMonth = [];
  185. if(this.options.events.length) {
  186. // MULTI-DAY EVENT PARSING
  187. // if we're using multi-day events, the start or end must be in the current month
  188. if(this.options.multiDayEvents) {
  189. this.eventsThisMonth = $(this.options.events).filter( function() {
  190. return this._clndrStartDateObject.format("YYYY-MM") == currentMonth.format("YYYY-MM")
  191. || this._clndrEndDateObject.format("YYYY-MM") == currentMonth.format("YYYY-MM");
  192. }).toArray();
  193. if(this.options.showAdjacentMonths) {
  194. var lastMonth = currentMonth.clone().subtract('months', 1);
  195. var nextMonth = currentMonth.clone().add('months', 1);
  196. this.eventsLastMonth = $(this.options.events).filter( function() {
  197. return this._clndrStartDateObject.format("YYYY-MM") == lastMonth.format("YYYY-MM")
  198. || this._clndrEndDateObject.format("YYYY-MM") == lastMonth.format("YYYY-MM");
  199. }).toArray();
  200. this.eventsNextMonth = $(this.options.events).filter( function() {
  201. return this._clndrStartDateObject.format("YYYY-MM") == nextMonth.format("YYYY-MM")
  202. || this._clndrEndDateObject.format("YYYY-MM") == nextMonth.format("YYYY-MM");
  203. }).toArray();
  204. }
  205. }
  206. // SINGLE-DAY EVENT PARSING
  207. // if we're using single-day events, use _clndrDateObject
  208. else {
  209. this.eventsThisMonth = $(this.options.events).filter( function() {
  210. return this._clndrDateObject.format("YYYY-MM") == currentMonth.format("YYYY-MM");
  211. }).toArray();
  212. // filter the adjacent months as well, if the option is true
  213. if(this.options.showAdjacentMonths) {
  214. var lastMonth = currentMonth.clone().subtract('months', 1);
  215. var nextMonth = currentMonth.clone().add('months', 1);
  216. this.eventsLastMonth = $(this.options.events).filter( function() {
  217. return this._clndrDateObject.format("YYYY-MM") == lastMonth.format("YYYY-MM");
  218. }).toArray();
  219. this.eventsNextMonth = $(this.options.events).filter( function() {
  220. return this._clndrDateObject.format("YYYY-MM") == nextMonth.format("YYYY-MM");
  221. }).toArray();
  222. }
  223. }
  224. }
  225. // if diff is greater than 0, we'll have to fill in last days of the previous month
  226. // to account for the empty boxes in the grid.
  227. // we also need to take into account the weekOffset parameter
  228. var diff = date.weekday() - this.options.weekOffset;
  229. if(diff < 0) diff += 7;
  230. if(this.options.showAdjacentMonths) {
  231. for(var i = 0; i < diff; i++) {
  232. var day = moment([currentMonth.year(), currentMonth.month(), i - diff + 1]);
  233. daysArray.push( this.createDayObject(day, this.eventsLastMonth) );
  234. }
  235. } else {
  236. for(var i = 0; i < diff; i++) {
  237. daysArray.push( this.calendarDay({ classes: this.options.targets.empty + " last-month" }) );
  238. }
  239. }
  240. // now we push all of the days in a month
  241. var numOfDays = date.daysInMonth();
  242. for(var i = 1; i <= numOfDays; i++) {
  243. var day = moment([currentMonth.year(), currentMonth.month(), i]);
  244. daysArray.push(this.createDayObject(day, this.eventsThisMonth) )
  245. }
  246. // ...and if there are any trailing blank boxes, fill those in
  247. // with the next month first days
  248. if(this.options.showAdjacentMonths) {
  249. i = 1;
  250. while(daysArray.length % 7 !== 0) {
  251. var day = moment([currentMonth.year(), currentMonth.month(), numOfDays + i]);
  252. daysArray.push( this.createDayObject(day, this.eventsNextMonth) );
  253. i++;
  254. }
  255. } else {
  256. i = 1;
  257. while(daysArray.length % 7 !== 0) {
  258. daysArray.push( this.calendarDay({ classes: this.options.targets.empty + " next-month" }) );
  259. i++;
  260. }
  261. }
  262. return daysArray;
  263. };
  264. Clndr.prototype.createDayObject = function(day, monthEvents) {
  265. var eventsToday = [];
  266. var now = moment();
  267. var self = this;
  268. var j = 0, l = monthEvents.length;
  269. for(j; j < l; j++) {
  270. // keep in mind that the events here already passed the month/year test.
  271. // now all we have to compare is the moment.date(), which returns the day of the month.
  272. if(self.options.multiDayEvents) {
  273. var start = monthEvents[j]._clndrStartDateObject;
  274. var end = monthEvents[j]._clndrEndDateObject;
  275. // if today is the same day as start or is after the start, and
  276. // if today is the same day as the end or before the end ...
  277. // woohoo semantics!
  278. if( ( day.isSame(start, 'day') || day.isAfter(start, 'day') ) &&
  279. ( day.isSame(end, 'day') || day.isBefore(end, 'day') ) ) {
  280. eventsToday.push( monthEvents[j] );
  281. }
  282. } else {
  283. if( monthEvents[j]._clndrDateObject.date() == day.date() ) {
  284. eventsToday.push( monthEvents[j] );
  285. }
  286. }
  287. }
  288. var extraClasses = "";
  289. if(now.format("YYYY-MM-DD") == day.format("YYYY-MM-DD")) {
  290. extraClasses += " today";
  291. }
  292. if(day.isBefore(now, 'day')) {
  293. extraClasses += " past";
  294. }
  295. if(eventsToday.length) {
  296. extraClasses += " event";
  297. }
  298. if(this.month.month() > day.month()) {
  299. extraClasses += " adjacent-month";
  300. this.month.year() === day.year()
  301. ? extraClasses += " last-month"
  302. : extraClasses += " next-month";
  303. } else if(this.month.month() < day.month()) {
  304. extraClasses += " adjacent-month";
  305. this.month.year() === day.year()
  306. ? extraClasses += " next-month"
  307. : extraClasses += " last-month";
  308. }
  309. // if there are constraints, we need to add the inactive class to the days outside of them
  310. if(this.options.constraints) {
  311. if(this.options.constraints.startDate && day.isBefore(moment( this.options.constraints.startDate ))) {
  312. extraClasses += " inactive";
  313. }
  314. if(this.options.constraints.endDate && day.isAfter(moment( this.options.constraints.endDate ))) {
  315. extraClasses += " inactive";
  316. }
  317. }
  318. // validate moment date
  319. if (!day.isValid() && day.hasOwnProperty('_d') && day._d != undefined) {
  320. day = moment(day._d);
  321. }
  322. // we're moving away from using IDs in favor of classes, since when
  323. // using multiple calendars on a page we are technically violating the
  324. // uniqueness of IDs.
  325. extraClasses += " calendar-day-" + day.format("YYYY-MM-DD");
  326. return this.calendarDay({
  327. day: day.date(),
  328. classes: this.options.targets.day + extraClasses,
  329. events: eventsToday,
  330. date: day
  331. });
  332. };
  333. Clndr.prototype.render = function() {
  334. // get rid of the previous set of calendar parts.
  335. // TODO: figure out if this is the right way to ensure proper garbage collection?
  336. this.calendarContainer.children().remove();
  337. // get an array of days and blank spaces
  338. var days = this.createDaysObject(this.month);
  339. // this is to prevent a scope/naming issue between this.month and data.month
  340. var currentMonth = this.month;
  341. var data = {
  342. daysOfTheWeek: this.daysOfTheWeek,
  343. numberOfRows: Math.ceil(days.length / 7),
  344. days: days,
  345. month: this.month.format('MMMM'),
  346. year: this.month.year(),
  347. eventsThisMonth: this.eventsThisMonth,
  348. eventsLastMonth: this.eventsLastMonth,
  349. eventsNextMonth: this.eventsNextMonth,
  350. extras: this.options.extras
  351. };
  352. // render the calendar with the data above & bind events to its elements
  353. if(!this.options.render) {
  354. this.calendarContainer.html(this.compiledClndrTemplate(data));
  355. } else {
  356. this.calendarContainer.html(this.options.render.apply(this, [data]));
  357. }
  358. // if there are constraints, we need to add the 'inactive' class to the controls
  359. if(this.options.constraints) {
  360. // in the interest of clarity we're just going to remove all inactive classes and re-apply them each render.
  361. for(target in this.options.targets) {
  362. if(target != this.options.targets.day) {
  363. this.element.find('.' + this.options.targets[target]).toggleClass('inactive', false);
  364. }
  365. }
  366. var start = null;
  367. var end = null;
  368. if(this.options.constraints.startDate) {
  369. start = moment(this.options.constraints.startDate);
  370. }
  371. if(this.options.constraints.endDate) {
  372. end = moment(this.options.constraints.endDate);
  373. }
  374. // deal with the month controls first.
  375. // are we at the start month?
  376. if(start && this.month.isSame( start, 'month' )) {
  377. this.element.find('.' + this.options.targets.previousButton).toggleClass('inactive', true);
  378. }
  379. // are we at the end month?
  380. if(end && this.month.isSame( end, 'month' )) {
  381. this.element.find('.' + this.options.targets.nextButton).toggleClass('inactive', true);
  382. }
  383. // what's last year looking like?
  384. if(start && moment(start).subtract('years', 1).isBefore(moment(this.month).subtract('years', 1)) ) {
  385. this.element.find('.' + this.options.targets.previousYearButton).toggleClass('inactive', true);
  386. }
  387. // how about next year?
  388. if(end && moment(end).add('years', 1).isAfter(moment(this.month).add('years', 1)) ) {
  389. this.element.find('.' + this.options.targets.nextYearButton).toggleClass('inactive', true);
  390. }
  391. // today? we could put this in init(), but we want to support the user changing the constraints on a living instance.
  392. if(( start && start.isAfter( moment(), 'month' ) ) || ( end && end.isBefore( moment(), 'month' ) )) {
  393. this.element.find('.' + this.options.targets.today).toggleClass('inactive', true);
  394. }
  395. }
  396. if(this.options.doneRendering) {
  397. this.options.doneRendering.apply(this, []);
  398. }
  399. };
  400. Clndr.prototype.bindEvents = function() {
  401. var $container = $(this.element);
  402. var self = this;
  403. // target the day elements and give them click events
  404. $container.on('click', '.'+this.options.targets.day, function(event) {
  405. if(self.options.clickEvents.click) {
  406. var target = self.buildTargetObject(event.currentTarget, true);
  407. self.options.clickEvents.click.apply(self, [target]);
  408. }
  409. // if adjacentDaysChangeMonth is on, we need to change the month here.
  410. if(self.options.adjacentDaysChangeMonth) {
  411. if($(event.currentTarget).is(".last-month")) {
  412. self.backActionWithContext(self);
  413. } else if($(event.currentTarget).is(".next-month")) {
  414. self.forwardActionWithContext(self);
  415. }
  416. }
  417. });
  418. // target the empty calendar boxes as well
  419. $container.on('click', '.'+this.options.targets.empty, function(event) {
  420. if(self.options.clickEvents.click) {
  421. var target = self.buildTargetObject(event.currentTarget, false);
  422. self.options.clickEvents.click.apply(self, [target]);
  423. }
  424. if(self.options.adjacentDaysChangeMonth) {
  425. if($(event.currentTarget).is(".last-month")) {
  426. self.backActionWithContext(self);
  427. } else if($(event.currentTarget).is(".next-month")) {
  428. self.forwardActionWithContext(self);
  429. }
  430. }
  431. });
  432. // bind the previous, next and today buttons
  433. $container
  434. .on('click', '.'+this.options.targets.previousButton, { context: this }, this.backAction)
  435. .on('click', '.'+this.options.targets.nextButton, { context: this }, this.forwardAction)
  436. .on('click', '.'+this.options.targets.todayButton, { context: this }, this.todayAction)
  437. .on('click', '.'+this.options.targets.nextYearButton, { context: this }, this.nextYearAction)
  438. .on('click', '.'+this.options.targets.previousYearButton, { context: this }, this.previousYearAction);
  439. }
  440. // If the user provided a click callback we'd like to give them something nice to work with.
  441. // buildTargetObject takes the DOM element that was clicked and returns an object with
  442. // the DOM element, events, and the date (if the latter two exist). Currently it is based on the id,
  443. // however it'd be nice to use a data- attribute in the future.
  444. Clndr.prototype.buildTargetObject = function(currentTarget, targetWasDay) {
  445. // This is our default target object, assuming we hit an empty day with no events.
  446. var target = {
  447. element: currentTarget,
  448. events: [],
  449. date: null
  450. };
  451. // did we click on a day or just an empty box?
  452. if(targetWasDay) {
  453. var dateString;
  454. // Our identifier is in the list of classNames. Find it!
  455. var classNameIndex = currentTarget.className.indexOf('calendar-day-');
  456. if(classNameIndex !== 0) {
  457. // our unique identifier is always 23 characters long.
  458. // If this feels a little wonky, that's probably because it is.
  459. // Open to suggestions on how to improve this guy.
  460. dateString = currentTarget.className.substring(classNameIndex + 13, classNameIndex + 23);
  461. target.date = moment(dateString);
  462. } else {
  463. target.date = null;
  464. }
  465. // do we have events?
  466. if(this.options.events) {
  467. // are any of the events happening today?
  468. if(this.options.multiDayEvents) {
  469. target.events = $.makeArray( $(this.options.events).filter( function() {
  470. // filter the dates down to the ones that match.
  471. return ( ( target.date.isSame(this._clndrStartDateObject, 'day') || target.date.isAfter(this._clndrStartDateObject, 'day') ) &&
  472. ( target.date.isSame(this._clndrEndDateObject, 'day') || target.date.isBefore(this._clndrEndDateObject, 'day') ) );
  473. }) );
  474. } else {
  475. target.events = $.makeArray( $(this.options.events).filter( function() {
  476. // filter the dates down to the ones that match.
  477. return this._clndrDateObject.format('YYYY-MM-DD') == dateString;
  478. }) );
  479. }
  480. }
  481. }
  482. return target;
  483. }
  484. // the click handlers in bindEvents need a context, so these are wrappers
  485. // to the actual functions. Todo: better way to handle this?
  486. Clndr.prototype.forwardAction = function(event) {
  487. var self = event.data.context;
  488. self.forwardActionWithContext(self);
  489. };
  490. Clndr.prototype.backAction = function(event) {
  491. var self = event.data.context;
  492. self.backActionWithContext(self);
  493. };
  494. // These are called directly, except for in the bindEvent click handlers,
  495. // where forwardAction and backAction proxy to these guys.
  496. Clndr.prototype.backActionWithContext = function(self) {
  497. // before we do anything, check if there is an inactive class on the month control.
  498. // if it does, we want to return and take no action.
  499. if(self.element.find('.' + self.options.targets.previousButton).hasClass('inactive')) {
  500. return;
  501. }
  502. // is subtracting one month going to switch the year?
  503. var yearChanged = !self.month.isSame( moment(self.month).subtract('months', 1), 'year');
  504. self.month.subtract('months', 1);
  505. self.render();
  506. if(self.options.clickEvents.previousMonth) {
  507. self.options.clickEvents.previousMonth.apply( self, [moment(self.month)] );
  508. }
  509. if(self.options.clickEvents.onMonthChange) {
  510. self.options.clickEvents.onMonthChange.apply( self, [moment(self.month)] );
  511. }
  512. if(yearChanged) {
  513. if(self.options.clickEvents.onYearChange) {
  514. self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] );
  515. }
  516. }
  517. };
  518. Clndr.prototype.forwardActionWithContext = function(self) {
  519. // before we do anything, check if there is an inactive class on the month control.
  520. // if it does, we want to return and take no action.
  521. if(self.element.find('.' + self.options.targets.nextButton).hasClass('inactive')) {
  522. return;
  523. }
  524. // is adding one month going to switch the year?
  525. var yearChanged = !self.month.isSame( moment(self.month).add('months', 1), 'year');
  526. self.month.add('months', 1);
  527. self.render();
  528. if(self.options.clickEvents.nextMonth) {
  529. self.options.clickEvents.nextMonth.apply(self, [moment(self.month)]);
  530. }
  531. if(self.options.clickEvents.onMonthChange) {
  532. self.options.clickEvents.onMonthChange.apply(self, [moment(self.month)]);
  533. }
  534. if(yearChanged) {
  535. if(self.options.clickEvents.onYearChange) {
  536. self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] );
  537. }
  538. }
  539. };
  540. Clndr.prototype.todayAction = function(event) {
  541. var self = event.data.context;
  542. // did we switch months when the today button was hit?
  543. var monthChanged = !self.month.isSame(moment(), 'month');
  544. var yearChanged = !self.month.isSame(moment(), 'year');
  545. self.month = moment().startOf('month');
  546. // fire the today event handler regardless of whether the month changed.
  547. if(self.options.clickEvents.today) {
  548. self.options.clickEvents.today.apply( self, [moment(self.month)] );
  549. }
  550. if(monthChanged) {
  551. // no need to re-render if we didn't change months.
  552. self.render();
  553. self.month = moment();
  554. // fire the onMonthChange callback
  555. if(self.options.clickEvents.onMonthChange) {
  556. self.options.clickEvents.onMonthChange.apply( self, [moment(self.month)] );
  557. }
  558. // maybe fire the onYearChange callback?
  559. if(yearChanged) {
  560. if(self.options.clickEvents.onYearChange) {
  561. self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] );
  562. }
  563. }
  564. }
  565. };
  566. Clndr.prototype.nextYearAction = function(event) {
  567. var self = event.data.context;
  568. // before we do anything, check if there is an inactive class on the month control.
  569. // if it does, we want to return and take no action.
  570. if(self.element.find('.' + self.options.targets.nextYearButton).hasClass('inactive')) {
  571. return;
  572. }
  573. self.month.add('years', 1);
  574. self.render();
  575. if(self.options.clickEvents.nextYear) {
  576. self.options.clickEvents.nextYear.apply( self, [moment(self.month)] );
  577. }
  578. if(self.options.clickEvents.onMonthChange) {
  579. self.options.clickEvents.onMonthChange.apply( self, [moment(self.month)] );
  580. }
  581. if(self.options.clickEvents.onYearChange) {
  582. self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] );
  583. }
  584. };
  585. Clndr.prototype.previousYearAction = function(event) {
  586. var self = event.data.context;
  587. // before we do anything, check if there is an inactive class on the month control.
  588. // if it does, we want to return and take no action.
  589. if(self.element.find('.' + self.options.targets.previousYear).hasClass('inactive')) {
  590. return;
  591. }
  592. self.month.subtract('years', 1);
  593. self.render();
  594. if(self.options.clickEvents.previousYear) {
  595. self.options.clickEvents.previousYear.apply( self, [moment(self.month)] );
  596. }
  597. if(self.options.clickEvents.onMonthChange) {
  598. self.options.clickEvents.onMonthChange.apply( self, [moment(self.month)] );
  599. }
  600. if(self.options.clickEvents.onYearChange) {
  601. self.options.clickEvents.onYearChange.apply( self, [moment(self.month)] );
  602. }
  603. };
  604. Clndr.prototype.forward = function(options) {
  605. this.month.add('months', 1);
  606. this.render();
  607. if(options && options.withCallbacks) {
  608. if(this.options.clickEvents.onMonthChange) {
  609. this.options.clickEvents.onMonthChange.apply( this, [moment(this.month)] );
  610. }
  611. // We entered a new year
  612. if (this.month.month() === 0 && this.options.clickEvents.onYearChange) {
  613. this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] );
  614. }
  615. }
  616. return this;
  617. }
  618. Clndr.prototype.back = function(options) {
  619. this.month.subtract('months', 1);
  620. this.render();
  621. if(options && options.withCallbacks) {
  622. if(this.options.clickEvents.onMonthChange) {
  623. this.options.clickEvents.onMonthChange.apply( this, [moment(this.month)] );
  624. }
  625. // We went all the way back to previous year
  626. if (this.month.month() === 11 && this.options.clickEvents.onYearChange) {
  627. this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] );
  628. }
  629. }
  630. return this;
  631. }
  632. // alternate names for convenience
  633. Clndr.prototype.next = function(options) {
  634. this.forward(options);
  635. return this;
  636. }
  637. Clndr.prototype.previous = function(options) {
  638. this.back(options);
  639. return this;
  640. }
  641. Clndr.prototype.setMonth = function(newMonth, options) {
  642. // accepts 0 - 11 or a full/partial month name e.g. "Jan", "February", "Mar"
  643. this.month.month(newMonth);
  644. this.render();
  645. if(options && options.withCallbacks) {
  646. if(this.options.clickEvents.onMonthChange) {
  647. this.options.clickEvents.onMonthChange.apply( this, [moment(this.month)] );
  648. }
  649. }
  650. return this;
  651. }
  652. Clndr.prototype.nextYear = function(options) {
  653. this.month.add('year', 1);
  654. this.render();
  655. if(options && options.withCallbacks) {
  656. if(this.options.clickEvents.onYearChange) {
  657. this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] );
  658. }
  659. }
  660. return this;
  661. }
  662. Clndr.prototype.previousYear = function(options) {
  663. this.month.subtract('year', 1);
  664. this.render();
  665. if(options && options.withCallbacks) {
  666. if(this.options.clickEvents.onYearChange) {
  667. this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] );
  668. }
  669. }
  670. return this;
  671. }
  672. Clndr.prototype.setYear = function(newYear, options) {
  673. this.month.year(newYear);
  674. this.render();
  675. if(options && options.withCallbacks) {
  676. if(this.options.clickEvents.onYearChange) {
  677. this.options.clickEvents.onYearChange.apply( this, [moment(this.month)] );
  678. }
  679. }
  680. return this;
  681. }
  682. Clndr.prototype.setEvents = function(events) {
  683. // go through each event and add a moment object
  684. if(this.options.multiDayEvents) {
  685. this.options.events = this.addMultiDayMomentObjectsToEvents(events);
  686. } else {
  687. this.options.events = this.addMomentObjectToEvents(events);
  688. }
  689. this.render();
  690. return this;
  691. };
  692. Clndr.prototype.addEvents = function(events) {
  693. // go through each event and add a moment object
  694. if(this.options.multiDayEvents) {
  695. this.options.events = $.merge(this.options.events, this.addMultiDayMomentObjectsToEvents(events));
  696. } else {
  697. this.options.events = $.merge(this.options.events, this.addMomentObjectToEvents(events));
  698. }
  699. this.render();
  700. return this;
  701. };
  702. Clndr.prototype.addMomentObjectToEvents = function(events) {
  703. var self = this;
  704. var i = 0, l = events.length;
  705. for(i; i < l; i++) {
  706. // stuff a _clndrDateObject in each event, which really, REALLY should not be
  707. // overriding any existing object... Man that would be weird.
  708. events[i]._clndrDateObject = moment( events[i][self.options.dateParameter] );
  709. }
  710. return events;
  711. }
  712. Clndr.prototype.addMultiDayMomentObjectsToEvents = function(events) {
  713. var self = this;
  714. var i = 0, l = events.length;
  715. for(i; i < l; i++) {
  716. events[i]._clndrStartDateObject = moment( events[i][self.options.multiDayEvents.startDate] );
  717. events[i]._clndrEndDateObject = moment( events[i][self.options.multiDayEvents.endDate] );
  718. }
  719. return events;
  720. }
  721. Clndr.prototype.calendarDay = function(options) {
  722. var defaults = { day: "", classes: this.options.targets.empty, events: [], date: null };
  723. return $.extend({}, defaults, options);
  724. }
  725. $.fn.clndr = function(options) {
  726. if( !$.data( this, 'plugin_clndr') ) {
  727. var clndr_instance = new Clndr(this, options);
  728. $.data(this, 'plugin_clndr', clndr_instance);
  729. return clndr_instance;
  730. }
  731. }
  732. })( jQuery, window, document );