You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1193 lines
38 KiB

  1. /*!
  2. * Pikaday
  3. *
  4. * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday
  5. */
  6. (function (root, factory)
  7. {
  8. 'use strict';
  9. var moment;
  10. if (typeof exports === 'object') {
  11. // CommonJS module
  12. // Load moment.js as an optional dependency
  13. try { moment = require('moment'); } catch (e) {}
  14. module.exports = factory(moment);
  15. } else if (typeof define === 'function' && define.amd) {
  16. // AMD. Register as an anonymous module.
  17. define(function (req)
  18. {
  19. // Load moment.js as an optional dependency
  20. var id = 'moment';
  21. try { moment = req(id); } catch (e) {}
  22. return factory(moment);
  23. });
  24. } else {
  25. root.Pikaday = factory(root.moment);
  26. }
  27. }(this, function (moment)
  28. {
  29. 'use strict';
  30. /**
  31. * feature detection and helper functions
  32. */
  33. var hasMoment = typeof moment === 'function',
  34. hasEventListeners = !!window.addEventListener,
  35. document = window.document,
  36. sto = window.setTimeout,
  37. addEvent = function(el, e, callback, capture)
  38. {
  39. if (hasEventListeners) {
  40. el.addEventListener(e, callback, !!capture);
  41. } else {
  42. el.attachEvent('on' + e, callback);
  43. }
  44. },
  45. removeEvent = function(el, e, callback, capture)
  46. {
  47. if (hasEventListeners) {
  48. el.removeEventListener(e, callback, !!capture);
  49. } else {
  50. el.detachEvent('on' + e, callback);
  51. }
  52. },
  53. fireEvent = function(el, eventName, data)
  54. {
  55. var ev;
  56. if (document.createEvent) {
  57. ev = document.createEvent('HTMLEvents');
  58. ev.initEvent(eventName, true, false);
  59. ev = extend(ev, data);
  60. el.dispatchEvent(ev);
  61. } else if (document.createEventObject) {
  62. ev = document.createEventObject();
  63. ev = extend(ev, data);
  64. el.fireEvent('on' + eventName, ev);
  65. }
  66. },
  67. trim = function(str)
  68. {
  69. return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,'');
  70. },
  71. hasClass = function(el, cn)
  72. {
  73. return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1;
  74. },
  75. addClass = function(el, cn)
  76. {
  77. if (!hasClass(el, cn)) {
  78. el.className = (el.className === '') ? cn : el.className + ' ' + cn;
  79. }
  80. },
  81. removeClass = function(el, cn)
  82. {
  83. el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' '));
  84. },
  85. isArray = function(obj)
  86. {
  87. return (/Array/).test(Object.prototype.toString.call(obj));
  88. },
  89. isDate = function(obj)
  90. {
  91. return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime());
  92. },
  93. isWeekend = function(date)
  94. {
  95. var day = date.getDay();
  96. return day === 0 || day === 6;
  97. },
  98. isLeapYear = function(year)
  99. {
  100. // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951
  101. return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
  102. },
  103. getDaysInMonth = function(year, month)
  104. {
  105. return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
  106. },
  107. setToStartOfDay = function(date)
  108. {
  109. if (isDate(date)) date.setHours(0,0,0,0);
  110. },
  111. compareDates = function(a,b)
  112. {
  113. // weak date comparison (use setToStartOfDay(date) to ensure correct result)
  114. return a.getTime() === b.getTime();
  115. },
  116. extend = function(to, from, overwrite)
  117. {
  118. var prop, hasProp;
  119. for (prop in from) {
  120. hasProp = to[prop] !== undefined;
  121. if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) {
  122. if (isDate(from[prop])) {
  123. if (overwrite) {
  124. to[prop] = new Date(from[prop].getTime());
  125. }
  126. }
  127. else if (isArray(from[prop])) {
  128. if (overwrite) {
  129. to[prop] = from[prop].slice(0);
  130. }
  131. } else {
  132. to[prop] = extend({}, from[prop], overwrite);
  133. }
  134. } else if (overwrite || !hasProp) {
  135. to[prop] = from[prop];
  136. }
  137. }
  138. return to;
  139. },
  140. adjustCalendar = function(calendar) {
  141. if (calendar.month < 0) {
  142. calendar.year -= Math.ceil(Math.abs(calendar.month)/12);
  143. calendar.month += 12;
  144. }
  145. if (calendar.month > 11) {
  146. calendar.year += Math.floor(Math.abs(calendar.month)/12);
  147. calendar.month -= 12;
  148. }
  149. return calendar;
  150. },
  151. /**
  152. * defaults and localisation
  153. */
  154. defaults = {
  155. // bind the picker to a form field
  156. field: null,
  157. // automatically show/hide the picker on `field` focus (default `true` if `field` is set)
  158. bound: undefined,
  159. // position of the datepicker, relative to the field (default to bottom & left)
  160. // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position)
  161. position: 'bottom left',
  162. // automatically fit in the viewport even if it means repositioning from the position option
  163. reposition: true,
  164. // the default output format for `.toString()` and `field` value
  165. format: 'YYYY-MM-DD',
  166. // the initial date to view when first opened
  167. defaultDate: null,
  168. // make the `defaultDate` the initial selected value
  169. setDefaultDate: false,
  170. // first day of week (0: Sunday, 1: Monday etc)
  171. firstDay: 0,
  172. // the default flag for moment's strict date parsing
  173. formatStrict: false,
  174. // the minimum/earliest date that can be selected
  175. minDate: null,
  176. // the maximum/latest date that can be selected
  177. maxDate: null,
  178. // number of years either side, or array of upper/lower range
  179. yearRange: 10,
  180. // show week numbers at head of row
  181. showWeekNumber: false,
  182. // used internally (don't config outside)
  183. minYear: 0,
  184. maxYear: 9999,
  185. minMonth: undefined,
  186. maxMonth: undefined,
  187. startRange: null,
  188. endRange: null,
  189. isRTL: false,
  190. // Additional text to append to the year in the calendar title
  191. yearSuffix: '',
  192. // Render the month after year in the calendar title
  193. showMonthAfterYear: false,
  194. // Render days of the calendar grid that fall in the next or previous month
  195. showDaysInNextAndPreviousMonths: false,
  196. // how many months are visible
  197. numberOfMonths: 1,
  198. // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`)
  199. // only used for the first display or when a selected date is not visible
  200. mainCalendar: 'left',
  201. // Specify a DOM element to render the calendar in
  202. container: undefined,
  203. // internationalization
  204. i18n: {
  205. previousMonth : 'Previous Month',
  206. nextMonth : 'Next Month',
  207. months : ['January','February','March','April','May','June','July','August','September','October','November','December'],
  208. weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
  209. weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
  210. },
  211. // Theme Classname
  212. theme: null,
  213. // callback function
  214. onSelect: null,
  215. onOpen: null,
  216. onClose: null,
  217. onDraw: null
  218. },
  219. /**
  220. * templating functions to abstract HTML rendering
  221. */
  222. renderDayName = function(opts, day, abbr)
  223. {
  224. day += opts.firstDay;
  225. while (day >= 7) {
  226. day -= 7;
  227. }
  228. return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day];
  229. },
  230. renderDay = function(opts)
  231. {
  232. var arr = [];
  233. var ariaSelected = 'false';
  234. if (opts.isEmpty) {
  235. if (opts.showDaysInNextAndPreviousMonths) {
  236. arr.push('is-outside-current-month');
  237. } else {
  238. return '<td class="is-empty"></td>';
  239. }
  240. }
  241. if (opts.isDisabled) {
  242. arr.push('is-disabled');
  243. }
  244. if (opts.isToday) {
  245. arr.push('is-today');
  246. }
  247. if (opts.isSelected) {
  248. arr.push('is-selected');
  249. ariaSelected = 'true';
  250. }
  251. if (opts.isInRange) {
  252. arr.push('is-inrange');
  253. }
  254. if (opts.isStartRange) {
  255. arr.push('is-startrange');
  256. }
  257. if (opts.isEndRange) {
  258. arr.push('is-endrange');
  259. }
  260. return '<td data-day="' + opts.day + '" class="' + arr.join(' ') + '" aria-selected="' + ariaSelected + '">' +
  261. '<button class="pika-button pika-day" type="button" ' +
  262. 'data-pika-year="' + opts.year + '" data-pika-month="' + opts.month + '" data-pika-day="' + opts.day + '">' +
  263. opts.day +
  264. '</button>' +
  265. '</td>';
  266. },
  267. renderWeek = function (d, m, y) {
  268. // Lifted from http://javascript.about.com/library/blweekyear.htm, lightly modified.
  269. var onejan = new Date(y, 0, 1),
  270. weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getDay()+1)/7);
  271. return '<td class="pika-week">' + weekNum + '</td>';
  272. },
  273. renderRow = function(days, isRTL)
  274. {
  275. return '<tr>' + (isRTL ? days.reverse() : days).join('') + '</tr>';
  276. },
  277. renderBody = function(rows)
  278. {
  279. return '<tbody>' + rows.join('') + '</tbody>';
  280. },
  281. renderHead = function(opts)
  282. {
  283. var i, arr = [];
  284. if (opts.showWeekNumber) {
  285. arr.push('<th></th>');
  286. }
  287. for (i = 0; i < 7; i++) {
  288. arr.push('<th scope="col"><abbr title="' + renderDayName(opts, i) + '">' + renderDayName(opts, i, true) + '</abbr></th>');
  289. }
  290. return '<thead><tr>' + (opts.isRTL ? arr.reverse() : arr).join('') + '</tr></thead>';
  291. },
  292. renderTitle = function(instance, c, year, month, refYear, randId)
  293. {
  294. var i, j, arr,
  295. opts = instance._o,
  296. isMinYear = year === opts.minYear,
  297. isMaxYear = year === opts.maxYear,
  298. html = '<div id="' + randId + '" class="pika-title" role="heading" aria-live="assertive">',
  299. monthHtml,
  300. yearHtml,
  301. prev = true,
  302. next = true;
  303. for (arr = [], i = 0; i < 12; i++) {
  304. arr.push('<option value="' + (year === refYear ? i - c : 12 + i - c) + '"' +
  305. (i === month ? ' selected="selected"': '') +
  306. ((isMinYear && i < opts.minMonth) || (isMaxYear && i > opts.maxMonth) ? 'disabled="disabled"' : '') + '>' +
  307. opts.i18n.months[i] + '</option>');
  308. }
  309. monthHtml = '<div class="pika-label">' + opts.i18n.months[month] + '<select class="pika-select pika-select-month" tabindex="-1">' + arr.join('') + '</select></div>';
  310. if (isArray(opts.yearRange)) {
  311. i = opts.yearRange[0];
  312. j = opts.yearRange[1] + 1;
  313. } else {
  314. i = year - opts.yearRange;
  315. j = 1 + year + opts.yearRange;
  316. }
  317. for (arr = []; i < j && i <= opts.maxYear; i++) {
  318. if (i >= opts.minYear) {
  319. arr.push('<option value="' + i + '"' + (i === year ? ' selected="selected"': '') + '>' + (i) + '</option>');
  320. }
  321. }
  322. yearHtml = '<div class="pika-label">' + year + opts.yearSuffix + '<select class="pika-select pika-select-year" tabindex="-1">' + arr.join('') + '</select></div>';
  323. if (opts.showMonthAfterYear) {
  324. html += yearHtml + monthHtml;
  325. } else {
  326. html += monthHtml + yearHtml;
  327. }
  328. if (isMinYear && (month === 0 || opts.minMonth >= month)) {
  329. prev = false;
  330. }
  331. if (isMaxYear && (month === 11 || opts.maxMonth <= month)) {
  332. next = false;
  333. }
  334. if (c === 0) {
  335. html += '<button class="pika-prev' + (prev ? '' : ' is-disabled') + '" type="button">' + opts.i18n.previousMonth + '</button>';
  336. }
  337. if (c === (instance._o.numberOfMonths - 1) ) {
  338. html += '<button class="pika-next' + (next ? '' : ' is-disabled') + '" type="button">' + opts.i18n.nextMonth + '</button>';
  339. }
  340. return html += '</div>';
  341. },
  342. renderTable = function(opts, data, randId)
  343. {
  344. return '<table cellpadding="0" cellspacing="0" class="pika-table" role="grid" aria-labelledby="' + randId + '">' + renderHead(opts) + renderBody(data) + '</table>';
  345. },
  346. /**
  347. * Pikaday constructor
  348. */
  349. Pikaday = function(options)
  350. {
  351. var self = this,
  352. opts = self.config(options);
  353. self._onMouseDown = function(e)
  354. {
  355. if (!self._v) {
  356. return;
  357. }
  358. e = e || window.event;
  359. var target = e.target || e.srcElement;
  360. if (!target) {
  361. return;
  362. }
  363. if (!hasClass(target, 'is-disabled')) {
  364. if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty') && !hasClass(target.parentNode, 'is-disabled')) {
  365. self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day')));
  366. if (opts.bound) {
  367. sto(function() {
  368. self.hide();
  369. if (opts.field) {
  370. opts.field.blur();
  371. }
  372. }, 100);
  373. }
  374. }
  375. else if (hasClass(target, 'pika-prev')) {
  376. self.prevMonth();
  377. }
  378. else if (hasClass(target, 'pika-next')) {
  379. self.nextMonth();
  380. }
  381. }
  382. if (!hasClass(target, 'pika-select')) {
  383. // if this is touch event prevent mouse events emulation
  384. if (e.preventDefault) {
  385. e.preventDefault();
  386. } else {
  387. e.returnValue = false;
  388. return false;
  389. }
  390. } else {
  391. self._c = true;
  392. }
  393. };
  394. self._onChange = function(e)
  395. {
  396. e = e || window.event;
  397. var target = e.target || e.srcElement;
  398. if (!target) {
  399. return;
  400. }
  401. if (hasClass(target, 'pika-select-month')) {
  402. self.gotoMonth(target.value);
  403. }
  404. else if (hasClass(target, 'pika-select-year')) {
  405. self.gotoYear(target.value);
  406. }
  407. };
  408. self._onKeyChange = function(e)
  409. {
  410. e = e || window.event;
  411. if (self.isVisible()) {
  412. switch(e.keyCode){
  413. case 13:
  414. case 27:
  415. opts.field.blur();
  416. break;
  417. case 37:
  418. e.preventDefault();
  419. self.adjustDate('subtract', 1);
  420. break;
  421. case 38:
  422. self.adjustDate('subtract', 7);
  423. break;
  424. case 39:
  425. self.adjustDate('add', 1);
  426. break;
  427. case 40:
  428. self.adjustDate('add', 7);
  429. break;
  430. }
  431. }
  432. };
  433. self._onInputChange = function(e)
  434. {
  435. var date;
  436. if (e.firedBy === self) {
  437. return;
  438. }
  439. if (hasMoment) {
  440. date = moment(opts.field.value, opts.format, opts.formatStrict);
  441. date = (date && date.isValid()) ? date.toDate() : null;
  442. }
  443. else {
  444. date = new Date(Date.parse(opts.field.value));
  445. }
  446. if (isDate(date)) {
  447. self.setDate(date);
  448. }
  449. if (!self._v) {
  450. self.show();
  451. }
  452. };
  453. self._onInputFocus = function()
  454. {
  455. self.show();
  456. };
  457. self._onInputClick = function()
  458. {
  459. self.show();
  460. };
  461. self._onInputBlur = function()
  462. {
  463. // IE allows pika div to gain focus; catch blur the input field
  464. var pEl = document.activeElement;
  465. do {
  466. if (hasClass(pEl, 'pika-single')) {
  467. return;
  468. }
  469. }
  470. while ((pEl = pEl.parentNode));
  471. if (!self._c) {
  472. self._b = sto(function() {
  473. self.hide();
  474. }, 50);
  475. }
  476. self._c = false;
  477. };
  478. self._onClick = function(e)
  479. {
  480. e = e || window.event;
  481. var target = e.target || e.srcElement,
  482. pEl = target;
  483. if (!target) {
  484. return;
  485. }
  486. if (!hasEventListeners && hasClass(target, 'pika-select')) {
  487. if (!target.onchange) {
  488. target.setAttribute('onchange', 'return;');
  489. addEvent(target, 'change', self._onChange);
  490. }
  491. }
  492. do {
  493. if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) {
  494. return;
  495. }
  496. }
  497. while ((pEl = pEl.parentNode));
  498. if (self._v && target !== opts.trigger && pEl !== opts.trigger) {
  499. self.hide();
  500. }
  501. };
  502. self.el = document.createElement('div');
  503. self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : '');
  504. addEvent(self.el, 'mousedown', self._onMouseDown, true);
  505. addEvent(self.el, 'touchend', self._onMouseDown, true);
  506. addEvent(self.el, 'change', self._onChange);
  507. addEvent(document, 'keydown', self._onKeyChange);
  508. if (opts.field) {
  509. if (opts.container) {
  510. opts.container.appendChild(self.el);
  511. } else if (opts.bound) {
  512. document.body.appendChild(self.el);
  513. } else {
  514. opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling);
  515. }
  516. addEvent(opts.field, 'change', self._onInputChange);
  517. if (!opts.defaultDate) {
  518. if (hasMoment && opts.field.value) {
  519. opts.defaultDate = moment(opts.field.value, opts.format).toDate();
  520. } else {
  521. opts.defaultDate = new Date(Date.parse(opts.field.value));
  522. }
  523. opts.setDefaultDate = true;
  524. }
  525. }
  526. var defDate = opts.defaultDate;
  527. if (isDate(defDate)) {
  528. if (opts.setDefaultDate) {
  529. self.setDate(defDate, true);
  530. } else {
  531. self.gotoDate(defDate);
  532. }
  533. } else {
  534. self.gotoDate(new Date());
  535. }
  536. if (opts.bound) {
  537. this.hide();
  538. self.el.className += ' is-bound';
  539. addEvent(opts.trigger, 'click', self._onInputClick);
  540. addEvent(opts.trigger, 'focus', self._onInputFocus);
  541. addEvent(opts.trigger, 'blur', self._onInputBlur);
  542. } else {
  543. this.show();
  544. }
  545. };
  546. /**
  547. * public Pikaday API
  548. */
  549. Pikaday.prototype = {
  550. /**
  551. * configure functionality
  552. */
  553. config: function(options)
  554. {
  555. if (!this._o) {
  556. this._o = extend({}, defaults, true);
  557. }
  558. var opts = extend(this._o, options, true);
  559. opts.isRTL = !!opts.isRTL;
  560. opts.field = (opts.field && opts.field.nodeName) ? opts.field : null;
  561. opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null;
  562. opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field);
  563. opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field;
  564. opts.disableWeekends = !!opts.disableWeekends;
  565. opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null;
  566. var nom = parseInt(opts.numberOfMonths, 10) || 1;
  567. opts.numberOfMonths = nom > 4 ? 4 : nom;
  568. if (!isDate(opts.minDate)) {
  569. opts.minDate = false;
  570. }
  571. if (!isDate(opts.maxDate)) {
  572. opts.maxDate = false;
  573. }
  574. if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) {
  575. opts.maxDate = opts.minDate = false;
  576. }
  577. if (opts.minDate) {
  578. this.setMinDate(opts.minDate);
  579. }
  580. if (opts.maxDate) {
  581. this.setMaxDate(opts.maxDate);
  582. }
  583. if (isArray(opts.yearRange)) {
  584. var fallback = new Date().getFullYear() - 10;
  585. opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback;
  586. opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback;
  587. } else {
  588. opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange;
  589. if (opts.yearRange > 100) {
  590. opts.yearRange = 100;
  591. }
  592. }
  593. return opts;
  594. },
  595. /**
  596. * return a formatted string of the current selection (using Moment.js if available)
  597. */
  598. toString: function(format)
  599. {
  600. return !isDate(this._d) ? '' : hasMoment ? moment(this._d).format(format || this._o.format) : this._d.toDateString();
  601. },
  602. /**
  603. * return a Moment.js object of the current selection (if available)
  604. */
  605. getMoment: function()
  606. {
  607. return hasMoment ? moment(this._d) : null;
  608. },
  609. /**
  610. * set the current selection from a Moment.js object (if available)
  611. */
  612. setMoment: function(date, preventOnSelect)
  613. {
  614. if (hasMoment && moment.isMoment(date)) {
  615. this.setDate(date.toDate(), preventOnSelect);
  616. }
  617. },
  618. /**
  619. * return a Date object of the current selection with fallback for the current date
  620. */
  621. getDate: function()
  622. {
  623. return isDate(this._d) ? new Date(this._d.getTime()) : new Date();
  624. },
  625. /**
  626. * set the current selection
  627. */
  628. setDate: function(date, preventOnSelect)
  629. {
  630. if (!date) {
  631. this._d = null;
  632. if (this._o.field) {
  633. this._o.field.value = '';
  634. fireEvent(this._o.field, 'change', { firedBy: this });
  635. }
  636. return this.draw();
  637. }
  638. if (typeof date === 'string') {
  639. date = new Date(Date.parse(date));
  640. }
  641. if (!isDate(date)) {
  642. return;
  643. }
  644. var min = this._o.minDate,
  645. max = this._o.maxDate;
  646. if (isDate(min) && date < min) {
  647. date = min;
  648. } else if (isDate(max) && date > max) {
  649. date = max;
  650. }
  651. this._d = new Date(date.getTime());
  652. setToStartOfDay(this._d);
  653. this.gotoDate(this._d);
  654. if (this._o.field) {
  655. this._o.field.value = this.toString();
  656. fireEvent(this._o.field, 'change', { firedBy: this });
  657. }
  658. if (!preventOnSelect && typeof this._o.onSelect === 'function') {
  659. this._o.onSelect.call(this, this.getDate());
  660. }
  661. },
  662. /**
  663. * change view to a specific date
  664. */
  665. gotoDate: function(date)
  666. {
  667. var newCalendar = true;
  668. if (!isDate(date)) {
  669. return;
  670. }
  671. if (this.calendars) {
  672. var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1),
  673. lastVisibleDate = new Date(this.calendars[this.calendars.length-1].year, this.calendars[this.calendars.length-1].month, 1),
  674. visibleDate = date.getTime();
  675. // get the end of the month
  676. lastVisibleDate.setMonth(lastVisibleDate.getMonth()+1);
  677. lastVisibleDate.setDate(lastVisibleDate.getDate()-1);
  678. newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate);
  679. }
  680. if (newCalendar) {
  681. this.calendars = [{
  682. month: date.getMonth(),
  683. year: date.getFullYear()
  684. }];
  685. if (this._o.mainCalendar === 'right') {
  686. this.calendars[0].month += 1 - this._o.numberOfMonths;
  687. }
  688. }
  689. this.adjustCalendars();
  690. },
  691. adjustDate: function(sign, days) {
  692. var day = this.getDate();
  693. var difference = parseInt(days)*24*60*60*1000;
  694. var newDay;
  695. if (sign === 'add') {
  696. newDay = new Date(day.valueOf() + difference);
  697. } else if (sign === 'subtract') {
  698. newDay = new Date(day.valueOf() - difference);
  699. }
  700. if (hasMoment) {
  701. if (sign === 'add') {
  702. newDay = moment(day).add(days, "days").toDate();
  703. } else if (sign === 'subtract') {
  704. newDay = moment(day).subtract(days, "days").toDate();
  705. }
  706. }
  707. this.setDate(newDay);
  708. },
  709. adjustCalendars: function() {
  710. this.calendars[0] = adjustCalendar(this.calendars[0]);
  711. for (var c = 1; c < this._o.numberOfMonths; c++) {
  712. this.calendars[c] = adjustCalendar({
  713. month: this.calendars[0].month + c,
  714. year: this.calendars[0].year
  715. });
  716. }
  717. this.draw();
  718. },
  719. gotoToday: function()
  720. {
  721. this.gotoDate(new Date());
  722. },
  723. /**
  724. * change view to a specific month (zero-index, e.g. 0: January)
  725. */
  726. gotoMonth: function(month)
  727. {
  728. if (!isNaN(month)) {
  729. this.calendars[0].month = parseInt(month, 10);
  730. this.adjustCalendars();
  731. }
  732. },
  733. nextMonth: function()
  734. {
  735. this.calendars[0].month++;
  736. this.adjustCalendars();
  737. },
  738. prevMonth: function()
  739. {
  740. this.calendars[0].month--;
  741. this.adjustCalendars();
  742. },
  743. /**
  744. * change view to a specific full year (e.g. "2012")
  745. */
  746. gotoYear: function(year)
  747. {
  748. if (!isNaN(year)) {
  749. this.calendars[0].year = parseInt(year, 10);
  750. this.adjustCalendars();
  751. }
  752. },
  753. /**
  754. * change the minDate
  755. */
  756. setMinDate: function(value)
  757. {
  758. if(value instanceof Date) {
  759. setToStartOfDay(value);
  760. this._o.minDate = value;
  761. this._o.minYear = value.getFullYear();
  762. this._o.minMonth = value.getMonth();
  763. } else {
  764. this._o.minDate = defaults.minDate;
  765. this._o.minYear = defaults.minYear;
  766. this._o.minMonth = defaults.minMonth;
  767. this._o.startRange = defaults.startRange;
  768. }
  769. this.draw();
  770. },
  771. /**
  772. * change the maxDate
  773. */
  774. setMaxDate: function(value)
  775. {
  776. if(value instanceof Date) {
  777. setToStartOfDay(value);
  778. this._o.maxDate = value;
  779. this._o.maxYear = value.getFullYear();
  780. this._o.maxMonth = value.getMonth();
  781. } else {
  782. this._o.maxDate = defaults.maxDate;
  783. this._o.maxYear = defaults.maxYear;
  784. this._o.maxMonth = defaults.maxMonth;
  785. this._o.endRange = defaults.endRange;
  786. }
  787. this.draw();
  788. },
  789. setStartRange: function(value)
  790. {
  791. this._o.startRange = value;
  792. },
  793. setEndRange: function(value)
  794. {
  795. this._o.endRange = value;
  796. },
  797. /**
  798. * refresh the HTML
  799. */
  800. draw: function(force)
  801. {
  802. if (!this._v && !force) {
  803. return;
  804. }
  805. var opts = this._o,
  806. minYear = opts.minYear,
  807. maxYear = opts.maxYear,
  808. minMonth = opts.minMonth,
  809. maxMonth = opts.maxMonth,
  810. html = '',
  811. randId;
  812. if (this._y <= minYear) {
  813. this._y = minYear;
  814. if (!isNaN(minMonth) && this._m < minMonth) {
  815. this._m = minMonth;
  816. }
  817. }
  818. if (this._y >= maxYear) {
  819. this._y = maxYear;
  820. if (!isNaN(maxMonth) && this._m > maxMonth) {
  821. this._m = maxMonth;
  822. }
  823. }
  824. randId = 'pika-title-' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 2);
  825. for (var c = 0; c < opts.numberOfMonths; c++) {
  826. html += '<div class="pika-lendar">' + renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year, randId) + this.render(this.calendars[c].year, this.calendars[c].month, randId) + '</div>';
  827. }
  828. this.el.innerHTML = html;
  829. if (opts.bound) {
  830. if(opts.field.type !== 'hidden') {
  831. sto(function() {
  832. opts.trigger.focus();
  833. }, 1);
  834. }
  835. }
  836. if (typeof this._o.onDraw === 'function') {
  837. this._o.onDraw(this);
  838. }
  839. if (opts.bound) {
  840. // let the screen reader user know to use arrow keys
  841. opts.field.setAttribute('aria-label', 'Use the arrow keys to pick a date');
  842. }
  843. },
  844. adjustPosition: function()
  845. {
  846. var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect;
  847. if (this._o.container) return;
  848. this.el.style.position = 'absolute';
  849. field = this._o.trigger;
  850. pEl = field;
  851. width = this.el.offsetWidth;
  852. height = this.el.offsetHeight;
  853. viewportWidth = window.innerWidth || document.documentElement.clientWidth;
  854. viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  855. scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
  856. if (typeof field.getBoundingClientRect === 'function') {
  857. clientRect = field.getBoundingClientRect();
  858. left = clientRect.left + window.pageXOffset;
  859. top = clientRect.bottom + window.pageYOffset;
  860. } else {
  861. left = pEl.offsetLeft;
  862. top = pEl.offsetTop + pEl.offsetHeight;
  863. while((pEl = pEl.offsetParent)) {
  864. left += pEl.offsetLeft;
  865. top += pEl.offsetTop;
  866. }
  867. }
  868. // default position is bottom & left
  869. if ((this._o.reposition && left + width > viewportWidth) ||
  870. (
  871. this._o.position.indexOf('right') > -1 &&
  872. left - width + field.offsetWidth > 0
  873. )
  874. ) {
  875. left = left - width + field.offsetWidth;
  876. }
  877. if ((this._o.reposition && top + height > viewportHeight + scrollTop) ||
  878. (
  879. this._o.position.indexOf('top') > -1 &&
  880. top - height - field.offsetHeight > 0
  881. )
  882. ) {
  883. top = top - height - field.offsetHeight;
  884. }
  885. this.el.style.left = left + 'px';
  886. this.el.style.top = top + 'px';
  887. },
  888. /**
  889. * render HTML for a particular month
  890. */
  891. render: function(year, month, randId)
  892. {
  893. var opts = this._o,
  894. now = new Date(),
  895. days = getDaysInMonth(year, month),
  896. before = new Date(year, month, 1).getDay(),
  897. data = [],
  898. row = [];
  899. setToStartOfDay(now);
  900. if (opts.firstDay > 0) {
  901. before -= opts.firstDay;
  902. if (before < 0) {
  903. before += 7;
  904. }
  905. }
  906. var previousMonth = month === 0 ? 11 : month - 1,
  907. nextMonth = month === 11 ? 0 : month + 1,
  908. yearOfPreviousMonth = month === 0 ? year - 1 : year,
  909. yearOfNextMonth = month === 11 ? year + 1 : year,
  910. daysInPreviousMonth = getDaysInMonth(yearOfPreviousMonth, previousMonth);
  911. var cells = days + before,
  912. after = cells;
  913. while(after > 7) {
  914. after -= 7;
  915. }
  916. cells += 7 - after;
  917. for (var i = 0, r = 0; i < cells; i++)
  918. {
  919. var day = new Date(year, month, 1 + (i - before)),
  920. isSelected = isDate(this._d) ? compareDates(day, this._d) : false,
  921. isToday = compareDates(day, now),
  922. isEmpty = i < before || i >= (days + before),
  923. dayNumber = 1 + (i - before),
  924. monthNumber = month,
  925. yearNumber = year,
  926. isStartRange = opts.startRange && compareDates(opts.startRange, day),
  927. isEndRange = opts.endRange && compareDates(opts.endRange, day),
  928. isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange,
  929. isDisabled = (opts.minDate && day < opts.minDate) ||
  930. (opts.maxDate && day > opts.maxDate) ||
  931. (opts.disableWeekends && isWeekend(day)) ||
  932. (opts.disableDayFn && opts.disableDayFn(day));
  933. if (isEmpty) {
  934. if (i < before) {
  935. dayNumber = daysInPreviousMonth + dayNumber;
  936. monthNumber = previousMonth;
  937. yearNumber = yearOfPreviousMonth;
  938. } else {
  939. dayNumber = dayNumber - days;
  940. monthNumber = nextMonth;
  941. yearNumber = yearOfNextMonth;
  942. }
  943. }
  944. var dayConfig = {
  945. day: dayNumber,
  946. month: monthNumber,
  947. year: yearNumber,
  948. isSelected: isSelected,
  949. isToday: isToday,
  950. isDisabled: isDisabled,
  951. isEmpty: isEmpty,
  952. isStartRange: isStartRange,
  953. isEndRange: isEndRange,
  954. isInRange: isInRange,
  955. showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths
  956. };
  957. row.push(renderDay(dayConfig));
  958. if (++r === 7) {
  959. if (opts.showWeekNumber) {
  960. row.unshift(renderWeek(i - before, month, year));
  961. }
  962. data.push(renderRow(row, opts.isRTL));
  963. row = [];
  964. r = 0;
  965. }
  966. }
  967. return renderTable(opts, data, randId);
  968. },
  969. isVisible: function()
  970. {
  971. return this._v;
  972. },
  973. show: function()
  974. {
  975. if (!this.isVisible()) {
  976. removeClass(this.el, 'is-hidden');
  977. this._v = true;
  978. this.draw();
  979. if (this._o.bound) {
  980. addEvent(document, 'click', this._onClick);
  981. this.adjustPosition();
  982. }
  983. if (typeof this._o.onOpen === 'function') {
  984. this._o.onOpen.call(this);
  985. }
  986. }
  987. },
  988. hide: function()
  989. {
  990. var v = this._v;
  991. if (v !== false) {
  992. if (this._o.bound) {
  993. removeEvent(document, 'click', this._onClick);
  994. }
  995. this.el.style.position = 'static'; // reset
  996. this.el.style.left = 'auto';
  997. this.el.style.top = 'auto';
  998. addClass(this.el, 'is-hidden');
  999. this._v = false;
  1000. if (v !== undefined && typeof this._o.onClose === 'function') {
  1001. this._o.onClose.call(this);
  1002. }
  1003. }
  1004. },
  1005. /**
  1006. * GAME OVER
  1007. */
  1008. destroy: function()
  1009. {
  1010. this.hide();
  1011. removeEvent(this.el, 'mousedown', this._onMouseDown, true);
  1012. removeEvent(this.el, 'touchend', this._onMouseDown, true);
  1013. removeEvent(this.el, 'change', this._onChange);
  1014. if (this._o.field) {
  1015. removeEvent(this._o.field, 'change', this._onInputChange);
  1016. if (this._o.bound) {
  1017. removeEvent(this._o.trigger, 'click', this._onInputClick);
  1018. removeEvent(this._o.trigger, 'focus', this._onInputFocus);
  1019. removeEvent(this._o.trigger, 'blur', this._onInputBlur);
  1020. }
  1021. }
  1022. if (this.el.parentNode) {
  1023. this.el.parentNode.removeChild(this.el);
  1024. }
  1025. }
  1026. };
  1027. return Pikaday;
  1028. }));