daterangepicker.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. /**
  2. * @version: 1.0.1
  3. * @author: Dan Grossman http://www.dangrossman.info/
  4. * @date: 2012-08-20
  5. * @copyright: Copyright (c) 2012 Dan Grossman. All rights reserved.
  6. * @license: Licensed under Apache License v2.0. See http://www.apache.org/licenses/LICENSE-2.0
  7. * @website: http://www.improvely.com/
  8. */
  9. !function ($) {
  10. var DateRangePicker = function (element, options, cb) {
  11. var hasOptions = typeof options == 'object'
  12. var localeObject;
  13. //state
  14. this.startDate = Date.today();
  15. this.endDate = Date.today();
  16. this.minDate = false;
  17. this.maxDate = false;
  18. this.changed = false;
  19. this.ranges = {};
  20. this.opens = 'right';
  21. this.cb = function () { };
  22. this.format = 'MM/dd/yyyy';
  23. this.separator = ' - ';
  24. this.showWeekNumbers = false;
  25. this.buttonClasses = ['btn-success'];
  26. this.locale = {
  27. applyLabel: 'Apply',
  28. fromLabel: 'From',
  29. toLabel: 'To',
  30. weekLabel: 'W',
  31. customRangeLabel: 'Custom Range',
  32. daysOfWeek: Date.CultureInfo.shortestDayNames,
  33. monthNames: Date.CultureInfo.monthNames,
  34. firstDay: 0
  35. };
  36. localeObject = this.locale;
  37. this.leftCalendar = {
  38. month: Date.today().set({ day: 1, month: this.startDate.getMonth(), year: this.startDate.getFullYear() }),
  39. calendar: Array()
  40. };
  41. this.rightCalendar = {
  42. month: Date.today().set({ day: 1, month: this.endDate.getMonth(), year: this.endDate.getFullYear() }),
  43. calendar: Array()
  44. };
  45. // by default, the daterangepicker element is placed at the bottom of HTML body
  46. this.parentEl = 'body';
  47. //element that triggered the date range picker
  48. this.element = $(element);
  49. if (this.element.hasClass('pull-right'))
  50. this.opens = 'left';
  51. if (this.element.is('input')) {
  52. this.element.on({
  53. click: $.proxy(this.show, this),
  54. focus: $.proxy(this.show, this)
  55. });
  56. } else {
  57. this.element.on('click', $.proxy(this.show, this));
  58. }
  59. if (hasOptions) {
  60. if(typeof options.locale == 'object') {
  61. $.each(localeObject, function (property, value) {
  62. localeObject[property] = options.locale[property] || value;
  63. });
  64. }
  65. }
  66. var DRPTemplate = '<div class="daterangepicker dropdown-menu">' +
  67. '<div class="calendar left"></div>' +
  68. '<div class="calendar right"></div>' +
  69. '<div class="ranges">' +
  70. '<div class="range_inputs">' +
  71. '<div>' +
  72. '<label for="daterangepicker_start">' + this.locale.fromLabel + '</label>' +
  73. '<input class="m-wrap input-mini" type="text" name="daterangepicker_start" value="" disabled="disabled" />' +
  74. '</div>' +
  75. '<div>' +
  76. '<label for="daterangepicker_end">' + this.locale.toLabel + '</label>' +
  77. '<input class="m-wrap input-mini" type="text" name="daterangepicker_end" value="" disabled="disabled" />' +
  78. '</div>' +
  79. '<button class="btn " disabled="disabled">' + this.locale.applyLabel + '</button>' +
  80. '</div>' +
  81. '</div>' +
  82. '</div>';
  83. this.parentEl = (hasOptions && options.parentEl && $(options.parentEl)) || $(this.parentEl);
  84. //the date range picker
  85. this.container = $(DRPTemplate).appendTo(this.parentEl);
  86. if (hasOptions) {
  87. if (typeof options.format == 'string')
  88. this.format = options.format;
  89. if (typeof options.separator == 'string')
  90. this.separator = options.separator;
  91. if (typeof options.startDate == 'string')
  92. this.startDate = Date.parse(options.startDate, this.format);
  93. if (typeof options.endDate == 'string')
  94. this.endDate = Date.parse(options.endDate, this.format);
  95. if (typeof options.minDate == 'string')
  96. this.minDate = Date.parse(options.minDate, this.format);
  97. if (typeof options.maxDate == 'string')
  98. this.maxDate = Date.parse(options.maxDate, this.format);
  99. if (typeof options.startDate == 'object')
  100. this.startDate = options.startDate;
  101. if (typeof options.endDate == 'object')
  102. this.endDate = options.endDate;
  103. if (typeof options.minDate == 'object')
  104. this.minDate = options.minDate;
  105. if (typeof options.maxDate == 'object')
  106. this.maxDate = options.maxDate;
  107. if (typeof options.ranges == 'object') {
  108. for (var range in options.ranges) {
  109. var start = options.ranges[range][0];
  110. var end = options.ranges[range][1];
  111. if (typeof start == 'string')
  112. start = Date.parse(start);
  113. if (typeof end == 'string')
  114. end = Date.parse(end);
  115. // If we have a min/max date set, bound this range
  116. // to it, but only if it would otherwise fall
  117. // outside of the min/max.
  118. if (this.minDate && start < this.minDate)
  119. start = this.minDate;
  120. if (this.maxDate && end > this.maxDate)
  121. end = this.maxDate;
  122. // If the end of the range is before the minimum (if min is set) OR
  123. // the start of the range is after the max (also if set) don't display this
  124. // range option.
  125. if ((this.minDate && end < this.minDate) || (this.maxDate && start > this.maxDate))
  126. {
  127. continue;
  128. }
  129. this.ranges[range] = [start, end];
  130. }
  131. var list = '<ul>';
  132. for (var range in this.ranges) {
  133. list += '<li>' + range + '</li>';
  134. }
  135. list += '<li>' + this.locale.customRangeLabel + '</li>';
  136. list += '</ul>';
  137. this.container.find('.ranges').prepend(list);
  138. }
  139. // update day names order to firstDay
  140. if (typeof options.locale == 'object') {
  141. if (typeof options.locale.firstDay == 'number') {
  142. this.locale.firstDay = options.locale.firstDay;
  143. var iterator = options.locale.firstDay;
  144. while (iterator > 0) {
  145. this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift());
  146. iterator--;
  147. }
  148. }
  149. }
  150. if (typeof options.opens == 'string')
  151. this.opens = options.opens;
  152. if (typeof options.showWeekNumbers == 'boolean') {
  153. this.showWeekNumbers = options.showWeekNumbers;
  154. }
  155. if (typeof options.buttonClasses == 'string') {
  156. this.buttonClasses = [options.buttonClasses];
  157. }
  158. if (typeof options.buttonClasses == 'object') {
  159. this.buttonClasses = options.buttonClasses;
  160. }
  161. }
  162. //apply CSS classes to buttons
  163. var c = this.container;
  164. $.each(this.buttonClasses, function (idx, val) {
  165. c.find('button').addClass(val);
  166. });
  167. if (this.opens == 'right') {
  168. //swap calendar positions
  169. var left = this.container.find('.calendar.left');
  170. var right = this.container.find('.calendar.right');
  171. left.removeClass('left').addClass('right');
  172. right.removeClass('right').addClass('left');
  173. }
  174. if (typeof options == 'undefined' || typeof options.ranges == 'undefined')
  175. this.container.find('.calendar').show();
  176. if (typeof cb == 'function')
  177. this.cb = cb;
  178. this.container.addClass('opens' + this.opens);
  179. //event listeners
  180. this.container.on('mousedown', $.proxy(this.mousedown, this));
  181. this.container.find('.calendar').on('click', '.prev', $.proxy(this.clickPrev, this));
  182. this.container.find('.calendar').on('click', '.next', $.proxy(this.clickNext, this));
  183. this.container.find('.ranges').on('click', 'button', $.proxy(this.clickApply, this));
  184. this.container.find('.calendar').on('click', 'td.available', $.proxy(this.clickDate, this));
  185. this.container.find('.calendar').on('mouseenter', 'td.available', $.proxy(this.enterDate, this));
  186. this.container.find('.calendar').on('mouseleave', 'td.available', $.proxy(this.updateView, this));
  187. this.container.find('.ranges').on('click', 'li', $.proxy(this.clickRange, this));
  188. this.container.find('.ranges').on('mouseenter', 'li', $.proxy(this.enterRange, this));
  189. this.container.find('.ranges').on('mouseleave', 'li', $.proxy(this.updateView, this));
  190. this.element.on('keyup', $.proxy(this.updateFromControl, this));
  191. this.updateView();
  192. this.updateCalendars();
  193. };
  194. DateRangePicker.prototype = {
  195. constructor: DateRangePicker,
  196. mousedown: function (e) {
  197. e.stopPropagation();
  198. e.preventDefault();
  199. },
  200. updateView: function () {
  201. this.leftCalendar.month.set({ month: this.startDate.getMonth(), year: this.startDate.getFullYear() });
  202. this.rightCalendar.month.set({ month: this.endDate.getMonth(), year: this.endDate.getFullYear() });
  203. this.container.find('input[name=daterangepicker_start]').val(this.startDate.toString(this.format));
  204. this.container.find('input[name=daterangepicker_end]').val(this.endDate.toString(this.format));
  205. if (this.startDate.equals(this.endDate) || this.startDate.isBefore(this.endDate)) {
  206. this.container.find('button').removeAttr('disabled');
  207. } else {
  208. this.container.find('button').attr('disabled', 'disabled');
  209. }
  210. },
  211. updateFromControl: function () {
  212. if (!this.element.is('input')) return;
  213. var dateString = this.element.val().split(this.separator);
  214. var start = Date.parseExact(dateString[0], this.format);
  215. var end = Date.parseExact(dateString[1], this.format);
  216. if (start == null || end == null) return;
  217. if (end.isBefore(start)) return;
  218. this.startDate = start;
  219. this.endDate = end;
  220. this.updateView();
  221. this.cb(this.startDate, this.endDate);
  222. this.updateCalendars();
  223. },
  224. notify: function () {
  225. this.updateView();
  226. if (this.element.is('input')) {
  227. this.element.val(this.startDate.toString(this.format) + this.separator + this.endDate.toString(this.format));
  228. }
  229. this.cb(this.startDate, this.endDate);
  230. },
  231. move: function () {
  232. var parentOffset = {
  233. top: this.parentEl.offset().top - this.parentEl.scrollTop(),
  234. left: this.parentEl.offset().left - this.parentEl.scrollLeft()
  235. };
  236. if (this.opens == 'left') {
  237. this.container.css({
  238. top: this.element.offset().top + this.element.outerHeight(),
  239. right: $(window).width() - this.element.offset().left - this.element.outerWidth() - parentOffset.left,
  240. left: 'auto'
  241. });
  242. } else {
  243. this.container.css({
  244. top: this.element.offset().top + this.element.outerHeight(),
  245. left: this.element.offset().left - parentOffset.left,
  246. right: 'auto'
  247. });
  248. }
  249. },
  250. show: function (e) {
  251. this.container.show();
  252. this.move();
  253. if (e) {
  254. e.stopPropagation();
  255. e.preventDefault();
  256. }
  257. this.changed = false;
  258. $(document).on('mousedown', $.proxy(this.hide, this));
  259. },
  260. hide: function (e) {
  261. this.container.hide();
  262. $(document).off('mousedown', this.hide);
  263. if (this.changed) {
  264. this.changed = false;
  265. this.notify();
  266. }
  267. },
  268. enterRange: function (e) {
  269. var label = e.target.innerHTML;
  270. if (label == this.locale.customRangeLabel) {
  271. this.updateView();
  272. } else {
  273. var dates = this.ranges[label];
  274. this.container.find('input[name=daterangepicker_start]').val(dates[0].toString(this.format));
  275. this.container.find('input[name=daterangepicker_end]').val(dates[1].toString(this.format));
  276. }
  277. },
  278. clickRange: function (e) {
  279. var label = e.target.innerHTML;
  280. if (label == this.locale.customRangeLabel) {
  281. this.container.find('.calendar').show();
  282. } else {
  283. var dates = this.ranges[label];
  284. this.startDate = dates[0];
  285. this.endDate = dates[1];
  286. this.leftCalendar.month.set({ month: this.startDate.getMonth(), year: this.startDate.getFullYear() });
  287. this.rightCalendar.month.set({ month: this.endDate.getMonth(), year: this.endDate.getFullYear() });
  288. this.updateCalendars();
  289. this.changed = true;
  290. this.container.find('.calendar').hide();
  291. this.hide();
  292. }
  293. },
  294. clickPrev: function (e) {
  295. var cal = $(e.target).parents('.calendar');
  296. if (cal.hasClass('left')) {
  297. this.leftCalendar.month.add({ months: -1 });
  298. } else {
  299. this.rightCalendar.month.add({ months: -1 });
  300. }
  301. this.updateCalendars();
  302. },
  303. clickNext: function (e) {
  304. var cal = $(e.target).parents('.calendar');
  305. if (cal.hasClass('left')) {
  306. this.leftCalendar.month.add({ months: 1 });
  307. } else {
  308. this.rightCalendar.month.add({ months: 1 });
  309. }
  310. this.updateCalendars();
  311. },
  312. enterDate: function (e) {
  313. var title = $(e.target).attr('title');
  314. var row = title.substr(1, 1);
  315. var col = title.substr(3, 1);
  316. var cal = $(e.target).parents('.calendar');
  317. if (cal.hasClass('left')) {
  318. this.container.find('input[name=daterangepicker_start]').val(this.leftCalendar.calendar[row][col].toString(this.format));
  319. } else {
  320. this.container.find('input[name=daterangepicker_end]').val(this.rightCalendar.calendar[row][col].toString(this.format));
  321. }
  322. },
  323. clickDate: function (e) {
  324. var title = $(e.target).attr('title');
  325. var row = title.substr(1, 1);
  326. var col = title.substr(3, 1);
  327. var cal = $(e.target).parents('.calendar');
  328. if (cal.hasClass('left')) {
  329. startDate = this.leftCalendar.calendar[row][col];
  330. endDate = this.endDate;
  331. } else {
  332. startDate = this.startDate;
  333. endDate = this.rightCalendar.calendar[row][col];
  334. }
  335. cal.find('td').removeClass('active');
  336. if (startDate.equals(endDate) || startDate.isBefore(endDate)) {
  337. $(e.target).addClass('active');
  338. if (!startDate.equals(this.startDate) || !endDate.equals(this.endDate))
  339. this.changed = true;
  340. this.startDate = startDate;
  341. this.endDate = endDate;
  342. }
  343. this.leftCalendar.month.set({ month: this.startDate.getMonth(), year: this.startDate.getFullYear() });
  344. this.rightCalendar.month.set({ month: this.endDate.getMonth(), year: this.endDate.getFullYear() });
  345. this.updateCalendars();
  346. },
  347. clickApply: function (e) {
  348. this.hide();
  349. },
  350. updateCalendars: function () {
  351. this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.getMonth(), this.leftCalendar.month.getFullYear());
  352. this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.getMonth(), this.rightCalendar.month.getFullYear());
  353. this.container.find('.calendar.left').html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.endDate));
  354. this.container.find('.calendar.right').html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.startDate, this.maxDate));
  355. },
  356. buildCalendar: function (month, year) {
  357. var firstDay = Date.today().set({ day: 1, month: month, year: year });
  358. var lastMonth = firstDay.clone().add(-1).day().getMonth();
  359. var lastYear = firstDay.clone().add(-1).day().getFullYear();
  360. var daysInMonth = Date.getDaysInMonth(year, month);
  361. var daysInLastMonth = Date.getDaysInMonth(lastYear, lastMonth);
  362. var dayOfWeek = firstDay.getDay();
  363. //initialize a 6 rows x 7 columns array for the calendar
  364. var calendar = Array();
  365. for (var i = 0; i < 6; i++) {
  366. calendar[i] = Array();
  367. }
  368. //populate the calendar with date objects
  369. var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1;
  370. if (startDay > daysInLastMonth)
  371. startDay -= 7;
  372. if (dayOfWeek == this.locale.firstDay)
  373. startDay = daysInLastMonth - 6;
  374. var curDate = Date.today().set({ day: startDay, month: lastMonth, year: lastYear });
  375. for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = curDate.clone().add(1).day()) {
  376. if (i > 0 && col % 7 == 0) {
  377. col = 0;
  378. row++;
  379. }
  380. calendar[row][col] = curDate;
  381. }
  382. return calendar;
  383. },
  384. renderCalendar: function (calendar, selected, minDate, maxDate) {
  385. var html = '<table class="table-condensed">';
  386. html += '<thead>';
  387. html += '<tr>';
  388. // add empty cell for week number
  389. if (this.showWeekNumbers)
  390. html += '<th></th>';
  391. if (!minDate || minDate < calendar[1][1])
  392. {
  393. html += '<th class="prev available"><i class="icon-angle-left"></i></th>';
  394. }
  395. else
  396. {
  397. html += '<th></th>';
  398. }
  399. html += '<th colspan="5" style="width: auto">' + this.locale.monthNames[calendar[1][1].getMonth()] + calendar[1][1].toString(" yyyy") + '</th>';
  400. if (!maxDate || maxDate > calendar[1][1])
  401. {
  402. html += '<th class="next available"><i class="icon-angle-right"></i></th>';
  403. }
  404. else
  405. {
  406. html += '<th></th>';
  407. }
  408. html += '</tr>';
  409. html += '<tr>';
  410. // add week number label
  411. if (this.showWeekNumbers)
  412. html += '<th class="week">' + this.locale.weekLabel + '</th>';
  413. $.each(this.locale.daysOfWeek, function (index, dayOfWeek) {
  414. html += '<th>' + dayOfWeek + '</th>';
  415. });
  416. html += '</tr>';
  417. html += '</thead>';
  418. html += '<tbody>';
  419. for (var row = 0; row < 6; row++) {
  420. html += '<tr>';
  421. // add week number
  422. if (this.showWeekNumbers)
  423. html += '<td class="week">' + calendar[row][0].getWeek() + '</td>';
  424. for (var col = 0; col < 7; col++) {
  425. var cname = 'available ';
  426. cname += (calendar[row][col].getMonth() == calendar[1][1].getMonth()) ? '' : 'off';
  427. // Normalise the time so the comparison won't fail
  428. selected.setHours(0,0,0,0);
  429. if ( (minDate && calendar[row][col] < minDate) || (maxDate && calendar[row][col] > maxDate))
  430. {
  431. cname = 'off disabled';
  432. }
  433. else if (calendar[row][col].equals(selected))
  434. {
  435. cname += 'active';
  436. }
  437. var title = 'r' + row + 'c' + col;
  438. html += '<td class="' + cname + '" title="' + title + '">' + calendar[row][col].getDate() + '</td>';
  439. }
  440. html += '</tr>';
  441. }
  442. html += '</tbody>';
  443. html += '</table>';
  444. return html;
  445. }
  446. };
  447. $.fn.daterangepicker = function (options, cb) {
  448. this.each(function() {
  449. var el = $(this);
  450. if (!el.data('daterangepicker'))
  451. el.data('daterangepicker', new DateRangePicker(el, options, cb));
  452. });
  453. return this;
  454. };
  455. } (window.jQuery);