flatpickr.js 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307
  1. /*! flatpickr v2.0, @license MIT */
  2. function Flatpickr(element, config) {
  3. const self = this;
  4. function init() {
  5. self.element = element;
  6. self.instanceConfig = config || {};
  7. self.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
  8. .test(navigator.userAgent);
  9. setupFormats();
  10. parseConfig();
  11. setupInputs();
  12. setupDates();
  13. setupHelperFunctions();
  14. self.changeMonth = changeMonth;
  15. self.clear = clear;
  16. self.close = close;
  17. self.destroy = destroy;
  18. self.formatDate = formatDate;
  19. self.jumpToDate = jumpToDate;
  20. self.open = open;
  21. self.parseDate = parseDate;
  22. self.redraw = redraw;
  23. self.set = set;
  24. self.setDate = setDate;
  25. self.toggle = toggle;
  26. if (self.isMobile && !self.config.disableMobile) {
  27. bind();
  28. setupMobile();
  29. }
  30. else {
  31. build();
  32. bind();
  33. }
  34. if (self.selectedDateObj)
  35. updateValue();
  36. triggerEvent("Ready");
  37. }
  38. function updateTime(e) {
  39. timeWrapper(e);
  40. updateValue(e);
  41. }
  42. function bind() {
  43. if (self.config.wrap) {
  44. ["open", "close", "toggle", "clear"].forEach(el => {
  45. try {
  46. self.element.querySelector(`[data-${el}]`)
  47. .addEventListener("click", self[el]);
  48. }
  49. catch (e) {
  50. //
  51. }
  52. });
  53. }
  54. if (self.isMobile)
  55. return;
  56. document.addEventListener("keydown", onKeyDown);
  57. window.addEventListener("resize", debounce(onResize, 300));
  58. document.addEventListener("click", documentClick);
  59. document.addEventListener("blur", documentClick);
  60. if (self.config.clickOpens)
  61. (self.altInput || self.input).addEventListener("focus", open);
  62. if (!self.config.noCalendar) {
  63. self.prevMonthNav.addEventListener("click", () => changeMonth(-1));
  64. self.nextMonthNav.addEventListener("click", () => changeMonth(1));
  65. self.currentYearElement.addEventListener("wheel", yearScroll);
  66. self.currentYearElement.addEventListener("focus", () => {
  67. self.currentYearElement.select();
  68. });
  69. self.currentYearElement.addEventListener("input", event => {
  70. if (event.target.value.length === 4)
  71. self.currentYearElement.blur();
  72. self.currentYear = parseInt(event.target.value, 10) || self.currentYear;
  73. self.redraw();
  74. });
  75. self.days.addEventListener("click", selectDate);
  76. }
  77. if (self.config.enableTime) {
  78. self.timeContainer.addEventListener("wheel", updateTime);
  79. self.timeContainer.addEventListener("wheel", debounce(() => triggerEvent("Change"), 1000));
  80. self.timeContainer.addEventListener("input", updateTime);
  81. self.hourElement.addEventListener("focus", () => self.hourElement.select());
  82. self.minuteElement.addEventListener("focus", () => self.minuteElement.select());
  83. if (self.secondElement)
  84. self.secondElement.addEventListener("focus", () => self.secondElement.select());
  85. if (self.amPM)
  86. self.amPM.addEventListener("click", updateTime);
  87. }
  88. }
  89. function jumpToDate(jumpDate) {
  90. jumpDate = jumpDate
  91. ? parseDate(jumpDate)
  92. : self.selectedDateObj || self.config.defaultDate || self.config.minDate || self.now;
  93. self.currentYear = jumpDate.getFullYear();
  94. self.currentMonth = jumpDate.getMonth();
  95. self.redraw();
  96. }
  97. function build() {
  98. const fragment = document.createDocumentFragment();
  99. self.calendarContainer = createElement("div", "flatpickr-calendar");
  100. if (!self.config.noCalendar) {
  101. fragment.appendChild(buildMonthNav());
  102. if (self.config.weekNumbers)
  103. fragment.appendChild(buildWeeks());
  104. self.rContainer = createElement("div", "flatpickr-rContainer");
  105. self.rContainer.appendChild(buildWeekdays());
  106. self.rContainer.appendChild(buildDays());
  107. fragment.appendChild(self.rContainer);
  108. }
  109. if (self.config.enableTime)
  110. fragment.appendChild(buildTime());
  111. self.calendarContainer.appendChild(fragment);
  112. if (self.config.inline || self.config.static) {
  113. self.calendarContainer.classList.add(self.config.inline ? "inline" : "static");
  114. positionCalendar();
  115. self.element.parentNode.appendChild(self.calendarContainer);
  116. }
  117. else
  118. document.body.appendChild(self.calendarContainer);
  119. }
  120. function buildDays() {
  121. if (!self.days) {
  122. self.days = createElement("div", "flatpickr-days");
  123. self.days.tabIndex = -1;
  124. }
  125. const firstOfMonth = (
  126. new Date(self.currentYear, self.currentMonth, 1).getDay() -
  127. Flatpickr.l10n.firstDayOfWeek + 7
  128. ) % 7,
  129. daysInMonth = self.utils.getDaysinMonth(),
  130. prevMonthDays = self.utils.getDaysinMonth((self.currentMonth - 1 + 12) % 12),
  131. days = document.createDocumentFragment();
  132. let dayNumber = prevMonthDays + 1 - firstOfMonth,
  133. currentDate,
  134. dateIsDisabled;
  135. if (self.config.weekNumbers)
  136. self.weekNumbers.innerHTML = "";
  137. self.days.innerHTML = "";
  138. // prepend days from the ending of previous month
  139. for (; dayNumber <= prevMonthDays; dayNumber++) {
  140. const curDate = new Date(self.currentYear, self.currentMonth - 1, dayNumber, 0, 0, 0, 0, 0),
  141. dateIsEnabled = isEnabled(curDate),
  142. dayElem = createElement(
  143. "span",
  144. "flatpickr-day prevMonthDay" + (dateIsEnabled ? "" : " disabled"),
  145. dayNumber
  146. );
  147. if (dateIsEnabled)
  148. dayElem.tabIndex = 0;
  149. days.appendChild(dayElem);
  150. }
  151. // Start at 1 since there is no 0th day
  152. for (dayNumber = 1; dayNumber <= daysInMonth; dayNumber++) {
  153. currentDate = new Date(self.currentYear, self.currentMonth, dayNumber, 0, 0, 0, 0, 0);
  154. if (self.config.weekNumbers && dayNumber % 7 === 1) {
  155. self.weekNumbers.insertAdjacentHTML(
  156. "beforeend",
  157. "<span class='disabled flatpickr-day'>" + self.getWeek(currentDate) + "</span>"
  158. );
  159. }
  160. dateIsDisabled = !isEnabled(currentDate);
  161. const dayElement = createElement(
  162. "span",
  163. dateIsDisabled ? "flatpickr-day disabled" : "flatpickr-day",
  164. dayNumber
  165. );
  166. if (!dateIsDisabled) {
  167. dayElement.tabIndex = 0;
  168. if (equalDates(currentDate, new Date()))
  169. dayElement.classList.add("today");
  170. if (self.selectedDateObj && equalDates(currentDate, self.selectedDateObj)) {
  171. dayElement.classList.add("selected");
  172. self.selectedDateElem = dayElement;
  173. }
  174. }
  175. days.appendChild(dayElement);
  176. }
  177. // append days from the next month
  178. for (let dayNum = daysInMonth + 1; dayNum <= 42 - firstOfMonth; dayNum++) {
  179. const curDate = new Date(
  180. self.currentYear,
  181. self.currentMonth + 1,
  182. dayNum % daysInMonth,
  183. 0, 0, 0, 0, 0
  184. ),
  185. dateIsEnabled = isEnabled(curDate),
  186. dayElement = createElement(
  187. "span",
  188. "flatpickr-day nextMonthDay" + (dateIsEnabled ? "" : " disabled"),
  189. dayNum % daysInMonth
  190. );
  191. if (self.config.weekNumbers && dayNum % 7 === 1) {
  192. self.weekNumbers.insertAdjacentHTML(
  193. "beforeend",
  194. "<span class='disabled flatpickr-day'>" + self.getWeek(curDate) + "</span>"
  195. );
  196. }
  197. if (dateIsEnabled)
  198. dayElement.tabIndex = 0;
  199. days.appendChild(dayElement);
  200. }
  201. self.days.appendChild(days);
  202. return self.days;
  203. }
  204. function buildMonthNav() {
  205. const monthNavFragment = document.createDocumentFragment();
  206. self.monthNav = createElement("div", "flatpickr-month");
  207. self.prevMonthNav = createElement("span", "flatpickr-prev-month");
  208. self.prevMonthNav.innerHTML = self.config.prevArrow;
  209. self.currentMonthElement = createElement("span", "cur_month");
  210. self.currentYearElement = createElement("input", "cur_year");
  211. self.currentYearElement.type = "number";
  212. self.currentYearElement.title = Flatpickr.l10n.scrollTitle;
  213. self.nextMonthNav = createElement("span", "flatpickr-next-month");
  214. self.nextMonthNav.innerHTML = self.config.nextArrow;
  215. self.navigationCurrentMonth = createElement("span", "flatpickr-current-month");
  216. self.navigationCurrentMonth.appendChild(self.currentMonthElement);
  217. self.navigationCurrentMonth.appendChild(self.currentYearElement);
  218. monthNavFragment.appendChild(self.prevMonthNav);
  219. monthNavFragment.appendChild(self.navigationCurrentMonth);
  220. monthNavFragment.appendChild(self.nextMonthNav);
  221. self.monthNav.appendChild(monthNavFragment);
  222. updateNavigationCurrentMonth();
  223. return self.monthNav;
  224. }
  225. function buildTime() {
  226. self.calendarContainer.classList.add("hasTime");
  227. self.timeContainer = createElement("div", "flatpickr-time");
  228. self.timeContainer.tabIndex = -1;
  229. const separator = createElement("span", "flatpickr-time-separator", ":");
  230. self.hourElement = createElement("input", "flatpickr-hour");
  231. self.minuteElement = createElement("input", "flatpickr-minute");
  232. self.hourElement.tabIndex = self.minuteElement.tabIndex = 0;
  233. self.hourElement.type = self.minuteElement.type = "number";
  234. self.hourElement.value =
  235. self.selectedDateObj ? pad(self.selectedDateObj.getHours()) : 12;
  236. self.minuteElement.value =
  237. self.selectedDateObj ? pad(self.selectedDateObj.getMinutes()) : "00";
  238. self.hourElement.step = self.config.hourIncrement;
  239. self.minuteElement.step = self.config.minuteIncrement;
  240. self.hourElement.min = -(self.config.time_24hr ? 1 : 0);
  241. self.hourElement.max = self.config.time_24hr ? 24 : 13;
  242. self.minuteElement.min = -self.minuteElement.step;
  243. self.minuteElement.max = 60;
  244. self.hourElement.title = self.minuteElement.title = Flatpickr.l10n.scrollTitle;
  245. self.timeContainer.appendChild(self.hourElement);
  246. self.timeContainer.appendChild(separator);
  247. self.timeContainer.appendChild(self.minuteElement);
  248. if (self.config.enableSeconds) {
  249. self.timeContainer.classList.add("has-seconds");
  250. self.secondElement = createElement("input", "flatpickr-second");
  251. self.secondElement.type = "number";
  252. self.secondElement.value =
  253. self.selectedDateObj ? pad(self.selectedDateObj.getSeconds()) : "00";
  254. self.secondElement.step = self.minuteElement.step;
  255. self.secondElement.min = self.minuteElement.min;
  256. self.secondElement.max = self.minuteElement.max;
  257. self.timeContainer.appendChild(
  258. createElement("span", "flatpickr-time-separator", ":")
  259. );
  260. self.timeContainer.appendChild(self.secondElement);
  261. }
  262. if (!self.config.time_24hr) { // add self.amPM if appropriate
  263. self.amPM = createElement(
  264. "span",
  265. "flatpickr-am-pm",
  266. ["AM", "PM"][(self.hourElement.value > 11) | 0]
  267. );
  268. self.amPM.title = Flatpickr.l10n.toggleTitle;
  269. self.amPM.tabIndex = 0;
  270. self.timeContainer.appendChild(self.amPM);
  271. }
  272. return self.timeContainer;
  273. }
  274. function buildWeekdays() {
  275. if (!self.weekdayContainer)
  276. self.weekdayContainer = createElement("div", "flatpickr-weekdays");
  277. const firstDayOfWeek = Flatpickr.l10n.firstDayOfWeek;
  278. let weekdays = Flatpickr.l10n.weekdays.shorthand.slice();
  279. if (firstDayOfWeek > 0 && firstDayOfWeek < weekdays.length) {
  280. weekdays = [].concat(
  281. weekdays.splice(firstDayOfWeek, weekdays.length),
  282. weekdays.splice(0, firstDayOfWeek)
  283. );
  284. }
  285. self.weekdayContainer.innerHTML = `<span class=flatpickr-weekday>${weekdays.join("</span><span class=flatpickr-weekday>")}</span>`;
  286. return self.weekdayContainer;
  287. }
  288. function buildWeeks() {
  289. self.calendarContainer.classList.add("hasWeeks");
  290. self.weekWrapper = createElement("div", "flatpickr-weekwrapper");
  291. self.weekWrapper.appendChild(
  292. createElement("span", "flatpickr-weekday", Flatpickr.l10n.weekAbbreviation)
  293. );
  294. self.weekNumbers = createElement("div", "flatpickr-weeks");
  295. self.weekWrapper.appendChild(self.weekNumbers);
  296. return self.weekWrapper;
  297. }
  298. function changeMonth(offset) {
  299. self.currentMonth += offset;
  300. handleYearChange();
  301. updateNavigationCurrentMonth();
  302. buildDays();
  303. (self.config.noCalendar ? self.timeContainer : self.days).focus();
  304. }
  305. function clear() {
  306. self.input.value = "";
  307. if (self.altInput)
  308. self.altInput.value = "";
  309. self.selectedDateObj = null;
  310. triggerEvent("Change");
  311. jumpToDate(self.now);
  312. }
  313. function close() {
  314. self.isOpen = false;
  315. self.calendarContainer.classList.remove("open");
  316. (self.altInput || self.input).classList.remove("active");
  317. triggerEvent("Close");
  318. }
  319. function destroy() {
  320. self.calendarContainer.parentNode.removeChild(self.calendarContainer);
  321. self.input.value = "";
  322. if (self.altInput) {
  323. self.input.type = "text";
  324. self.altInput.parentNode.removeChild(self.altInput);
  325. }
  326. document.removeEventListener("keydown", onKeyDown);
  327. window.removeEventListener("resize", onResize);
  328. document.removeEventListener("click", documentClick);
  329. document.removeEventListener("blur", documentClick);
  330. delete self.input._flatpickr;
  331. }
  332. function documentClick(e) {
  333. const isCalendarElement = self.calendarContainer.contains(e.target),
  334. isInput = self.element.contains(e.target) || e.target === self.altInput;
  335. if (self.isOpen && !isCalendarElement && !isInput)
  336. self.close();
  337. }
  338. function formatDate(frmt, dateObj) {
  339. const chars = frmt.split("");
  340. return chars.map((c, i) => self.formats[c] && chars[i - 1] !== "\\"
  341. ? self.formats[c](dateObj)
  342. : c !== "\\" ? c : ""
  343. ).join("");
  344. }
  345. function handleYearChange() {
  346. if (self.currentMonth < 0 || self.currentMonth > 11) {
  347. self.currentYear += self.currentMonth % 11;
  348. self.currentMonth = (self.currentMonth + 12) % 12;
  349. }
  350. }
  351. function isEnabled(dateToCheck) {
  352. if (
  353. (self.config.minDate && dateToCheck < self.config.minDate) ||
  354. (self.config.maxDate && dateToCheck > self.config.maxDate)
  355. )
  356. return false;
  357. if (!self.config.enable.length && !self.config.disable.length)
  358. return true;
  359. dateToCheck = parseDate(dateToCheck, true); // timeless
  360. const bool = self.config.enable.length > 0,
  361. array = bool ? self.config.enable : self.config.disable;
  362. let d;
  363. for (let i = 0; i < array.length; i++) {
  364. d = array[i];
  365. if (d instanceof Function && d(dateToCheck)) // disabled by function
  366. return bool;
  367. else if ((d instanceof Date || (typeof d === "string")) &&
  368. parseDate(d, true).getTime() === dateToCheck.getTime()
  369. )
  370. // disabled by date string
  371. return bool;
  372. else if ( // disabled by range
  373. typeof d === "object" &&
  374. d.hasOwnProperty("from") &&
  375. dateToCheck >= parseDate(d.from) &&
  376. dateToCheck <= parseDate(d.to)
  377. )
  378. return bool;
  379. }
  380. return !bool;
  381. }
  382. function onKeyDown(e) {
  383. if (!self.isOpen)
  384. return;
  385. switch (e.which) {
  386. case 13:
  387. if (self.timeContainer && self.timeContainer.contains(e.target))
  388. updateValue(e);
  389. else
  390. selectDate(e);
  391. break;
  392. case 27:
  393. self.close();
  394. break;
  395. case 37:
  396. if (e.target !== self.input & e.target !== self.altInput)
  397. changeMonth(-1);
  398. break;
  399. case 38:
  400. e.preventDefault();
  401. if (self.timeContainer.contains(e.target))
  402. updateTime(e);
  403. else {
  404. self.currentYear++;
  405. self.redraw();
  406. }
  407. break;
  408. case 39:
  409. if (e.target !== self.input & e.target !== self.altInput)
  410. changeMonth(1);
  411. break;
  412. case 40:
  413. e.preventDefault();
  414. if (self.timeContainer.contains(e.target))
  415. updateTime(e);
  416. else {
  417. self.currentYear--;
  418. self.redraw();
  419. }
  420. break;
  421. default: break;
  422. }
  423. }
  424. function onResize() {
  425. if (self.isOpen && !self.config.inline && !self.config.static)
  426. positionCalendar();
  427. }
  428. function open(e) {
  429. if (self.isMobile) {
  430. e.preventDefault();
  431. e.target.blur();
  432. setTimeout(() => {
  433. self.mobileInput.click();
  434. }, 0);
  435. triggerEvent("Open");
  436. return;
  437. }
  438. else if (self.isOpen || (self.altInput || self.input).disabled || self.config.inline)
  439. return;
  440. self.calendarContainer.classList.add("open");
  441. if (!self.config.static)
  442. positionCalendar();
  443. self.isOpen = true;
  444. if (!self.config.allowInput) {
  445. (self.altInput || self.input).blur();
  446. (self.config.noCalendar
  447. ? self.timeContainer
  448. : self.selectedDateObj
  449. ? self.selectedDateElem
  450. : self.days).focus();
  451. }
  452. (self.altInput || self.input).classList.add("active");
  453. triggerEvent("Open");
  454. }
  455. function pad(number) {
  456. return `0${number}`.slice(-2);
  457. }
  458. function parseConfig() {
  459. self.config = self.instanceConfig;
  460. Object.keys(self.element.dataset).forEach(
  461. k => self.config[k] = typeof Flatpickr.defaultConfig[k] === "boolean"
  462. ? self.element.dataset[k] !== "false"
  463. : self.element.dataset[k]
  464. );
  465. if (!self.config.dateFormat && self.config.enableTime) {
  466. self.config.dateFormat = Flatpickr.defaultConfig.dateFormat;
  467. if (self.config.noCalendar) { // time picker
  468. self.config.dateFormat = "H:i" + (self.config.enableSeconds ? ":S" : "");
  469. self.config.altFormat = "h:i" + (self.config.enableSeconds ? ":S K" : " K");
  470. }
  471. else {
  472. self.config.dateFormat += " H:i" + (self.config.enableSeconds ? ":S" : "");
  473. self.config.altFormat = `h:i${self.config.enableSeconds ? ":S" : ""} K`;
  474. }
  475. }
  476. Object.keys(Flatpickr.defaultConfig).forEach(k =>
  477. self.config[k] = typeof self.config[k] !== "undefined"
  478. ? self.config[k]
  479. : Flatpickr.defaultConfig[k]
  480. );
  481. }
  482. function parseDate(date, timeless = false) {
  483. if (typeof date === "string") {
  484. date = date.trim();
  485. if (date === "today") {
  486. date = new Date();
  487. timeless = true;
  488. }
  489. else if (self.config.parseDate)
  490. date = self.config.parseDate(date);
  491. else if (/^\d\d:\d\d/.test(date)) { // time picker
  492. const m = date.match(/^(\d{1,2})[:\s]?(\d\d)?[:\s]?(\d\d)?/);
  493. date = new Date();
  494. date.setHours(m[1], m[2] || 0, m[2] || 0);
  495. }
  496. else if (/Z$/.test(date) || /GMT$/.test(date)) // datestrings w/ timezone
  497. date = new Date(date);
  498. else if (/(\d+)/g.test(date)) {
  499. const d = date.match(/(\d+)/g);
  500. date = new Date(
  501. `${d[0]}/${d[1] || 1}/${d[2] || 1} ${d[3] || 0}:${d[4] || 0}:${d[5] || 0}`
  502. );
  503. }
  504. }
  505. if (!(date instanceof Date) || !date.getTime()) {
  506. console.warn(`flatpickr: invalid date ${date}`);
  507. console.info(self.element);
  508. return null;
  509. }
  510. if (self.config.utc && !date.fp_isUTC)
  511. date = date.fp_toUTC();
  512. if (timeless)
  513. date.setHours(0, 0, 0, 0);
  514. return date;
  515. }
  516. function positionCalendar() {
  517. const calendarHeight = self.calendarContainer.offsetHeight,
  518. input = (self.altInput || self.input),
  519. inputBounds = input.getBoundingClientRect(),
  520. distanceFromBottom = window.innerHeight - inputBounds.bottom + input.offsetHeight;
  521. let top,
  522. left = (window.pageXOffset + inputBounds.left);
  523. if (distanceFromBottom < calendarHeight) {
  524. top = (window.pageYOffset - calendarHeight + inputBounds.top) - 2;
  525. self.calendarContainer.classList.remove("arrowTop");
  526. self.calendarContainer.classList.add("arrowBottom");
  527. }
  528. else {
  529. top = (window.pageYOffset + input.offsetHeight + inputBounds.top) + 2;
  530. self.calendarContainer.classList.remove("arrowBottom");
  531. self.calendarContainer.classList.add("arrowTop");
  532. }
  533. if (!self.config.inline) {
  534. self.calendarContainer.style.top = `${top}px`;
  535. self.calendarContainer.style.left = `${left}px`;
  536. }
  537. }
  538. function redraw() {
  539. if (self.config.noCalendar || self.isMobile)
  540. return;
  541. buildWeekdays();
  542. updateNavigationCurrentMonth();
  543. buildDays();
  544. }
  545. function selectDate(e) {
  546. e.preventDefault();
  547. e.stopPropagation();
  548. if (self.config.allowInput && (e.target === self.altInput || e.target === self.input) && e.which === 13)
  549. self.setDate((self.altInput || self.input).value);
  550. else if (e.target.classList.contains("flatpickr-day") && !e.target.classList.contains("disabled")) {
  551. const isPrevMonthDay = e.target.classList.contains("prevMonthDay"),
  552. isNextMonthDay = e.target.classList.contains("nextMonthDay");
  553. if (isPrevMonthDay || isNextMonthDay)
  554. changeMonth(+isNextMonthDay - isPrevMonthDay);
  555. self.selectedDateObj = parseDate(new Date(self.currentYear, self.currentMonth, e.target.innerHTML));
  556. updateValue(e);
  557. buildDays();
  558. triggerEvent("Change");
  559. if (!self.config.enableTime)
  560. self.close();
  561. }
  562. }
  563. function set(option, value) {
  564. self.config[option] = value;
  565. jumpToDate();
  566. }
  567. function setDate(date, triggerChange) {
  568. date = parseDate(date);
  569. if (date instanceof Date && date.getTime()) {
  570. self.selectedDateObj = date;
  571. jumpToDate(self.selectedDateObj);
  572. updateValue(false);
  573. if (triggerChange)
  574. triggerEvent("Change");
  575. }
  576. else
  577. (self.altInput || self.input).value = "";
  578. }
  579. function setupDates() {
  580. self.now = new Date();
  581. if (self.config.defaultDate || self.input.value)
  582. self.selectedDateObj = parseDate(self.config.defaultDate || self.input.value);
  583. if (self.config.minDate)
  584. self.config.minDate = parseDate(self.config.minDate, true);
  585. if (self.config.maxDate)
  586. self.config.maxDate = parseDate(self.config.maxDate, true);
  587. const initialDate = (self.selectedDateObj || self.config.defaultDate ||
  588. self.config.minDate || new Date()
  589. );
  590. self.currentYear = initialDate.getFullYear();
  591. self.currentMonth = initialDate.getMonth();
  592. }
  593. function setupFormats() {
  594. self.formats = {
  595. // weekday name, short, e.g. Thu
  596. D: date => Flatpickr.l10n.weekdays.shorthand[self.formats.w(date)],
  597. // full month name e.g. January
  598. F: date => self.utils.monthToStr(self.formats.n(date) - 1, false),
  599. // hours with leading zero e.g. 03
  600. H: date => pad(date.getHours()),
  601. // day (1-30) with ordinal suffix e.g. 1st, 2nd
  602. J: date => date.getDate() + Flatpickr.l10n.ordinal(date.getDate()),
  603. // AM/PM
  604. K: date => date.getHours() > 11 ? "PM" : "AM",
  605. // shorthand month e.g. Jan, Sep, Oct, etc
  606. M: date => self.utils.monthToStr(date.getMonth(), true),
  607. // seconds 00-59
  608. S: date => pad(date.getSeconds()),
  609. // unix timestamp
  610. U: date => date.getTime() / 1000,
  611. // full year e.g. 2016
  612. Y: date => date.getFullYear(),
  613. // day in month, padded (01-30)
  614. d: date => pad(self.formats.j(date)),
  615. // hour from 1-12 (am/pm)
  616. h: date => date.getHours() % 12 ? date.getHours() % 12 : 12,
  617. // minutes, padded with leading zero e.g. 09
  618. i: date => pad(date.getMinutes()),
  619. // day in month (1-30)
  620. j: date => date.getDate(),
  621. // weekday name, full, e.g. Thursday
  622. l: date => Flatpickr.l10n.weekdays.longhand[self.formats.w(date)],
  623. // padded month number (01-12)
  624. m: date => pad(self.formats.n(date)),
  625. // the month number (1-12)
  626. n: date => date.getMonth() + 1,
  627. // seconds 0-59
  628. s: date => date.getSeconds(),
  629. // number of the day of the week
  630. w: date => date.getDay(),
  631. // last two digits of year e.g. 16 for 2016
  632. y: date => String(self.formats.Y(date)).substring(2)
  633. };
  634. }
  635. function setupHelperFunctions() {
  636. self.utils = {
  637. getDaysinMonth: (month = self.currentMonth, yr = self.currentYear) => {
  638. if (month === 1 && ((yr % 4 === 0) && (yr % 100 !== 0)) || (yr % 400 === 0))
  639. return 29;
  640. return Flatpickr.l10n.daysInMonth[month];
  641. },
  642. monthToStr: (monthNumber, short = self.config.shorthandCurrentMonth) =>
  643. Flatpickr.l10n.months[(`${short ? "short" : "long"}hand`)][monthNumber],
  644. };
  645. }
  646. function setupInputs() {
  647. self.input = self.config.wrap ? self.element.querySelector("[data-input]") : self.element;
  648. self.input.classList.add("flatpickr-input");
  649. if (self.config.altInput) {
  650. // replicate self.element
  651. self.altInput = createElement(self.input.nodeName, "flatpickr-input " + self.config.altInputClass);
  652. self.altInput.placeholder = self.input.placeholder;
  653. self.altInput.type = "text";
  654. self.input.type = "hidden";
  655. self.input.parentNode.insertBefore(self.altInput, self.input.nextSibling);
  656. }
  657. if (!self.config.allowInput)
  658. (self.altInput || self.input).setAttribute("readonly", "readonly");
  659. }
  660. function setupMobile() {
  661. const inputType = self.config.enableTime
  662. ? (self.config.noCalendar ? "time" : "datetime-local")
  663. : "date";
  664. self.mobileInput = createElement("input", "flatpickr-input");
  665. self.mobileInput.step = "any";
  666. self.mobileInput.tabIndex = -1;
  667. self.mobileInput.type = inputType;
  668. if (self.selectedDateObj) {
  669. const formatStr = inputType === "datetime-local" ? "Y-m-d\\TH:i:S" :
  670. inputType === "date" ? "Y-m-d" : "H:i:S";
  671. const mobileFormattedDate = formatDate(formatStr, self.selectedDateObj);
  672. self.mobileInput.defaultValue = self.mobileInput.value = mobileFormattedDate;
  673. }
  674. if (self.config.minDate)
  675. self.mobileInput.min = formatDate("Y-m-d", self.config.minDate);
  676. if (self.config.maxDate)
  677. self.mobileInput.max = formatDate("Y-m-d", self.config.maxDate);
  678. self.input.type = "hidden";
  679. if (self.config.altInput)
  680. self.altInput.type = "hidden";
  681. try {
  682. self.input.parentNode.insertBefore(self.mobileInput, self.input.nextSibling);
  683. }
  684. catch (e) {
  685. //
  686. }
  687. self.mobileInput.addEventListener("change", e => {
  688. self.setDate(e.target.value);
  689. triggerEvent("Change");
  690. triggerEvent("Close");
  691. });
  692. }
  693. function toggle() {
  694. if (self.isOpen)
  695. self.close();
  696. else
  697. self.open();
  698. }
  699. function triggerEvent(event) {
  700. if (self.config["on" + event])
  701. self.config["on" + event](self.selectedDateObj, self.input.value, self);
  702. }
  703. function updateNavigationCurrentMonth() {
  704. self.currentMonthElement.textContent = self.utils.monthToStr(self.currentMonth) + " ";
  705. self.currentYearElement.value = self.currentYear;
  706. }
  707. function updateValue(readTimeInput = true) {
  708. if (self.config.noCalendar && !self.selectedDateObj)
  709. // picking time only and method triggered from picker
  710. self.selectedDateObj = new Date();
  711. else if (!self.selectedDateObj)
  712. return;
  713. if (self.config.enableTime && !self.isMobile) {
  714. let hours,
  715. minutes,
  716. seconds;
  717. if (readTimeInput) {
  718. // update time
  719. hours = (parseInt(self.hourElement.value, 10) || 0);
  720. minutes = (60 + (parseInt(self.minuteElement.value, 10) || 0)) % 60;
  721. if (self.config.enableSeconds)
  722. seconds = (60 + (parseInt(self.secondElement.value, 10)) || 0) % 60;
  723. if (!self.config.time_24hr)
  724. // the real number of hours for the date object
  725. hours = hours % 12 + 12 * (self.amPM.innerHTML === "PM");
  726. self.selectedDateObj.setHours(hours, minutes, seconds || 0, 0);
  727. }
  728. else {
  729. hours = self.selectedDateObj.getHours();
  730. minutes = self.selectedDateObj.getMinutes();
  731. seconds = self.selectedDateObj.getSeconds();
  732. }
  733. self.hourElement.value = pad(
  734. !self.config.time_24hr ? (12 + hours) % 12 + 12 * (hours % 12 === 0) : hours
  735. );
  736. self.minuteElement.value = pad(minutes);
  737. if (self.secondElement !== undefined)
  738. self.secondElement.value = pad(seconds);
  739. }
  740. self.input.value = formatDate(self.config.dateFormat, self.selectedDateObj);
  741. if (self.altInput)
  742. self.altInput.value = formatDate(self.config.altFormat, self.selectedDateObj);
  743. triggerEvent("ValueUpdate");
  744. }
  745. function yearScroll(e) {
  746. e.preventDefault();
  747. const delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.deltaY)));
  748. self.currentYear = e.target.value = parseInt(e.target.value, 10) + delta;
  749. self.redraw();
  750. }
  751. function createElement(tag, className = "", content = "") {
  752. const e = document.createElement(tag);
  753. e.className = className;
  754. if (content)
  755. e.textContent = content;
  756. return e;
  757. }
  758. function debounce(func, wait, immediate) {
  759. let timeout;
  760. return function (...args) {
  761. const context = this;
  762. const later = function () {
  763. timeout = null;
  764. if (!immediate)
  765. func.apply(context, args);
  766. };
  767. clearTimeout(timeout);
  768. timeout = setTimeout(later, wait);
  769. if (immediate && !timeout)
  770. func.apply(context, args);
  771. };
  772. }
  773. function equalDates(date1, date2) {
  774. return date1.getDate() === date2.getDate() &&
  775. date1.getMonth() === date2.getMonth() &&
  776. date1.getFullYear() === date2.getFullYear();
  777. }
  778. function timeWrapper(e) {
  779. e.preventDefault();
  780. if (e && e.type !== "keydown")
  781. e.target.blur();
  782. if (e.target.className === "flatpickr-am-pm") {
  783. e.target.textContent = ["AM", "PM"][(e.target.textContent === "AM") | 0];
  784. e.stopPropagation();
  785. return;
  786. }
  787. const min = parseInt(e.target.min, 10),
  788. max = parseInt(e.target.max, 10),
  789. step = parseInt(e.target.step, 10),
  790. value = parseInt(e.target.value, 10);
  791. let newValue = value;
  792. if (e.type === "wheel")
  793. newValue = value + step * (Math.max(-1, Math.min(1, (e.wheelDelta || -e.deltaY))));
  794. else if (e.type === "keydown")
  795. newValue = value + step * (e.which === 38 ? 1 : -1);
  796. if (newValue <= min)
  797. newValue = max - step;
  798. else if (newValue >= max)
  799. newValue = min + step;
  800. e.target.value = pad(newValue);
  801. }
  802. init();
  803. return self;
  804. }
  805. Flatpickr.defaultConfig = {
  806. /* if true, dates will be parsed, formatted, and displayed in UTC.
  807. preloading date strings w/ timezones is recommended but not necessary */
  808. utc: false,
  809. // wrap: see https://chmln.github.io/flatpickr/#strap
  810. wrap: false,
  811. // enables week numbers
  812. weekNumbers: false,
  813. allowInput: false,
  814. /*
  815. clicking on input opens the date(time)picker.
  816. disable if you wish to open the calendar manually with .open()
  817. */
  818. clickOpens: true,
  819. // display time picker in 24 hour mode
  820. time_24hr: false,
  821. // enables the time picker functionality
  822. enableTime: false,
  823. // noCalendar: true will hide the calendar. use for a time picker along w/ enableTime
  824. noCalendar: false,
  825. // more date format chars at https://chmln.github.io/flatpickr/#dateformat
  826. dateFormat: "Y-m-d",
  827. // altInput - see https://chmln.github.io/flatpickr/#altinput
  828. altInput: null,
  829. // the created altInput element will have this class.
  830. altInputClass: "",
  831. // same as dateFormat, but for altInput
  832. altFormat: "F j, Y", // defaults to e.g. June 10, 2016
  833. // defaultDate - either a datestring or a date object. used for datetimepicker"s initial value
  834. defaultDate: null,
  835. // the minimum date that user can pick (inclusive)
  836. minDate: null,
  837. // the maximum date that user can pick (inclusive)
  838. maxDate: null,
  839. // dateparser that transforms a given string to a date object
  840. parseDate: null,
  841. // see https://chmln.github.io/flatpickr/#disable
  842. enable: [],
  843. // see https://chmln.github.io/flatpickr/#disable
  844. disable: [],
  845. // display the short version of month names - e.g. Sep instead of September
  846. shorthandCurrentMonth: false,
  847. // displays calendar inline. see https://chmln.github.io/flatpickr/#inline-calendar
  848. inline: false,
  849. // position calendar inside wrapper and next to the input element
  850. // leave at false unless you know what you"re doing
  851. static: false,
  852. // code for previous/next icons. this is where you put your custom icon code e.g. fontawesome
  853. prevArrow: "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 17 17'><g></g><path d='M5.207 8.471l7.146 7.147-0.707 0.707-7.853-7.854 7.854-7.853 0.707 0.707-7.147 7.146z' /></svg>",
  854. nextArrow: "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 17 17'><g></g><path d='M13.207 8.472l-7.854 7.854-0.707-0.707 7.146-7.146-7.146-7.148 0.707-0.707 7.854 7.854z' /></svg>",
  855. // enables seconds in the time picker
  856. enableSeconds: false,
  857. // step size used when scrolling/incrementing the hour element
  858. hourIncrement: 1,
  859. // step size used when scrolling/incrementing the minute element
  860. minuteIncrement: 5,
  861. // disable native mobile datetime input support
  862. disableMobile: false,
  863. // onChange callback when user selects a date or time
  864. onChange: null, // function (dateObj, dateStr) {}
  865. // called every time calendar is opened
  866. onOpen: null, // function (dateObj, dateStr) {}
  867. // called every time calendar is closed
  868. onClose: null, // function (dateObj, dateStr) {}
  869. // called after calendar is ready
  870. onReady: null, // function (dateObj, dateStr) {}
  871. onValueUpdate: null
  872. };
  873. Flatpickr.l10n = {
  874. weekdays: {
  875. shorthand: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
  876. longhand: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
  877. },
  878. months: {
  879. shorthand: [
  880. "Jan", "Feb", "Mar", "Apr",
  881. "May", "Jun", "Jul", "Aug",
  882. "Sep", "Oct", "Nov", "Dec"
  883. ],
  884. longhand: [
  885. "January", "February", "March", "April",
  886. "May", "June", "July", "August",
  887. "September", "October", "November", "December"
  888. ]
  889. },
  890. daysInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
  891. firstDayOfWeek: 0,
  892. ordinal: (nth) => {
  893. const s = nth % 100;
  894. if (s > 3 && s < 21) return "th";
  895. switch (s % 10) {
  896. case 1: return "st";
  897. case 2: return "nd";
  898. case 3: return "rd";
  899. default: return "th";
  900. }
  901. },
  902. weekAbbreviation: "Wk",
  903. scrollTitle: "Scroll to increment",
  904. toggleTitle: "Click to toggle"
  905. };
  906. Flatpickr.localize = function (l10n) {
  907. Object.keys(l10n).forEach(k => Flatpickr.l10n[k] = l10n[k]);
  908. };
  909. function _flatpickr(nodeList, config) {
  910. let instances = [];
  911. for (let i = 0; i < nodeList.length; i++) {
  912. if (nodeList[i]._flatpickr)
  913. nodeList[i]._flatpickr.destroy();
  914. try {
  915. nodeList[i]._flatpickr = new Flatpickr(nodeList[i], config || {});
  916. instances.push(nodeList[i]._flatpickr);
  917. }
  918. catch (e) {
  919. console.warn(e, e.stack);
  920. }
  921. }
  922. return instances.length === 1 ? instances[0] : instances;
  923. }
  924. HTMLCollection.prototype.flatpickr = NodeList.prototype.flatpickr = function (config) {
  925. return _flatpickr(this, config);
  926. };
  927. HTMLElement.prototype.flatpickr = function (config) {
  928. return _flatpickr([this], config);
  929. };
  930. if (typeof jQuery !== "undefined") {
  931. jQuery.fn.flatpickr = function (config) {
  932. return _flatpickr(this, config);
  933. };
  934. }
  935. Date.prototype.fp_incr = function (days) {
  936. return new Date(
  937. this.getFullYear(),
  938. this.getMonth(),
  939. this.getDate() + parseInt(days, 10)
  940. );
  941. };
  942. Date.prototype.fp_isUTC = false;
  943. Date.prototype.fp_toUTC = function () {
  944. const newDate = new Date(this.getTime() + this.getTimezoneOffset() * 60000);
  945. newDate.fp_isUTC = true;
  946. return newDate;
  947. };
  948. Flatpickr.prototype.getWeek = function (givenDate) {
  949. const date = new Date(givenDate.getTime());
  950. date.setHours(0, 0, 0, 0);
  951. // Thursday in current week decides the year.
  952. date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
  953. // January 4 is always in week 1.
  954. const week1 = new Date(date.getFullYear(), 0, 4);
  955. // Adjust to Thursday in week 1 and count number of weeks from date to week1.
  956. return 1 +
  957. Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 +
  958. (week1.getDay() + 6) % 7) / 7);
  959. };
  960. // IE9 classList polyfill
  961. if (!("classList" in document.documentElement) && Object.defineProperty && typeof HTMLElement !== "undefined") {
  962. Object.defineProperty(HTMLElement.prototype, "classList", {
  963. get: function () {
  964. let self = this;
  965. function update(fn) {
  966. return function (value) {
  967. let classes = self.className.split(/\s+/),
  968. index = classes.indexOf(value);
  969. fn(classes, index, value);
  970. self.className = classes.join(" ");
  971. };
  972. }
  973. let ret = {
  974. add: update((classes, index, value) => {
  975. ~index || classes.push(value);
  976. }),
  977. remove: update((classes, index) => {
  978. ~index && classes.splice(index, 1);
  979. }),
  980. toggle: update((classes, index, value) => {
  981. ~index ? classes.splice(index, 1) : classes.push(value);
  982. }),
  983. contains: value => !!~self.className.split(/\s+/).indexOf(value),
  984. item: function (i) {
  985. return self.className.split(/\s+/)[i] || null;
  986. }
  987. };
  988. Object.defineProperty(ret, "length", {
  989. get: function () {
  990. return self.className.split(/\s+/).length;
  991. }
  992. });
  993. return ret;
  994. }
  995. });
  996. }
  997. if (typeof module !== "undefined")
  998. module.exports = Flatpickr;