calendar.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243
  1. /**
  2. * Bootstrap based calendar full view.
  3. *
  4. * https://github.com/Serhioromano/bootstrap-calendar
  5. *
  6. * User: Sergey Romanov <serg4172@mail.ru>
  7. */
  8. "use strict";
  9. Date.prototype.getWeek = function() {
  10. var onejan = new Date(this.getFullYear(), 0, 1);
  11. return Math.ceil((((this.getTime() - onejan.getTime()) / 86400000) + onejan.getDay() + 1) / 7);
  12. };
  13. Date.prototype.getMonthFormatted = function() {
  14. var month = this.getMonth() + 1;
  15. return month < 10 ? '0' + month : month;
  16. };
  17. Date.prototype.getDateFormatted = function() {
  18. var date = this.getDate();
  19. return date < 10 ? '0' + date : date;
  20. };
  21. if(!String.prototype.format) {
  22. String.prototype.format = function() {
  23. var args = arguments;
  24. return this.replace(/{(\d+)}/g, function(match, number) {
  25. return typeof args[number] != 'undefined' ? args[number] : match;
  26. });
  27. };
  28. }
  29. if(!String.prototype.formatNum) {
  30. String.prototype.formatNum = function(decimal) {
  31. var r = "" + this;
  32. while(r.length < decimal)
  33. r = "0" + r;
  34. return r;
  35. };
  36. }
  37. (function($) {
  38. var defaults = {
  39. // Width of the calendar
  40. width: '100%',
  41. // Initial view (can be 'month', 'week', 'day')
  42. view: 'month',
  43. // Initial date. No matter month, week or day this will be a starting point. Can be 'now' or a date in format 'yyyy-mm-dd'
  44. day: 'now',
  45. // Day Start time and end time with time intervals. Time split 10, 15 or 30.
  46. time_start: '06:00',
  47. time_end: '22:00',
  48. time_split: '30',
  49. // Source of events data. It can be one of the following:
  50. // - URL to return JSON list of events in special format.
  51. // {success:1, result: [....]} or for error {success:0, error:'Something terrible happened'}
  52. // events: [...] as described in events property description
  53. // The start and end variables will be sent to this url
  54. // - A function that received the start and end date, and that
  55. // returns an array of events (as described in events property description)
  56. // - An array containing the events
  57. events_source: '',
  58. // Set format12 to true if you want to use 12 Hour format instead of 24 Hour
  59. format12: false,
  60. am_suffix: "AM",
  61. pm_suffix: "PM",
  62. // Path to templates should end with slash /. It can be as relative
  63. // /component/bootstrap-calendar/tmpls/
  64. // or absolute
  65. // http://localhost/component/bootstrap-calendar/tmpls/
  66. tmpl_path: 'tmpls/',
  67. tmpl_cache: true,
  68. classes: {
  69. months: {
  70. inmonth: 'cal-day-inmonth',
  71. outmonth: 'cal-day-outmonth',
  72. saturday: 'cal-day-weekend',
  73. sunday: 'cal-day-weekend',
  74. holidays: 'cal-day-holiday',
  75. today: 'cal-day-today'
  76. },
  77. week: {
  78. workday: 'cal-day-workday',
  79. saturday: 'cal-day-weekend',
  80. sunday: 'cal-day-weekend',
  81. holidays: 'cal-day-holiday',
  82. today: 'cal-day-today'
  83. }
  84. },
  85. // ID of the element of modal window. If set, events URLs will be opened in modal windows.
  86. modal: null,
  87. // modal handling setting, one of "iframe", "ajax" or "template"
  88. modal_type: "iframe",
  89. // function to set modal title, will be passed the event as a parameter
  90. modal_title: null,
  91. views: {
  92. year: {
  93. slide_events: 1,
  94. enable: 1
  95. },
  96. month: {
  97. slide_events: 1,
  98. enable: 1
  99. },
  100. week: {
  101. enable: 1
  102. },
  103. day: {
  104. enable: 1
  105. }
  106. },
  107. merge_holidays: false,
  108. display_week_numbers: true,
  109. weekbox: true,
  110. // ------------------------------------------------------------
  111. // CALLBACKS. Events triggered by calendar class. You can use
  112. // those to affect you UI
  113. // ------------------------------------------------------------
  114. onAfterEventsLoad: function(events) {
  115. // Inside this function 'this' is the calendar instance
  116. },
  117. onBeforeEventsLoad: function(next) {
  118. // Inside this function 'this' is the calendar instance
  119. next();
  120. },
  121. onAfterViewLoad: function(view) {
  122. // Inside this function 'this' is the calendar instance
  123. },
  124. onAfterModalShown: function(events) {
  125. // Inside this function 'this' is the calendar instance
  126. },
  127. onAfterModalHidden: function(events) {
  128. // Inside this function 'this' is the calendar instance
  129. },
  130. // -------------------------------------------------------------
  131. // INTERNAL USE ONLY. DO NOT ASSIGN IT WILL BE OVERRIDDEN ANYWAY
  132. // -------------------------------------------------------------
  133. events: [],
  134. templates: {
  135. year: '',
  136. month: '',
  137. week: '',
  138. day: ''
  139. },
  140. stop_cycling: false
  141. };
  142. var defaults_extended = {
  143. first_day: 2,
  144. holidays: {
  145. // January 1
  146. '01-01': "New Year's Day",
  147. // Third (+3*) Monday (1) in January (01)
  148. '01+3*1': "Birthday of Dr. Martin Luther King, Jr.",
  149. // Third (+3*) Monday (1) in February (02)
  150. '02+3*1': "Washington's Birthday",
  151. // Last (-1*) Monday (1) in May (05)
  152. '05-1*1': "Memorial Day",
  153. // July 4
  154. '04-07': "Independence Day",
  155. // First (+1*) Monday (1) in September (09)
  156. '09+1*1': "Labor Day",
  157. // Second (+2*) Monday (1) in October (10)
  158. '10+2*1': "Columbus Day",
  159. // November 11
  160. '11-11': "Veterans Day",
  161. // Fourth (+4*) Thursday (4) in November (11)
  162. '11+4*4': "Thanksgiving Day",
  163. // December 25
  164. '25-12': "Christmas"
  165. }
  166. };
  167. var strings = {
  168. error_noview: 'Calendar: View {0} not found',
  169. error_dateformat: 'Calendar: Wrong date format {0}. Should be either "now" or "yyyy-mm-dd"',
  170. error_loadurl: 'Calendar: Event URL is not set',
  171. error_where: 'Calendar: Wrong navigation direction {0}. Can be only "next" or "prev" or "today"',
  172. error_timedevide: 'Calendar: Time split parameter should divide 60 without decimals. Something like 10, 15, 30',
  173. no_events_in_day: 'No events in this day.',
  174. title_year: '{0}',
  175. title_month: '{0} {1}',
  176. title_week: 'week {0} of {1}',
  177. title_day: '{0} {1} {2}, {3}',
  178. week: 'Week {0}',
  179. all_day: 'All day',
  180. time: 'Time',
  181. events: 'Events',
  182. before_time: 'Ends before timeline',
  183. after_time: 'Starts after timeline',
  184. m0: 'January',
  185. m1: 'February',
  186. m2: 'March',
  187. m3: 'April',
  188. m4: 'May',
  189. m5: 'June',
  190. m6: 'July',
  191. m7: 'August',
  192. m8: 'September',
  193. m9: 'October',
  194. m10: 'November',
  195. m11: 'December',
  196. ms0: 'Jan',
  197. ms1: 'Feb',
  198. ms2: 'Mar',
  199. ms3: 'Apr',
  200. ms4: 'May',
  201. ms5: 'Jun',
  202. ms6: 'Jul',
  203. ms7: 'Aug',
  204. ms8: 'Sep',
  205. ms9: 'Oct',
  206. ms10: 'Nov',
  207. ms11: 'Dec',
  208. d0: 'Sunday',
  209. d1: 'Monday',
  210. d2: 'Tuesday',
  211. d3: 'Wednesday',
  212. d4: 'Thursday',
  213. d5: 'Friday',
  214. d6: 'Saturday'
  215. };
  216. var browser_timezone = '';
  217. try {
  218. if($.type(window.jstz) == 'object' && $.type(jstz.determine) == 'function') {
  219. browser_timezone = jstz.determine().name();
  220. if($.type(browser_timezone) !== 'string') {
  221. browser_timezone = '';
  222. }
  223. }
  224. }
  225. catch(e) {
  226. }
  227. function buildEventsUrl(events_url, data) {
  228. var separator, key, url;
  229. url = events_url;
  230. separator = (events_url.indexOf('?') < 0) ? '?' : '&';
  231. for(key in data) {
  232. url += separator + key + '=' + encodeURIComponent(data[key]);
  233. separator = '&';
  234. }
  235. return url;
  236. }
  237. function getExtentedOption(cal, option_name) {
  238. var fromOptions = (cal.options[option_name] != null) ? cal.options[option_name] : null;
  239. var fromLanguage = (cal.locale[option_name] != null) ? cal.locale[option_name] : null;
  240. if((option_name == 'holidays') && cal.options.merge_holidays) {
  241. var holidays = {};
  242. $.extend(true, holidays, fromLanguage ? fromLanguage : defaults_extended.holidays);
  243. if(fromOptions) {
  244. $.extend(true, holidays, fromOptions);
  245. }
  246. return holidays;
  247. }
  248. else {
  249. if(fromOptions != null) {
  250. return fromOptions;
  251. }
  252. if(fromLanguage != null) {
  253. return fromLanguage;
  254. }
  255. return defaults_extended[option_name];
  256. }
  257. }
  258. function getHolidays(cal, year) {
  259. var hash = [];
  260. var holidays_def = getExtentedOption(cal, 'holidays');
  261. for(var k in holidays_def) {
  262. hash.push(k + ':' + holidays_def[k]);
  263. }
  264. hash.push(year);
  265. hash = hash.join('|');
  266. if(hash in getHolidays.cache) {
  267. return getHolidays.cache[hash];
  268. }
  269. var holidays = [];
  270. $.each(holidays_def, function(key, name) {
  271. var firstDay = null, lastDay = null, failed = false;
  272. $.each(key.split('>'), function(i, chunk) {
  273. var m, date = null;
  274. if(m = /^(\d\d)-(\d\d)$/.exec(chunk)) {
  275. date = new Date(year, parseInt(m[2], 10) - 1, parseInt(m[1], 10));
  276. }
  277. else if(m = /^(\d\d)-(\d\d)-(\d\d\d\d)$/.exec(chunk)) {
  278. if(parseInt(m[3], 10) == year) {
  279. date = new Date(year, parseInt(m[2], 10) - 1, parseInt(m[1], 10));
  280. }
  281. }
  282. else if(m = /^easter(([+\-])(\d+))?$/.exec(chunk)) {
  283. date = getEasterDate(year, m[1] ? parseInt(m[1], 10) : 0);
  284. }
  285. else if(m = /^(\d\d)([+\-])([1-5])\*([0-6])$/.exec(chunk)) {
  286. var month = parseInt(m[1], 10) - 1;
  287. var direction = m[2];
  288. var offset = parseInt(m[3]);
  289. var weekday = parseInt(m[4]);
  290. switch(direction) {
  291. case '+':
  292. var d = new Date(year, month, 1 - 7);
  293. while(d.getDay() != weekday) {
  294. d = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1);
  295. }
  296. date = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 7 * offset);
  297. break;
  298. case '-':
  299. var d = new Date(year, month + 1, 0 + 7);
  300. while(d.getDay() != weekday) {
  301. d = new Date(d.getFullYear(), d.getMonth(), d.getDate() - 1);
  302. }
  303. date = new Date(d.getFullYear(), d.getMonth(), d.getDate() - 7 * offset);
  304. break;
  305. }
  306. }
  307. if(!date) {
  308. warn('Unknown holiday: ' + key);
  309. failed = true;
  310. return false;
  311. }
  312. switch(i) {
  313. case 0:
  314. firstDay = date;
  315. break;
  316. case 1:
  317. if(date.getTime() <= firstDay.getTime()) {
  318. warn('Unknown holiday: ' + key);
  319. failed = true;
  320. return false;
  321. }
  322. lastDay = date;
  323. break;
  324. default:
  325. warn('Unknown holiday: ' + key);
  326. failed = true;
  327. return false;
  328. }
  329. });
  330. if(!failed) {
  331. var days = [];
  332. if(lastDay) {
  333. for(var date = new Date(firstDay.getTime()); date.getTime() <= lastDay.getTime(); date.setDate(date.getDate() + 1)) {
  334. days.push(new Date(date.getTime()));
  335. }
  336. }
  337. else {
  338. days.push(firstDay);
  339. }
  340. holidays.push({name: name, days: days});
  341. }
  342. });
  343. getHolidays.cache[hash] = holidays;
  344. return getHolidays.cache[hash];
  345. }
  346. getHolidays.cache = {};
  347. function warn(message) {
  348. if($.type(window.console) == 'object' && $.type(window.console.warn) == 'function') {
  349. window.console.warn('[Bootstrap-Calendar] ' + message);
  350. }
  351. }
  352. function Calendar(params, context) {
  353. this.options = $.extend(true, {position: {start: new Date(), end: new Date()}}, defaults, params);
  354. this.setLanguage(this.options.language);
  355. this.context = context;
  356. context.css('width', this.options.width).addClass('cal-context');
  357. this.view();
  358. return this;
  359. }
  360. Calendar.prototype.setOptions = function(object) {
  361. $.extend(this.options, object);
  362. if('language' in object) {
  363. this.setLanguage(object.language);
  364. }
  365. if('modal' in object) {
  366. this._update_modal();
  367. }
  368. }
  369. Calendar.prototype.setLanguage = function(lang) {
  370. if(window.calendar_languages && (lang in window.calendar_languages)) {
  371. this.locale = $.extend(true, {}, strings, calendar_languages[lang]);
  372. this.options.language = lang;
  373. } else {
  374. this.locale = strings;
  375. delete this.options.language;
  376. }
  377. }
  378. Calendar.prototype._render = function() {
  379. this.context.html('');
  380. this._loadTemplate(this.options.view);
  381. this.stop_cycling = false;
  382. var data = {};
  383. data.cal = this;
  384. data.day = 1;
  385. // Getting list of days in a week in correct order. Works for month and week views
  386. if(getExtentedOption(this, 'first_day') == 1) {
  387. data.days_name = [this.locale.d1, this.locale.d2, this.locale.d3, this.locale.d4, this.locale.d5, this.locale.d6, this.locale.d0]
  388. } else {
  389. data.days_name = [this.locale.d0, this.locale.d1, this.locale.d2, this.locale.d3, this.locale.d4, this.locale.d5, this.locale.d6]
  390. }
  391. // Get all events between start and end
  392. var start = parseInt(this.options.position.start.getTime());
  393. var end = parseInt(this.options.position.end.getTime());
  394. data.events = this.getEventsBetween(start, end);
  395. switch(this.options.view) {
  396. case 'month':
  397. break;
  398. case 'week':
  399. this._calculate_hour_minutes(data);
  400. break;
  401. case 'day':
  402. this._calculate_hour_minutes(data);
  403. break;
  404. }
  405. data.start = new Date(this.options.position.start.getTime());
  406. data.lang = this.locale;
  407. this.context.append(this.options.templates[this.options.view](data));
  408. this._update();
  409. };
  410. Calendar.prototype._format_hour = function(str_hour) {
  411. var hour_split = str_hour.split(":");
  412. var hour = parseInt(hour_split[0]);
  413. var minutes = parseInt(hour_split[1]);
  414. var suffix = '';
  415. if(this.options.format12) {
  416. if(hour < 12) {
  417. suffix = this.options.am_suffix;
  418. }
  419. else {
  420. suffix = this.options.pm_suffix;
  421. }
  422. hour = hour % 12;
  423. if(hour == 0) {
  424. hour = 12;
  425. }
  426. }
  427. return hour.toString().formatNum(2) + ':' + minutes.toString().formatNum(2) + suffix;
  428. };
  429. Calendar.prototype._format_time = function(datetime) {
  430. return this._format_hour(datetime.getHours() + ':' + datetime.getMinutes());
  431. };
  432. Calendar.prototype._calculate_hour_minutes = function(data) {
  433. var $self = this;
  434. var time_split = parseInt(this.options.time_split);
  435. var time_split_count = 60 / time_split;
  436. var time_split_hour = Math.min(time_split_count, 1);
  437. if(((time_split_count >= 1) && (time_split_count % 1 != 0)) || ((time_split_count < 1) && (1440 / time_split % 1 != 0))) {
  438. $.error(this.locale.error_timedevide);
  439. }
  440. var time_start = this.options.time_start.split(":");
  441. var time_end = this.options.time_end.split(":");
  442. data.hours = (parseInt(time_end[0]) - parseInt(time_start[0])) * time_split_hour;
  443. var lines = data.hours * time_split_count - parseInt(time_start[1]) / time_split;
  444. var ms_per_line = (60000 * time_split);
  445. var start = new Date(this.options.position.start.getTime());
  446. start.setHours(time_start[0]);
  447. start.setMinutes(time_start[1]);
  448. var end = new Date(this.options.position.end.getTime());
  449. end.setHours(time_end[0]);
  450. end.setMinutes(time_end[1]);
  451. data.all_day = [];
  452. data.by_hour = [];
  453. data.after_time = [];
  454. data.before_time = [];
  455. $.each(data.events, function(k, e) {
  456. var s = new Date(parseInt(e.start));
  457. var f = new Date(parseInt(e.end));
  458. e.start_hour = $self._format_time(s);
  459. e.end_hour = $self._format_time(f);
  460. if(e.start < start.getTime()) {
  461. warn(1);
  462. e.start_hour = s.getDate() + ' ' + $self.locale['ms' + s.getMonth()] + ' ' + e.start_hour;
  463. }
  464. if(e.end > end.getTime()) {
  465. warn(1);
  466. e.end_hour = f.getDate() + ' ' + $self.locale['ms' + f.getMonth()] + ' ' + e.end_hour;
  467. }
  468. if(e.start < start.getTime() && e.end > end.getTime()) {
  469. data.all_day.push(e);
  470. return;
  471. }
  472. if(e.end < start.getTime()) {
  473. data.before_time.push(e);
  474. return;
  475. }
  476. if(e.start > end.getTime()) {
  477. data.after_time.push(e);
  478. return;
  479. }
  480. var event_start = start.getTime() - e.start;
  481. if(event_start >= 0) {
  482. e.top = 0;
  483. } else {
  484. e.top = Math.abs(event_start) / ms_per_line;
  485. }
  486. var lines_left = Math.abs(lines - e.top);
  487. var lines_in_event = (e.end - e.start) / ms_per_line;
  488. if(event_start >= 0) {
  489. lines_in_event = (e.end - start.getTime()) / ms_per_line;
  490. }
  491. e.lines = lines_in_event;
  492. if(lines_in_event > lines_left) {
  493. e.lines = lines_left;
  494. }
  495. data.by_hour.push(e);
  496. });
  497. //var d = new Date('2013-03-14 13:20:00');
  498. //warn(d.getTime());
  499. };
  500. Calendar.prototype._hour_min = function(hour) {
  501. var time_start = this.options.time_start.split(":");
  502. var time_split = parseInt(this.options.time_split);
  503. var in_hour = 60 / time_split;
  504. return (hour == 0) ? (in_hour - (parseInt(time_start[1]) / time_split)) : in_hour;
  505. };
  506. Calendar.prototype._hour = function(hour, part) {
  507. var time_start = this.options.time_start.split(":");
  508. var time_split = parseInt(this.options.time_split);
  509. var h = "" + (parseInt(time_start[0]) + hour * Math.max(time_split / 60, 1));
  510. var m = "" + time_split * part;
  511. return this._format_hour(h.formatNum(2) + ":" + m.formatNum(2));
  512. };
  513. Calendar.prototype._week = function(event) {
  514. this._loadTemplate('week-days');
  515. var t = {};
  516. var start = parseInt(this.options.position.start.getTime());
  517. var end = parseInt(this.options.position.end.getTime());
  518. var events = [];
  519. var self = this;
  520. var first_day = getExtentedOption(this, 'first_day');
  521. $.each(this.getEventsBetween(start, end), function(k, event) {
  522. event.start_day = new Date(parseInt(event.start)).getDay();
  523. if(first_day == 1) {
  524. event.start_day = (event.start_day + 6) % 7;
  525. }
  526. if((event.end - event.start) <= 86400000) {
  527. event.days = 1;
  528. } else {
  529. event.days = ((event.end - event.start) / 86400000);
  530. }
  531. if(event.start < start) {
  532. event.days = event.days - ((start - event.start) / 86400000);
  533. event.start_day = 0;
  534. }
  535. event.days = Math.ceil(event.days);
  536. if(event.start_day + event.days > 7) {
  537. event.days = 7 - (event.start_day);
  538. }
  539. events.push(event);
  540. });
  541. t.events = events;
  542. t.cal = this;
  543. return self.options.templates['week-days'](t);
  544. }
  545. Calendar.prototype._month = function(month) {
  546. this._loadTemplate('year-month');
  547. var t = {cal: this};
  548. var newmonth = month + 1;
  549. t.data_day = this.options.position.start.getFullYear() + '-' + (newmonth < 10 ? '0' + newmonth : newmonth) + '-' + '01';
  550. t.month_name = this.locale['m' + month];
  551. var curdate = new Date(this.options.position.start.getFullYear(), month, 1, 0, 0, 0);
  552. t.start = parseInt(curdate.getTime());
  553. t.end = parseInt(new Date(this.options.position.start.getFullYear(), month + 1, 1, 0, 0, 0).getTime());
  554. t.events = this.getEventsBetween(t.start, t.end);
  555. return this.options.templates['year-month'](t);
  556. }
  557. Calendar.prototype._day = function(week, day) {
  558. this._loadTemplate('month-day');
  559. var t = {tooltip: '', cal: this};
  560. var cls = this.options.classes.months.outmonth;
  561. var firstday = this.options.position.start.getDay();
  562. if(getExtentedOption(this, 'first_day') == 2) {
  563. firstday++;
  564. } else {
  565. firstday = (firstday == 0 ? 7 : firstday);
  566. }
  567. day = (day - firstday) + 1;
  568. var curdate = new Date(this.options.position.start.getFullYear(), this.options.position.start.getMonth(), day, 0, 0, 0);
  569. // if day of the current month
  570. if(day > 0) {
  571. cls = this.options.classes.months.inmonth;
  572. }
  573. // stop cycling table rows;
  574. var daysinmonth = (new Date(this.options.position.end.getTime() - 1)).getDate();
  575. if((day + 1) > daysinmonth) {
  576. this.stop_cycling = true;
  577. }
  578. // if day of the next month
  579. if(day > daysinmonth) {
  580. day = day - daysinmonth;
  581. cls = this.options.classes.months.outmonth;
  582. }
  583. cls = $.trim(cls + " " + this._getDayClass("months", curdate));
  584. if(day <= 0) {
  585. var daysinprevmonth = (new Date(this.options.position.start.getFullYear(), this.options.position.start.getMonth(), 0)).getDate();
  586. day = daysinprevmonth - Math.abs(day);
  587. cls += ' cal-month-first-row';
  588. }
  589. var holiday = this._getHoliday(curdate);
  590. if(holiday !== false) {
  591. t.tooltip = holiday;
  592. }
  593. t.data_day = curdate.getFullYear() + '-' + curdate.getMonthFormatted() + '-' + (day < 10 ? '0' + day : day);
  594. t.cls = cls;
  595. t.day = day;
  596. t.start = parseInt(curdate.getTime());
  597. t.end = parseInt(t.start + 86400000);
  598. t.events = this.getEventsBetween(t.start, t.end);
  599. return this.options.templates['month-day'](t);
  600. }
  601. Calendar.prototype._getHoliday = function(date) {
  602. var result = false;
  603. $.each(getHolidays(this, date.getFullYear()), function() {
  604. var found = false;
  605. $.each(this.days, function() {
  606. if(this.toDateString() == date.toDateString()) {
  607. found = true;
  608. return false;
  609. }
  610. });
  611. if(found) {
  612. result = this.name;
  613. return false;
  614. }
  615. });
  616. return result;
  617. };
  618. Calendar.prototype._getHolidayName = function(date) {
  619. var holiday = this._getHoliday(date);
  620. return (holiday === false) ? "" : holiday;
  621. };
  622. Calendar.prototype._getDayClass = function(class_group, date) {
  623. var self = this;
  624. var addClass = function(which, to) {
  625. var cls;
  626. cls = (self.options.classes && (class_group in self.options.classes) && (which in self.options.classes[class_group])) ? self.options.classes[class_group][which] : "";
  627. if((typeof(cls) == "string") && cls.length) {
  628. to.push(cls);
  629. }
  630. };
  631. var classes = [];
  632. if(date.toDateString() == (new Date()).toDateString()) {
  633. addClass("today", classes);
  634. }
  635. var holiday = this._getHoliday(date);
  636. if(holiday !== false) {
  637. addClass("holidays", classes);
  638. }
  639. switch(date.getDay()) {
  640. case 0:
  641. addClass("sunday", classes);
  642. break;
  643. case 6:
  644. addClass("saturday", classes);
  645. break;
  646. }
  647. addClass(date.toDateString(), classes);
  648. return classes.join(" ");
  649. };
  650. Calendar.prototype.view = function(view) {
  651. if(view) {
  652. if(!this.options.views[view].enable) {
  653. return;
  654. }
  655. this.options.view = view;
  656. }
  657. this._init_position();
  658. this._loadEvents();
  659. this._render();
  660. this.options.onAfterViewLoad.call(this, this.options.view);
  661. };
  662. Calendar.prototype.navigate = function(where, next) {
  663. var to = $.extend({}, this.options.position);
  664. if(where == 'next') {
  665. switch(this.options.view) {
  666. case 'year':
  667. to.start.setFullYear(this.options.position.start.getFullYear() + 1);
  668. break;
  669. case 'month':
  670. to.start.setMonth(this.options.position.start.getMonth() + 1);
  671. break;
  672. case 'week':
  673. to.start.setDate(this.options.position.start.getDate() + 7);
  674. break;
  675. case 'day':
  676. to.start.setDate(this.options.position.start.getDate() + 1);
  677. break;
  678. }
  679. } else if(where == 'prev') {
  680. switch(this.options.view) {
  681. case 'year':
  682. to.start.setFullYear(this.options.position.start.getFullYear() - 1);
  683. break;
  684. case 'month':
  685. to.start.setMonth(this.options.position.start.getMonth() - 1);
  686. break;
  687. case 'week':
  688. to.start.setDate(this.options.position.start.getDate() - 7);
  689. break;
  690. case 'day':
  691. to.start.setDate(this.options.position.start.getDate() - 1);
  692. break;
  693. }
  694. } else if(where == 'today') {
  695. to.start.setTime(new Date().getTime());
  696. }
  697. else {
  698. $.error(this.locale.error_where.format(where))
  699. }
  700. this.options.day = to.start.getFullYear() + '-' + to.start.getMonthFormatted() + '-' + to.start.getDateFormatted();
  701. this.view();
  702. if(_.isFunction(next)) {
  703. next();
  704. }
  705. };
  706. Calendar.prototype._init_position = function() {
  707. var year, month, day;
  708. if(this.options.day == 'now') {
  709. var date = new Date();
  710. year = date.getFullYear();
  711. month = date.getMonth();
  712. day = date.getDate();
  713. } else if(this.options.day.match(/^\d{4}-\d{2}-\d{2}$/g)) {
  714. var list = this.options.day.split('-');
  715. year = parseInt(list[0], 10);
  716. month = parseInt(list[1], 10) - 1;
  717. day = parseInt(list[2], 10);
  718. }
  719. else {
  720. $.error(this.locale.error_dateformat.format(this.options.day));
  721. }
  722. switch(this.options.view) {
  723. case 'year':
  724. this.options.position.start.setTime(new Date(year, 0, 1).getTime());
  725. this.options.position.end.setTime(new Date(year + 1, 0, 1).getTime());
  726. break;
  727. case 'month':
  728. this.options.position.start.setTime(new Date(year, month, 1).getTime());
  729. this.options.position.end.setTime(new Date(year, month + 1, 1).getTime());
  730. break;
  731. case 'day':
  732. this.options.position.start.setTime(new Date(year, month, day).getTime());
  733. this.options.position.end.setTime(new Date(year, month, day + 1).getTime());
  734. break;
  735. case 'week':
  736. var curr = new Date(year, month, day);
  737. var first;
  738. if(getExtentedOption(this, 'first_day') == 1) {
  739. first = curr.getDate() - ((curr.getDay() + 6) % 7);
  740. }
  741. else {
  742. first = curr.getDate() - curr.getDay();
  743. }
  744. this.options.position.start.setTime(new Date(year, month, first).getTime());
  745. this.options.position.end.setTime(new Date(year, month, first + 7).getTime());
  746. break;
  747. default:
  748. $.error(this.locale.error_noview.format(this.options.view))
  749. }
  750. return this;
  751. };
  752. Calendar.prototype.getTitle = function() {
  753. var p = this.options.position.start;
  754. switch(this.options.view) {
  755. case 'year':
  756. return this.locale.title_year.format(p.getFullYear());
  757. break;
  758. case 'month':
  759. return this.locale.title_month.format(this.locale['m' + p.getMonth()], p.getFullYear());
  760. break;
  761. case 'week':
  762. return this.locale.title_week.format(p.getWeek(), p.getFullYear());
  763. break;
  764. case 'day':
  765. return this.locale.title_day.format(this.locale['d' + p.getDay()], p.getDate(), this.locale['m' + p.getMonth()], p.getFullYear());
  766. break;
  767. }
  768. return;
  769. };
  770. Calendar.prototype.isToday = function() {
  771. var now = new Date().getTime();
  772. return ((now > this.options.position.start) && (now < this.options.position.end));
  773. }
  774. Calendar.prototype.getStartDate = function() {
  775. return this.options.position.start;
  776. }
  777. Calendar.prototype.getEndDate = function() {
  778. return this.options.position.end;
  779. }
  780. Calendar.prototype._loadEvents = function() {
  781. var self = this;
  782. var source = null;
  783. if('events_source' in this.options && this.options.events_source !== '') {
  784. source = this.options.events_source;
  785. }
  786. else if('events_url' in this.options) {
  787. source = this.options.events_url;
  788. warn('The events_url option is DEPRECATED and it will be REMOVED in near future. Please use events_source instead.');
  789. }
  790. var loader;
  791. switch($.type(source)) {
  792. case 'function':
  793. loader = function() {
  794. return source(self.options.position.start, self.options.position.end, browser_timezone);
  795. };
  796. break;
  797. case 'array':
  798. loader = function() {
  799. return [].concat(source);
  800. };
  801. break;
  802. case 'string':
  803. if(source.length) {
  804. loader = function() {
  805. var events = [];
  806. var d = new Date();
  807. var utc_offset = d.getTimezoneOffset();
  808. var params = {from: self.options.position.start.getTime(), to: self.options.position.end.getTime(), utc_offset: utc_offset};
  809. if(browser_timezone.length) {
  810. params.browser_timezone = browser_timezone;
  811. }
  812. $.ajax({
  813. url: buildEventsUrl(source, params),
  814. dataType: 'json',
  815. type: 'GET',
  816. async: false
  817. }).done(function(json) {
  818. if(!json.success) {
  819. $.error(json.error);
  820. }
  821. if(json.result) {
  822. events = json.result;
  823. }
  824. });
  825. return events;
  826. };
  827. }
  828. break;
  829. }
  830. if(!loader) {
  831. $.error(this.locale.error_loadurl);
  832. }
  833. this.options.onBeforeEventsLoad.call(this, function() {
  834. self.options.events = loader();
  835. self.options.events.sort(function(a, b) {
  836. var delta;
  837. delta = a.start - b.start;
  838. if(delta == 0) {
  839. delta = a.end - b.end;
  840. }
  841. return delta;
  842. });
  843. self.options.onAfterEventsLoad.call(self, self.options.events);
  844. });
  845. };
  846. Calendar.prototype._templatePath = function(name) {
  847. if(typeof this.options.tmpl_path == 'function') {
  848. return this.options.tmpl_path(name)
  849. }
  850. else {
  851. return this.options.tmpl_path + name + '.html';
  852. }
  853. };
  854. Calendar.prototype._loadTemplate = function(name) {
  855. if(this.options.templates[name]) {
  856. return;
  857. }
  858. var self = this;
  859. $.ajax({
  860. url: self._templatePath(name),
  861. dataType: 'html',
  862. type: 'GET',
  863. async: false,
  864. cache: this.options.tmpl_cache
  865. }).done(function(html) {
  866. self.options.templates[name] = _.template(html);
  867. });
  868. };
  869. Calendar.prototype._update = function() {
  870. var self = this;
  871. $('*[data-toggle="tooltip"]').tooltip({container: 'body'});
  872. $('*[data-cal-date]').click(function() {
  873. var view = $(this).data('cal-view');
  874. self.options.day = $(this).data('cal-date');
  875. self.view(view);
  876. });
  877. $('.cal-cell').dblclick(function() {
  878. var view = $('[data-cal-date]', this).data('cal-view');
  879. self.options.day = $('[data-cal-date]', this).data('cal-date');
  880. self.view(view);
  881. });
  882. this['_update_' + this.options.view]();
  883. this._update_modal();
  884. };
  885. Calendar.prototype._update_modal = function() {
  886. var self = this;
  887. $('a[data-event-id]', this.context).unbind('click');
  888. if(!self.options.modal) {
  889. return;
  890. }
  891. var modal = $(self.options.modal);
  892. if(!modal.length) {
  893. return;
  894. }
  895. var ifrm = null;
  896. if(self.options.modal_type == "iframe") {
  897. ifrm = $(document.createElement("iframe"))
  898. .attr({
  899. width: "100%",
  900. frameborder: "0"
  901. });
  902. }
  903. $('a[data-event-id]', this.context).on('click', function(event) {
  904. event.preventDefault();
  905. event.stopPropagation();
  906. var url = $(this).attr('href');
  907. var id = $(this).data("event-id");
  908. var event = _.find(self.options.events, function(event) {
  909. return event.id == id
  910. });
  911. if(self.options.modal_type == "iframe") {
  912. ifrm.attr('src', url);
  913. $('.modal-body', modal).html(ifrm);
  914. }
  915. if(!modal.data('handled.bootstrap-calendar') || (modal.data('handled.bootstrap-calendar') && modal.data('handled.event-id') != event.id)) {
  916. modal.off('show.bs.modal')
  917. .off('shown.bs.modal')
  918. .off('hidden.bs.modal')
  919. .on('show.bs.modal', function() {
  920. var modal_body = $(this).find('.modal-body');
  921. switch(self.options.modal_type) {
  922. case "iframe" :
  923. var height = modal_body.height() - parseInt(modal_body.css('padding-top'), 10) - parseInt(modal_body.css('padding-bottom'), 10);
  924. $(this).find('iframe').height(Math.max(height, 50));
  925. break;
  926. case "ajax":
  927. $.ajax({
  928. url: url, dataType: "html", async: false, success: function(data) {
  929. modal_body.html(data);
  930. }
  931. });
  932. break;
  933. case "template":
  934. self._loadTemplate("modal");
  935. // also serve calendar instance to underscore template to be able to access current language strings
  936. modal_body.html(self.options.templates["modal"]({"event": event, "calendar": self}))
  937. break;
  938. }
  939. // set the title of the bootstrap modal
  940. if(_.isFunction(self.options.modal_title)) {
  941. modal.find(".modal-title").html(self.options.modal_title(event));
  942. }
  943. })
  944. .on('shown.bs.modal', function() {
  945. self.options.onAfterModalShown.call(self, self.options.events);
  946. })
  947. .on('hidden.bs.modal', function() {
  948. self.options.onAfterModalHidden.call(self, self.options.events);
  949. })
  950. .data('handled.bootstrap-calendar', true).data('handled.event-id', event.id);
  951. }
  952. modal.modal('show');
  953. });
  954. };
  955. Calendar.prototype._update_day = function() {
  956. $('#cal-day-panel').height($('#cal-day-panel-hour').height());
  957. };
  958. Calendar.prototype._update_week = function() {
  959. };
  960. Calendar.prototype._update_year = function() {
  961. this._update_month_year();
  962. };
  963. Calendar.prototype._update_month = function() {
  964. this._update_month_year();
  965. var self = this;
  966. if(this.options.weekbox == true) {
  967. var week = $(document.createElement('div')).attr('id', 'cal-week-box');
  968. var start = this.options.position.start.getFullYear() + '-' + this.options.position.start.getMonthFormatted() + '-';
  969. self.context.find('.cal-month-box .cal-row-fluid')
  970. .on('mouseenter', function() {
  971. var p = new Date(self.options.position.start);
  972. var child = $('.cal-cell1:first-child .cal-month-day', this);
  973. var day = (child.hasClass('cal-month-first-row') ? 1 : $('[data-cal-date]', child).text());
  974. p.setDate(parseInt(day));
  975. day = (day < 10 ? '0' + day : day);
  976. week.html(self.locale.week.format(self.options.display_week_numbers == true ? p.getWeek() : ''));
  977. week.attr('data-cal-week', start + day).show().appendTo(child);
  978. })
  979. .on('mouseleave', function() {
  980. week.hide();
  981. });
  982. week.click(function() {
  983. self.options.day = $(this).data('cal-week');
  984. self.view('week');
  985. });
  986. }
  987. $('a.event').mouseenter(function() {
  988. $('a[data-event-id="' + $(this).data('event-id') + '"]').closest('.cal-cell1').addClass('day-highlight dh-' + $(this).data('event-class'));
  989. });
  990. $('a.event').mouseleave(function() {
  991. $('div.cal-cell1').removeClass('day-highlight dh-' + $(this).data('event-class'));
  992. });
  993. };
  994. Calendar.prototype._update_month_year = function() {
  995. if(!this.options.views[this.options.view].slide_events) {
  996. return;
  997. }
  998. var self = this;
  999. var activecell = 0;
  1000. var downbox = $(document.createElement('div')).attr('id', 'cal-day-tick').html('<i class="icon-chevron-down glyphicon glyphicon-chevron-down"></i>');
  1001. $('.cal-month-day, .cal-year-box .span3')
  1002. .on('mouseenter', function() {
  1003. if($('.events-list', this).length == 0) {
  1004. return;
  1005. }
  1006. if($(this).children('[data-cal-date]').text() == self.activecell) {
  1007. return;
  1008. }
  1009. downbox.show().appendTo(this);
  1010. })
  1011. .on('mouseleave', function() {
  1012. downbox.hide();
  1013. })
  1014. .on('click', function(event) {
  1015. if($('.events-list', this).length == 0) {
  1016. return;
  1017. }
  1018. if($(this).children('[data-cal-date]').text() == self.activecell) {
  1019. return;
  1020. }
  1021. showEventsList(event, downbox, slider, self);
  1022. })
  1023. ;
  1024. var slider = $(document.createElement('div')).attr('id', 'cal-slide-box');
  1025. slider.hide().click(function(event) {
  1026. event.stopPropagation();
  1027. });
  1028. this._loadTemplate('events-list');
  1029. downbox.click(function(event) {
  1030. showEventsList(event, $(this), slider, self);
  1031. });
  1032. };
  1033. Calendar.prototype.getEventsBetween = function(start, end) {
  1034. var events = [];
  1035. $.each(this.options.events, function() {
  1036. if(this.start == null) {
  1037. return true;
  1038. }
  1039. var event_end = this.end || this.start;
  1040. if((parseInt(this.start) < end) && (parseInt(event_end) >= start)) {
  1041. events.push(this);
  1042. }
  1043. });
  1044. return events;
  1045. };
  1046. function showEventsList(event, that, slider, self) {
  1047. event.stopPropagation();
  1048. var that = $(that);
  1049. var cell = that.closest('.cal-cell');
  1050. var row = cell.closest('.cal-before-eventlist');
  1051. var tick_position = cell.data('cal-row');
  1052. that.fadeOut('fast');
  1053. slider.slideUp('fast', function() {
  1054. var event_list = $('.events-list', cell);
  1055. slider.html(self.options.templates['events-list']({
  1056. cal: self,
  1057. events: self.getEventsBetween(parseInt(event_list.data('cal-start')), parseInt(event_list.data('cal-end')))
  1058. }));
  1059. row.after(slider);
  1060. self.activecell = $('[data-cal-date]', cell).text();
  1061. $('#cal-slide-tick').addClass('tick' + tick_position).show();
  1062. slider.slideDown('fast', function() {
  1063. $('body').one('click', function() {
  1064. slider.slideUp('fast');
  1065. self.activecell = 0;
  1066. });
  1067. });
  1068. });
  1069. // Wait 400ms before updating the modal & attach the mouseenter&mouseleave(400ms is the time for the slider to fade out and slide up)
  1070. setTimeout(function() {
  1071. $('a.event-item').mouseenter(function() {
  1072. $('a[data-event-id="' + $(this).data('event-id') + '"]').closest('.cal-cell1').addClass('day-highlight dh-' + $(this).data('event-class'));
  1073. });
  1074. $('a.event-item').mouseleave(function() {
  1075. $('div.cal-cell1').removeClass('day-highlight dh-' + $(this).data('event-class'));
  1076. });
  1077. self._update_modal();
  1078. }, 400);
  1079. }
  1080. function getEasterDate(year, offsetDays) {
  1081. var a = year % 19;
  1082. var b = Math.floor(year / 100);
  1083. var c = year % 100;
  1084. var d = Math.floor(b / 4);
  1085. var e = b % 4;
  1086. var f = Math.floor((b + 8) / 25);
  1087. var g = Math.floor((b - f + 1) / 3);
  1088. var h = (19 * a + b - d - g + 15) % 30;
  1089. var i = Math.floor(c / 4);
  1090. var k = c % 4;
  1091. var l = (32 + 2 * e + 2 * i - h - k) % 7;
  1092. var m = Math.floor((a + 11 * h + 22 * l) / 451);
  1093. var n0 = (h + l + 7 * m + 114)
  1094. var n = Math.floor(n0 / 31) - 1;
  1095. var p = n0 % 31 + 1;
  1096. return new Date(year, n, p + (offsetDays ? offsetDays : 0), 0, 0, 0);
  1097. }
  1098. $.fn.calendar = function(params) {
  1099. return new Calendar(params, this);
  1100. }
  1101. }(jQuery));