pikaday.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  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. isLeapYear = function(year)
  94. {
  95. // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951
  96. return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
  97. },
  98. getDaysInMonth = function(year, month)
  99. {
  100. return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
  101. },
  102. setToStartOfDay = function(date)
  103. {
  104. if (isDate(date)) date.setHours(0,0,0,0);
  105. },
  106. compareDates = function(a,b)
  107. {
  108. // weak date comparison (use setToStartOfDay(date) to ensure correct result)
  109. return a.getTime() === b.getTime();
  110. },
  111. extend = function(to, from, overwrite)
  112. {
  113. var prop, hasProp;
  114. for (prop in from) {
  115. hasProp = to[prop] !== undefined;
  116. if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) {
  117. if (isDate(from[prop])) {
  118. if (overwrite) {
  119. to[prop] = new Date(from[prop].getTime());
  120. }
  121. }
  122. else if (isArray(from[prop])) {
  123. if (overwrite) {
  124. to[prop] = from[prop].slice(0);
  125. }
  126. } else {
  127. to[prop] = extend({}, from[prop], overwrite);
  128. }
  129. } else if (overwrite || !hasProp) {
  130. to[prop] = from[prop];
  131. }
  132. }
  133. return to;
  134. },
  135. adjustCalendar = function(calendar) {
  136. if (calendar.month < 0) {
  137. calendar.year -= Math.ceil(Math.abs(calendar.month)/12);
  138. calendar.month += 12;
  139. }
  140. if (calendar.month > 11) {
  141. calendar.year += Math.floor(Math.abs(calendar.month)/12);
  142. calendar.month -= 12;
  143. }
  144. return calendar;
  145. },
  146. /**
  147. * defaults and localisation
  148. */
  149. defaults = {
  150. // bind the picker to a form field
  151. field: null,
  152. // automatically show/hide the picker on `field` focus (default `true` if `field` is set)
  153. bound: undefined,
  154. // position of the datepicker, relative to the field (default to bottom & left)
  155. // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position)
  156. position: 'bottom left',
  157. // automatically fit in the viewport even if it means repositioning from the position option
  158. reposition: true,
  159. // the default output format for `.toString()` and `field` value
  160. format: 'YYYY-MM-DD',
  161. // the initial date to view when first opened
  162. defaultDate: null,
  163. // make the `defaultDate` the initial selected value
  164. setDefaultDate: false,
  165. // first day of week (0: Sunday, 1: Monday etc)
  166. firstDay: 0,
  167. // the minimum/earliest date that can be selected
  168. minDate: null,
  169. // the maximum/latest date that can be selected
  170. maxDate: null,
  171. // number of years either side, or array of upper/lower range
  172. yearRange: 10,
  173. // show week numbers at head of row
  174. showWeekNumber: false,
  175. // used internally (don't config outside)
  176. minYear: 0,
  177. maxYear: 9999,
  178. minMonth: undefined,
  179. maxMonth: undefined,
  180. isRTL: false,
  181. // Additional text to append to the year in the calendar title
  182. yearSuffix: '',
  183. // Render the month after year in the calendar title
  184. showMonthAfterYear: false,
  185. // how many months are visible
  186. numberOfMonths: 1,
  187. // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`)
  188. // only used for the first display or when a selected date is not visible
  189. mainCalendar: 'left',
  190. // Specify a DOM element to render the calendar in
  191. container: undefined,
  192. // internationalization
  193. i18n: {
  194. previousMonth : 'Previous Month',
  195. nextMonth : 'Next Month',
  196. months : ['January','February','March','April','May','June','July','August','September','October','November','December'],
  197. weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
  198. weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
  199. },
  200. // callback function
  201. onSelect: null,
  202. onOpen: null,
  203. onClose: null,
  204. onDraw: null
  205. },
  206. /**
  207. * templating functions to abstract HTML rendering
  208. */
  209. renderDayName = function(opts, day, abbr)
  210. {
  211. day += opts.firstDay;
  212. while (day >= 7) {
  213. day -= 7;
  214. }
  215. return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day];
  216. },
  217. renderDay = function(d, m, y, isSelected, isToday, isDisabled, isEmpty)
  218. {
  219. if (isEmpty) {
  220. return '<td class="is-empty"></td>';
  221. }
  222. var arr = [];
  223. if (isDisabled) {
  224. arr.push('is-disabled');
  225. }
  226. if (isToday) {
  227. arr.push('is-today');
  228. }
  229. if (isSelected) {
  230. arr.push('is-selected');
  231. }
  232. return '<td data-day="' + d + '" class="' + arr.join(' ') + '">' +
  233. '<button class="pika-button pika-day" type="button" ' +
  234. 'data-pika-year="' + y + '" data-pika-month="' + m + '" data-pika-day="' + d + '">' +
  235. d +
  236. '</button>' +
  237. '</td>';
  238. },
  239. renderWeek = function (d, m, y) {
  240. // Lifted from http://javascript.about.com/library/blweekyear.htm, lightly modified.
  241. var onejan = new Date(y, 0, 1),
  242. weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getDay()+1)/7);
  243. return '<td class="pika-week">' + weekNum + '</td>';
  244. },
  245. renderRow = function(days, isRTL)
  246. {
  247. return '<tr>' + (isRTL ? days.reverse() : days).join('') + '</tr>';
  248. },
  249. renderBody = function(rows)
  250. {
  251. return '<tbody>' + rows.join('') + '</tbody>';
  252. },
  253. renderHead = function(opts)
  254. {
  255. var i, arr = [];
  256. if (opts.showWeekNumber) {
  257. arr.push('<th></th>');
  258. }
  259. for (i = 0; i < 7; i++) {
  260. arr.push('<th scope="col"><abbr title="' + renderDayName(opts, i) + '">' + renderDayName(opts, i, true) + '</abbr></th>');
  261. }
  262. return '<thead>' + (opts.isRTL ? arr.reverse() : arr).join('') + '</thead>';
  263. },
  264. renderTitle = function(instance, c, year, month, refYear)
  265. {
  266. var i, j, arr,
  267. opts = instance._o,
  268. isMinYear = year === opts.minYear,
  269. isMaxYear = year === opts.maxYear,
  270. html = '<div class="pika-title">',
  271. monthHtml,
  272. yearHtml,
  273. prev = true,
  274. next = true;
  275. for (arr = [], i = 0; i < 12; i++) {
  276. arr.push('<option value="' + (year === refYear ? i - c : 12 + i - c) + '"' +
  277. (i === month ? ' selected': '') +
  278. ((isMinYear && i < opts.minMonth) || (isMaxYear && i > opts.maxMonth) ? 'disabled' : '') + '>' +
  279. opts.i18n.months[i] + '</option>');
  280. }
  281. monthHtml = '<div class="pika-label">' + opts.i18n.months[month] + '<select class="pika-select pika-select-month">' + arr.join('') + '</select></div>';
  282. if (isArray(opts.yearRange)) {
  283. i = opts.yearRange[0];
  284. j = opts.yearRange[1] + 1;
  285. } else {
  286. i = year - opts.yearRange;
  287. j = 1 + year + opts.yearRange;
  288. }
  289. for (arr = []; i < j && i <= opts.maxYear; i++) {
  290. if (i >= opts.minYear) {
  291. arr.push('<option value="' + i + '"' + (i === year ? ' selected': '') + '>' + (i) + '</option>');
  292. }
  293. }
  294. yearHtml = '<div class="pika-label">' + year + opts.yearSuffix + '<select class="pika-select pika-select-year">' + arr.join('') + '</select></div>';
  295. if (opts.showMonthAfterYear) {
  296. html += yearHtml + monthHtml;
  297. } else {
  298. html += monthHtml + yearHtml;
  299. }
  300. if (isMinYear && (month === 0 || opts.minMonth >= month)) {
  301. prev = false;
  302. }
  303. if (isMaxYear && (month === 11 || opts.maxMonth <= month)) {
  304. next = false;
  305. }
  306. if (c === 0) {
  307. html += '<button class="pika-prev' + (prev ? '' : ' is-disabled') + '" type="button">' + opts.i18n.previousMonth + '</button>';
  308. }
  309. if (c === (instance._o.numberOfMonths - 1) ) {
  310. html += '<button class="pika-next' + (next ? '' : ' is-disabled') + '" type="button">' + opts.i18n.nextMonth + '</button>';
  311. }
  312. return html += '</div>';
  313. },
  314. renderTable = function(opts, data)
  315. {
  316. return '<table cellpadding="0" cellspacing="0" class="pika-table">' + renderHead(opts) + renderBody(data) + '</table>';
  317. },
  318. /**
  319. * Pikaday constructor
  320. */
  321. Pikaday = function(options)
  322. {
  323. var self = this,
  324. opts = self.config(options);
  325. self._onMouseDown = function(e)
  326. {
  327. if (!self._v) {
  328. return;
  329. }
  330. e = e || window.event;
  331. var target = e.target || e.srcElement;
  332. if (!target) {
  333. return;
  334. }
  335. if (!hasClass(target, 'is-disabled')) {
  336. if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty')) {
  337. self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day')));
  338. if (opts.bound) {
  339. sto(function() {
  340. self.hide();
  341. if (opts.field) {
  342. opts.field.blur();
  343. }
  344. }, 100);
  345. }
  346. return;
  347. }
  348. else if (hasClass(target, 'pika-prev')) {
  349. self.prevMonth();
  350. }
  351. else if (hasClass(target, 'pika-next')) {
  352. self.nextMonth();
  353. }
  354. }
  355. if (!hasClass(target, 'pika-select')) {
  356. if (e.preventDefault) {
  357. e.preventDefault();
  358. } else {
  359. e.returnValue = false;
  360. return false;
  361. }
  362. } else {
  363. self._c = true;
  364. }
  365. };
  366. self._onChange = function(e)
  367. {
  368. e = e || window.event;
  369. var target = e.target || e.srcElement;
  370. if (!target) {
  371. return;
  372. }
  373. if (hasClass(target, 'pika-select-month')) {
  374. self.gotoMonth(target.value);
  375. }
  376. else if (hasClass(target, 'pika-select-year')) {
  377. self.gotoYear(target.value);
  378. }
  379. };
  380. self._onInputChange = function(e)
  381. {
  382. var date;
  383. if (e.firedBy === self) {
  384. return;
  385. }
  386. if (hasMoment) {
  387. date = moment(opts.field.value, opts.format);
  388. date = (date && date.isValid()) ? date.toDate() : null;
  389. }
  390. else {
  391. date = new Date(Date.parse(opts.field.value));
  392. }
  393. self.setDate(isDate(date) ? date : null);
  394. if (!self._v) {
  395. self.show();
  396. }
  397. };
  398. self._onInputFocus = function()
  399. {
  400. self.show();
  401. };
  402. self._onInputClick = function()
  403. {
  404. self.show();
  405. };
  406. self._onInputBlur = function()
  407. {
  408. if (!self._c) {
  409. self._b = sto(function() {
  410. self.hide();
  411. }, 50);
  412. }
  413. self._c = false;
  414. };
  415. self._onClick = function(e)
  416. {
  417. e = e || window.event;
  418. var target = e.target || e.srcElement,
  419. pEl = target;
  420. if (!target) {
  421. return;
  422. }
  423. if (!hasEventListeners && hasClass(target, 'pika-select')) {
  424. if (!target.onchange) {
  425. target.setAttribute('onchange', 'return;');
  426. addEvent(target, 'change', self._onChange);
  427. }
  428. }
  429. do {
  430. if (pEl === opts.trigger) {
  431. return;
  432. }
  433. }
  434. while ((pEl = pEl.parentNode));
  435. if (self._v && pEl !== opts.trigger) {
  436. self.hide();
  437. }
  438. };
  439. self.el = document.createElement('div');
  440. self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '');
  441. addEvent(self.el, 'mousedown', self._onMouseDown, true);
  442. addEvent(self.el, 'change', self._onChange);
  443. if (opts.field) {
  444. if (opts.container) {
  445. opts.container.appendChild(self.el);
  446. } else if (opts.bound) {
  447. document.body.appendChild(self.el);
  448. } else {
  449. opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling);
  450. }
  451. addEvent(opts.field, 'change', self._onInputChange);
  452. if (!opts.defaultDate) {
  453. if (hasMoment && opts.field.value) {
  454. opts.defaultDate = moment(opts.field.value, opts.format).toDate();
  455. } else {
  456. opts.defaultDate = new Date(Date.parse(opts.field.value));
  457. }
  458. opts.setDefaultDate = true;
  459. }
  460. }
  461. var defDate = opts.defaultDate;
  462. if (isDate(defDate)) {
  463. if (opts.setDefaultDate) {
  464. self.setDate(defDate, true);
  465. } else {
  466. self.gotoDate(defDate);
  467. }
  468. } else {
  469. self.gotoDate(new Date());
  470. }
  471. if (opts.bound) {
  472. this.hide();
  473. self.el.className += ' is-bound';
  474. addEvent(opts.trigger, 'click', self._onInputClick);
  475. addEvent(opts.trigger, 'focus', self._onInputFocus);
  476. addEvent(opts.trigger, 'blur', self._onInputBlur);
  477. } else {
  478. this.show();
  479. }
  480. };
  481. /**
  482. * public Pikaday API
  483. */
  484. Pikaday.prototype = {
  485. /**
  486. * configure functionality
  487. */
  488. config: function(options)
  489. {
  490. if (!this._o) {
  491. this._o = extend({}, defaults, true);
  492. }
  493. var opts = extend(this._o, options, true);
  494. opts.isRTL = !!opts.isRTL;
  495. opts.field = (opts.field && opts.field.nodeName) ? opts.field : null;
  496. opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field);
  497. opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field;
  498. var nom = parseInt(opts.numberOfMonths, 10) || 1;
  499. opts.numberOfMonths = nom > 4 ? 4 : nom;
  500. if (!isDate(opts.minDate)) {
  501. opts.minDate = false;
  502. }
  503. if (!isDate(opts.maxDate)) {
  504. opts.maxDate = false;
  505. }
  506. if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) {
  507. opts.maxDate = opts.minDate = false;
  508. }
  509. if (opts.minDate) {
  510. setToStartOfDay(opts.minDate);
  511. opts.minYear = opts.minDate.getFullYear();
  512. opts.minMonth = opts.minDate.getMonth();
  513. }
  514. if (opts.maxDate) {
  515. setToStartOfDay(opts.maxDate);
  516. opts.maxYear = opts.maxDate.getFullYear();
  517. opts.maxMonth = opts.maxDate.getMonth();
  518. }
  519. if (isArray(opts.yearRange)) {
  520. var fallback = new Date().getFullYear() - 10;
  521. opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback;
  522. opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback;
  523. } else {
  524. opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange;
  525. if (opts.yearRange > 100) {
  526. opts.yearRange = 100;
  527. }
  528. }
  529. return opts;
  530. },
  531. /**
  532. * return a formatted string of the current selection (using Moment.js if available)
  533. */
  534. toString: function(format)
  535. {
  536. return !isDate(this._d) ? '' : hasMoment ? moment(this._d).format(format || this._o.format) : this._d.toDateString();
  537. },
  538. /**
  539. * return a Moment.js object of the current selection (if available)
  540. */
  541. getMoment: function()
  542. {
  543. return hasMoment ? moment(this._d) : null;
  544. },
  545. /**
  546. * set the current selection from a Moment.js object (if available)
  547. */
  548. setMoment: function(date, preventOnSelect)
  549. {
  550. if (hasMoment && moment.isMoment(date)) {
  551. this.setDate(date.toDate(), preventOnSelect);
  552. }
  553. },
  554. /**
  555. * return a Date object of the current selection
  556. */
  557. getDate: function()
  558. {
  559. return isDate(this._d) ? new Date(this._d.getTime()) : null;
  560. },
  561. /**
  562. * set the current selection
  563. */
  564. setDate: function(date, preventOnSelect)
  565. {
  566. if (!date) {
  567. this._d = null;
  568. if (this._o.field) {
  569. this._o.field.value = '';
  570. fireEvent(this._o.field, 'change', { firedBy: this });
  571. }
  572. return this.draw();
  573. }
  574. if (typeof date === 'string') {
  575. date = new Date(Date.parse(date));
  576. }
  577. if (!isDate(date)) {
  578. return;
  579. }
  580. var min = this._o.minDate,
  581. max = this._o.maxDate;
  582. if (isDate(min) && date < min) {
  583. date = min;
  584. } else if (isDate(max) && date > max) {
  585. date = max;
  586. }
  587. this._d = new Date(date.getTime());
  588. setToStartOfDay(this._d);
  589. this.gotoDate(this._d);
  590. if (this._o.field) {
  591. this._o.field.value = this.toString();
  592. fireEvent(this._o.field, 'change', { firedBy: this });
  593. }
  594. if (!preventOnSelect && typeof this._o.onSelect === 'function') {
  595. this._o.onSelect.call(this, this.getDate());
  596. }
  597. },
  598. /**
  599. * change view to a specific date
  600. */
  601. gotoDate: function(date)
  602. {
  603. var newCalendar = true;
  604. if (!isDate(date)) {
  605. return;
  606. }
  607. if (this.calendars) {
  608. var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1),
  609. lastVisibleDate = new Date(this.calendars[this.calendars.length-1].year, this.calendars[this.calendars.length-1].month, 1),
  610. visibleDate = date.getTime();
  611. // get the end of the month
  612. lastVisibleDate.setMonth(lastVisibleDate.getMonth()+1);
  613. lastVisibleDate.setDate(lastVisibleDate.getDate()-1);
  614. newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate);
  615. }
  616. if (newCalendar) {
  617. this.calendars = [{
  618. month: date.getMonth(),
  619. year: date.getFullYear()
  620. }];
  621. if (this._o.mainCalendar === 'right') {
  622. this.calendars[0].month += 1 - this._o.numberOfMonths;
  623. }
  624. }
  625. this.adjustCalendars();
  626. },
  627. adjustCalendars: function() {
  628. this.calendars[0] = adjustCalendar(this.calendars[0]);
  629. for (var c = 1; c < this._o.numberOfMonths; c++) {
  630. this.calendars[c] = adjustCalendar({
  631. month: this.calendars[0].month + c,
  632. year: this.calendars[0].year
  633. });
  634. }
  635. this.draw();
  636. },
  637. gotoToday: function()
  638. {
  639. this.gotoDate(new Date());
  640. },
  641. /**
  642. * change view to a specific month (zero-index, e.g. 0: January)
  643. */
  644. gotoMonth: function(month)
  645. {
  646. if (!isNaN(month)) {
  647. this.calendars[0].month = parseInt(month, 10);
  648. this.adjustCalendars();
  649. }
  650. },
  651. nextMonth: function()
  652. {
  653. this.calendars[0].month++;
  654. this.adjustCalendars();
  655. },
  656. prevMonth: function()
  657. {
  658. this.calendars[0].month--;
  659. this.adjustCalendars();
  660. },
  661. /**
  662. * change view to a specific full year (e.g. "2012")
  663. */
  664. gotoYear: function(year)
  665. {
  666. if (!isNaN(year)) {
  667. this.calendars[0].year = parseInt(year, 10);
  668. this.adjustCalendars();
  669. }
  670. },
  671. /**
  672. * change the minDate
  673. */
  674. setMinDate: function(value)
  675. {
  676. this._o.minDate = value;
  677. },
  678. /**
  679. * change the maxDate
  680. */
  681. setMaxDate: function(value)
  682. {
  683. this._o.maxDate = value;
  684. },
  685. /**
  686. * refresh the HTML
  687. */
  688. draw: function(force)
  689. {
  690. if (!this._v && !force) {
  691. return;
  692. }
  693. var opts = this._o,
  694. minYear = opts.minYear,
  695. maxYear = opts.maxYear,
  696. minMonth = opts.minMonth,
  697. maxMonth = opts.maxMonth,
  698. html = '';
  699. if (this._y <= minYear) {
  700. this._y = minYear;
  701. if (!isNaN(minMonth) && this._m < minMonth) {
  702. this._m = minMonth;
  703. }
  704. }
  705. if (this._y >= maxYear) {
  706. this._y = maxYear;
  707. if (!isNaN(maxMonth) && this._m > maxMonth) {
  708. this._m = maxMonth;
  709. }
  710. }
  711. for (var c = 0; c < opts.numberOfMonths; c++) {
  712. html += '<div class="pika-lendar">' + renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year) + this.render(this.calendars[c].year, this.calendars[c].month) + '</div>';
  713. }
  714. this.el.innerHTML = html;
  715. if (opts.bound) {
  716. if(opts.field.type !== 'hidden') {
  717. sto(function() {
  718. opts.trigger.focus();
  719. }, 1);
  720. }
  721. }
  722. if (typeof this._o.onDraw === 'function') {
  723. var self = this;
  724. sto(function() {
  725. self._o.onDraw.call(self);
  726. }, 0);
  727. }
  728. },
  729. adjustPosition: function()
  730. {
  731. if (this._o.container) return;
  732. var field = this._o.trigger, pEl = field,
  733. width = this.el.offsetWidth, height = this.el.offsetHeight,
  734. viewportWidth = window.innerWidth || document.documentElement.clientWidth,
  735. viewportHeight = window.innerHeight || document.documentElement.clientHeight,
  736. scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop,
  737. left, top, clientRect;
  738. if (typeof field.getBoundingClientRect === 'function') {
  739. clientRect = field.getBoundingClientRect();
  740. left = clientRect.left + window.pageXOffset;
  741. top = clientRect.bottom + window.pageYOffset;
  742. } else {
  743. left = pEl.offsetLeft;
  744. top = pEl.offsetTop + pEl.offsetHeight;
  745. while((pEl = pEl.offsetParent)) {
  746. left += pEl.offsetLeft;
  747. top += pEl.offsetTop;
  748. }
  749. }
  750. // default position is bottom & left
  751. if ((this._o.reposition && left + width > viewportWidth) ||
  752. (
  753. this._o.position.indexOf('right') > -1 &&
  754. left - width + field.offsetWidth > 0
  755. )
  756. ) {
  757. left = left - width + field.offsetWidth;
  758. }
  759. if ((this._o.reposition && top + height > viewportHeight + scrollTop) ||
  760. (
  761. this._o.position.indexOf('top') > -1 &&
  762. top - height - field.offsetHeight > 0
  763. )
  764. ) {
  765. top = top - height - field.offsetHeight;
  766. }
  767. this.el.style.cssText = [
  768. 'position: absolute',
  769. 'left: ' + left + 'px',
  770. 'top: ' + top + 'px'
  771. ].join(';');
  772. },
  773. /**
  774. * render HTML for a particular month
  775. */
  776. render: function(year, month)
  777. {
  778. var opts = this._o,
  779. now = new Date(),
  780. days = getDaysInMonth(year, month),
  781. before = new Date(year, month, 1).getDay(),
  782. data = [],
  783. row = [];
  784. setToStartOfDay(now);
  785. if (opts.firstDay > 0) {
  786. before -= opts.firstDay;
  787. if (before < 0) {
  788. before += 7;
  789. }
  790. }
  791. var cells = days + before,
  792. after = cells;
  793. while(after > 7) {
  794. after -= 7;
  795. }
  796. cells += 7 - after;
  797. for (var i = 0, r = 0; i < cells; i++)
  798. {
  799. var day = new Date(year, month, 1 + (i - before)),
  800. isDisabled = (opts.minDate && day < opts.minDate) || (opts.maxDate && day > opts.maxDate),
  801. isSelected = isDate(this._d) ? compareDates(day, this._d) : false,
  802. isToday = compareDates(day, now),
  803. isEmpty = i < before || i >= (days + before);
  804. row.push(renderDay(1 + (i - before), month, year, isSelected, isToday, isDisabled, isEmpty));
  805. if (++r === 7) {
  806. if (opts.showWeekNumber) {
  807. row.unshift(renderWeek(i - before, month, year));
  808. }
  809. data.push(renderRow(row, opts.isRTL));
  810. row = [];
  811. r = 0;
  812. }
  813. }
  814. return renderTable(opts, data);
  815. },
  816. isVisible: function()
  817. {
  818. return this._v;
  819. },
  820. show: function()
  821. {
  822. if (!this._v) {
  823. removeClass(this.el, 'is-hidden');
  824. this._v = true;
  825. this.draw();
  826. if (this._o.bound) {
  827. addEvent(document, 'click', this._onClick);
  828. this.adjustPosition();
  829. }
  830. if (typeof this._o.onOpen === 'function') {
  831. this._o.onOpen.call(this);
  832. }
  833. }
  834. },
  835. hide: function()
  836. {
  837. var v = this._v;
  838. if (v !== false) {
  839. if (this._o.bound) {
  840. removeEvent(document, 'click', this._onClick);
  841. }
  842. this.el.style.cssText = '';
  843. addClass(this.el, 'is-hidden');
  844. this._v = false;
  845. if (v !== undefined && typeof this._o.onClose === 'function') {
  846. this._o.onClose.call(this);
  847. }
  848. }
  849. },
  850. /**
  851. * GAME OVER
  852. */
  853. destroy: function()
  854. {
  855. this.hide();
  856. removeEvent(this.el, 'mousedown', this._onMouseDown, true);
  857. removeEvent(this.el, 'change', this._onChange);
  858. if (this._o.field) {
  859. removeEvent(this._o.field, 'change', this._onInputChange);
  860. if (this._o.bound) {
  861. removeEvent(this._o.trigger, 'click', this._onInputClick);
  862. removeEvent(this._o.trigger, 'focus', this._onInputFocus);
  863. removeEvent(this._o.trigger, 'blur', this._onInputBlur);
  864. }
  865. }
  866. if (this.el.parentNode) {
  867. this.el.parentNode.removeChild(this.el);
  868. }
  869. }
  870. };
  871. return Pikaday;
  872. }));