UNPKG

flatpickr

Version:

A lightweight, powerful datetimepicker

797 lines (579 loc) 25.3 kB
var flatpickr = function (selector, config) { 'use strict'; let elements, instances, createInstance = element => { if (element._flatpickr) element._flatpickr.destroy(); element._flatpickr = new flatpickr.init(element, config); return element._flatpickr; }; if (selector.nodeName) return createInstance(selector); /* Utilize the performance of native getters if applicable https://jsperf.com/getelementsbyclassname-vs-queryselectorall/18 https://jsperf.com/jquery-vs-javascript-performance-comparison/22 */ else if ( /^\#[a-zA-Z0-9\-\_]*$/.test(selector) ) return createInstance(document.getElementById(selector.slice(1))); else if ( /^\.[a-zA-Z0-9\-\_]*$/.test(selector) ) elements = document.getElementsByClassName(selector.slice(1)); else elements = document.querySelectorAll(selector); instances = [].slice.call(elements).map(createInstance); return { calendars: instances, byID: id => { for(let i=0;i<instances.length;i++) if(instances[i].element.id === id) return instances[i]; } }; }; /** * @constructor */ flatpickr.init = function (element, instanceConfig) { 'use strict'; // functions var self = this, wrap, uDate, equalDates, pad, formatDate, monthToStr, isDisabled, buildWeekdays, buildDays, buildTime, timeWrapper, yearScroll, updateValue, updateNavigationCurrentMonth, buildMonthNavigation, handleYearChange, documentClick, calendarClick, buildCalendar, bind, init, triggerChange, changeMonth, getDaysinMonth; // elements & variables var calendarContainer = document.createElement('div'), navigationCurrentMonth = document.createElement('span'), monthsNav = document.createElement('div'), prevMonthNav = document.createElement('span'), cur_year = document.createElement('span'), cur_month = document.createElement('span'), nextMonthNav = document.createElement('span'), calendar = document.createElement('div'), currentDate = new Date(), wrapperElement = document.createElement('div'), hourElement, minuteElement, am_pm, clickEvt; init = function () { navigationCurrentMonth.className = 'flatpickr-current-month'; calendarContainer.className = 'flatpickr-calendar'; calendar.className = "flatpickr-days"; instanceConfig = instanceConfig || {}; self.config = {}; self.element = element; for (var config in self.defaultConfig) self.config[config] = instanceConfig[config] || self.element.dataset&&self.element.dataset[config.toLowerCase()] || self.element.getAttribute("data-"+config)|| self.defaultConfig[config]; self.input = (self.config.wrap) ? element.querySelector("[data-input]") : element; if(self.config.defaultDate){ self.config.defaultDate = uDate(self.config.defaultDate); } if ( self.input.value || self.config.defaultDate ) self.selectedDateObj = uDate(self.config.defaultDate||self.input.value); self.jumpToDate(); wrap(); buildCalendar(); bind(); updateValue(); }; uDate = function(date, timeless){ timeless = timeless||false; if (date === 'today'){ date = new Date(); timeless=true; } else if (typeof date === 'string'){ if ( Date.parse(date) ) return new Date(date); else if (self.config.noCalendar && /\d\d[:\s]\d\d/.test(date)) // time-only picker return new Date(`${currentDate.toDateString()} ${date}`); console.error(`flatpickr: invalid date string ${date}`); console.info(self.element); return null; } if(timeless && date) date.setHours(0,0,0,0); return date; }; equalDates = function(date1, date2){ return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate(); }; wrap = function () { wrapperElement.className = 'flatpickr-wrapper'; self.element.parentNode.insertBefore(wrapperElement, self.element); wrapperElement.appendChild(self.element); if(self.config.inline) wrapperElement.classList.add('inline'); if ( self.config.altInput ){ // replicate self.element self.altInput = document.createElement(self.input.nodeName); self.altInput.placeholder = self.input.placeholder; self.altInput.type = self.input.type||"text"; self.input.type='hidden'; wrapperElement.appendChild(self.altInput); } }; getDaysinMonth = function(givenMonth){ let yr = self.currentYear, month = givenMonth||self.currentMonth; if (month === 1 && ((yr % 4 === 0) && (yr % 100 !== 0)) || (yr % 400 === 0) ) return 29; return self.l10n.daysInMonth[month]; }; updateValue = function(){ if (self.selectedDateObj && self.config.enableTime ){ // update time var hour = parseInt(hourElement.value), minute = (60+parseInt(minuteElement.value ))%60; if (!self.config.time_24hr) hour = hour%12 + 12*(am_pm.innerHTML=== "PM"); self.selectedDateObj.setHours(hour , minute ); hourElement.value = pad( self.config.time_24hr ? hour : ((12 + hour)%12+12*(hour%12===0))); minuteElement.value = pad(minute); } if ( self.altInput && self.selectedDateObj ) self.altInput.value = formatDate(self.config.altFormat); if ( self.selectedDateObj ) self.input.value = formatDate(self.config.dateFormat); triggerChange(); }; pad = num =>("0" + num).slice(-2); formatDate = function (dateFormat) { if (self.config.noCalendar) dateFormat = ""; if(self.config.enableTime) dateFormat+= " " + self.config.timeFormat; let formattedDate = '', formats = { D: () => self.l10n.weekdays.shorthand[ formats.w() ], F: () => monthToStr( formats.n() - 1, false ), H: () => pad(self.selectedDateObj.getHours()), K: () => self.selectedDateObj.getHours() > 11 ? "PM" : "AM", M: () => monthToStr( formats.n() - 1, true ), U: () => self.selectedDateObj.getTime() / 1000, Y: () => self.selectedDateObj.getFullYear(), d: () => pad( formats.j() ), h: () => self.selectedDateObj.getHours()%12 ? self.selectedDateObj.getHours()%12 : 12, i: () => pad( self.selectedDateObj.getMinutes() ), j: () => self.selectedDateObj.getDate(), l: () => self.l10n.weekdays.longhand[ formats.w() ], m: () => pad( formats.n() ), n: () => self.selectedDateObj.getMonth() + 1, w: () => self.selectedDateObj.getDay(), y: () => String( formats.Y() ).substring(2) }, formatPieces = dateFormat.split(''); for(let i = 0; i < formatPieces.length; i++){ if (formats[formatPieces[i]] && formatPieces[i - 1] !== '\\') formattedDate += formats[formatPieces[i]](); else if (formatPieces[i] !== '\\') formattedDate += formatPieces[i]; } return formattedDate; }; monthToStr = function (date, shorthand) { shorthand=shorthand||self.config.shorthandCurrentMonth; return shorthand ? self.l10n.months.shorthand[date] : self.l10n.months.longhand[date]; }; isDisabled = function(date){ for (let i = 0;i<self.config.disable.length;i++) if ( date >= uDate(self.config.disable[i].from) && date <= uDate(self.config.disable[i].to) ) return true; return false; }; yearScroll = event => { event.preventDefault(); let delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.deltaY ))); self.currentYear = event.target.innerHTML = parseInt(event.target.innerHTML,10) + delta; self.redraw(); }; timeWrapper = function (e){ e.preventDefault(); let min = parseInt(e.target.min), max = parseInt(e.target.max), step = parseInt(e.target.step), delta = step * ( Math.max(-1, Math.min(1, (e.wheelDelta || -e.deltaY ))) ), newValue = ( parseInt(e.target.value) + delta )%(max+(min===0)); if (newValue < min) newValue = max + (min === 0) - step*(min === 0); e.target.value = pad( newValue ); }; updateNavigationCurrentMonth = function () { cur_month.innerHTML = monthToStr(self.currentMonth) +" "; cur_year.innerHTML = self.currentYear; }; handleYearChange = function () { if (self.currentMonth < 0 || self.currentMonth > 11) { self.currentYear += self.currentMonth % 11; self.currentMonth = (self.currentMonth + 12) % 12; } }; documentClick = function (event) { if (wrapperElement.classList.contains("open") && !wrapperElement.contains(event.target)) self.close(); }; changeMonth = function(offset) { self.currentMonth += offset; handleYearChange(); updateNavigationCurrentMonth(); buildDays(); }; calendarClick = function (e) { e.preventDefault(); if ( e.target.classList.contains('slot') ) { self.selectedDateObj = new Date( self.currentYear, self.currentMonth, e.target.innerHTML ); updateValue(); buildDays(); if ( !self.config.inline && !self.config.enableTime ) self.close(); } }; buildCalendar = function () { if ( !self.config.noCalendar) { buildMonthNavigation(); buildWeekdays(); buildDays(); calendarContainer.appendChild(calendar); } wrapperElement.appendChild(calendarContainer); if(self.config.enableTime) buildTime(); }; buildMonthNavigation = function () { monthsNav.className = 'flatpickr-month'; prevMonthNav.className = "flatpickr-prev-month"; prevMonthNav.innerHTML = self.config.prevArrow; nextMonthNav.className = "flatpickr-next-month"; nextMonthNav.innerHTML = self.config.nextArrow; cur_month.className = "cur_month"; cur_year.className = "cur_year"; cur_year.title = "Scroll to increment"; navigationCurrentMonth.appendChild(cur_month); navigationCurrentMonth.appendChild(cur_year); monthsNav.appendChild(prevMonthNav); monthsNav.appendChild(navigationCurrentMonth); monthsNav.appendChild(nextMonthNav); updateNavigationCurrentMonth(); calendarContainer.appendChild(monthsNav); }; buildWeekdays = function () { let weekdayContainer = document.createElement('div'), firstDayOfWeek = self.l10n.firstDayOfWeek, weekdays = self.l10n.weekdays.shorthand.slice(); weekdayContainer.className = "flatpickr-weekdays"; if (firstDayOfWeek > 0 && firstDayOfWeek < weekdays.length) { weekdays = [].concat(weekdays.splice(firstDayOfWeek, weekdays.length), weekdays.splice(0, firstDayOfWeek)); } weekdayContainer.innerHTML = '<span>' + weekdays.join('</span><span>') + '</span>'; calendarContainer.appendChild(weekdayContainer); }; buildDays = function () { let firstOfMonth = ( new Date(self.currentYear, self.currentMonth, 1).getDay() - self.l10n.firstDayOfWeek + 7 )%7, numDays = getDaysinMonth(), prevMonthDays = getDaysinMonth( ( self.currentMonth - 1 + 12)%12 ), dayNumber = prevMonthDays + 1 - firstOfMonth, className, cur_date, date_is_disabled, date_outside_minmax; calendar.innerHTML = ''; self.config.minDate = uDate(self.config.minDate === "today" ? new Date() : self.config.minDate, true); self.config.maxDate = uDate(self.config.maxDate, true); // prepend days from the ending of previous month for( ; dayNumber <= prevMonthDays; dayNumber++ ){ let d = document.createElement("span"); d.className="disabled flatpickr-day"; d.innerHTML=dayNumber; calendar.appendChild(d); } // Start at 1 since there is no 0th day for (dayNumber = 1; dayNumber <= 42 - firstOfMonth; dayNumber++) { if (dayNumber <= numDays) // avoids new date objects for appended dates cur_date = new Date(self.currentYear, self.currentMonth, dayNumber); date_outside_minmax = (self.config.minDate && cur_date < self.config.minDate ) || (self.config.maxDate && cur_date > self.config.maxDate); date_is_disabled = dayNumber > numDays || date_outside_minmax || isDisabled( cur_date ); className = date_is_disabled ? "disabled flatpickr-day" : "slot flatpickr-day"; if (!date_is_disabled && !self.selectedDateObj && equalDates(cur_date, currentDate) ) className += ' today'; if (!date_is_disabled && self.selectedDateObj && equalDates(cur_date, self.selectedDateObj) ) className += ' selected'; let cell = document.createElement("span"); cell.className = className; cell.innerHTML = (dayNumber > numDays ? dayNumber % numDays : dayNumber); calendar.appendChild(cell); } }; buildTime = function(){ let timeContainer = document.createElement("div"), separator = document.createElement("span"); timeContainer.className = "flatpickr-time"; hourElement = document.createElement("input"); minuteElement = document.createElement("input"); separator.className = "flatpickr-time-separator"; separator.innerHTML = ":"; hourElement.type = minuteElement.type = "number"; hourElement.className = "flatpickr-hour"; minuteElement.className = "flatpickr-minute"; hourElement.value = self.selectedDateObj ? pad(self.selectedDateObj.getHours()) : 12; minuteElement.value = self.selectedDateObj ? pad(self.selectedDateObj.getMinutes()) : "00"; hourElement.step = self.config.hourIncrement; minuteElement.step = self.config.minuteIncrement; hourElement.min = +!self.config.time_24hr; // 0 in 24hr mode, 1 in 12hr mode hourElement.max = self.config.time_24hr ? 23 : 12; minuteElement.min = 0; minuteElement.max = 59; hourElement.title = minuteElement.title = "Scroll to increment"; timeContainer.appendChild(hourElement); timeContainer.appendChild(separator); timeContainer.appendChild(minuteElement); if (!self.config.time_24hr){ // add am_pm if appropriate am_pm = document.createElement("span"); am_pm.className = "flatpickr-am-pm"; am_pm.innerHTML = ["AM","PM"][(hourElement.value > 11)|0]; am_pm.title="Click to toggle"; timeContainer.appendChild(am_pm); } // picking time only // if (self.config.noCalendar) // self.selectedDateObj = new Date(); calendarContainer.appendChild(timeContainer); }; bind = function () { function am_pm_toggle(e){ e.preventDefault(); am_pm.innerHTML = ["AM","PM"][(am_pm.innerHTML === "AM")|0]; } if (String(self.config.clickOpens)==='true'){ self.input.addEventListener( 'focus' , self.open); if(self.altInput) self.altInput.addEventListener( 'focus' , self.open); } if (self.config.wrap && self.element.querySelector("[data-open]")) self.element.querySelector("[data-open]").addEventListener( 'click' , self.open); if (self.config.wrap && self.element.querySelector("[data-close]")) self.element.querySelector("[data-close]").addEventListener( 'click' , self.close); if (self.config.wrap && self.element.querySelector("[data-toggle]")) self.element.querySelector("[data-toggle]").addEventListener( 'click' , self.toggle); if (self.config.wrap && self.element.querySelector("[data-clear]")) self.element.querySelector("[data-clear]").addEventListener( 'click' , self.clear); prevMonthNav.addEventListener('click', () => { changeMonth(-1); }); nextMonthNav.addEventListener('click', () => { changeMonth(1); }); cur_year.addEventListener('wheel', yearScroll); calendar.addEventListener('click', calendarClick); document.addEventListener('click', documentClick, true); if ( self.config.enableTime ){ hourElement.addEventListener("wheel", timeWrapper); minuteElement.addEventListener("wheel", timeWrapper); hourElement.addEventListener("mouseout", updateValue); minuteElement.addEventListener("mouseout", updateValue); hourElement.addEventListener("change", updateValue); minuteElement.addEventListener("change", updateValue); hourElement.addEventListener("click", () => {hourElement.select();}); minuteElement.addEventListener("click", () => {minuteElement.select();}); if (!self.config.time_24hr) { am_pm.addEventListener("focus", () => am_pm.blur()); am_pm.addEventListener("click", am_pm_toggle); am_pm.addEventListener("wheel", am_pm_toggle); am_pm.addEventListener("mouseout", updateValue); } } if(document.createEvent){ clickEvt = document.createEvent("MouseEvent"); // without all these args ms edge spergs out clickEvt.initMouseEvent("click",true,true,window,0,0,0,0,0,false,false,false,false,0,null); } else clickEvt = new MouseEvent('click', { 'view': window, 'bubbles': true, 'cancelable': true }); }; self.open = function () { if (self.input.disabled || self.config.inline) return; self.input.blur(); self.input.classList.add('active'); if(self.altInput){ self.altInput.blur(); self.altInput.classList.add('active'); } self.element.parentNode.classList.add('open'); if (self.config.onOpen) self.config.onOpen(); }; self.toggle = function () { if (self.input.disabled) return; self.element.parentNode.classList.toggle('open'); if(self.altInput) self.altInput.classList.toggle('active'); self.input.classList.toggle('active'); }; self.close = function () { self.element.parentNode.classList.remove('open'); self.input.classList.remove('active'); if (self.altInput) self.altInput.classList.remove('active'); if (self.config.onClose) self.config.onClose(); }; self.clear = function() { self.input.value=""; self.selectedDateObj = null; self.jumpToDate(); }; triggerChange = function(){ self.input.dispatchEvent(clickEvt); if (self.config.onChange) self.config.onChange(self.selectedDateObj, self.input.value); }; self.destroy = function () { let parent = self.element.parentNode, element = parent.removeChild(self.element); document.removeEventListener('click', documentClick, false); parent.removeChild(calendarContainer); parent.parentNode.replaceChild(element, parent); }; self.redraw = function(){ updateNavigationCurrentMonth(); buildDays(); }; self.jumpToDate = function(jumpDate){ jumpDate = uDate( jumpDate||self.selectedDateObj||self.config.defaultDate||self.config.minDate||currentDate ); self.currentYear = jumpDate.getFullYear(); self.currentMonth = jumpDate.getMonth(); self.redraw(); }; self.setDate = function(date, triggerChangeEvent){ self.selectedDateObj = uDate(date); self.jumpToDate(self.selectedDateObj); updateValue(); triggerChangeEvent = triggerChangeEvent||false; if(triggerChangeEvent) triggerChange(); }; self.set = function(key, value){ if (key in self.config) { self.config[key] = value; self.jumpToDate(); } }; init(); return self; }; flatpickr.init.prototype = { l10n: { weekdays: { shorthand: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], longhand: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] }, months: { shorthand: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], longhand: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] }, daysInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], firstDayOfWeek: 0 }, defaultConfig : { noCalendar: false, wrap: false, clickOpens: true, dateFormat: 'Y-m-d', altInput: false, altFormat: "F j, Y", defaultDate: null, minDate: null, maxDate: null, disable: [], shorthandCurrentMonth: false, inline: false, prevArrow: '&lt;', nextArrow: '&gt;', enableTime: false, timeFormat: "h:i K", time_24hr: false, hourIncrement: 1, minuteIncrement: 5, onChange: null, //function( dateObj, dateStr ){} onOpen: null, onClose: null // function() {} } }; Date.prototype.fp_incr = function(days){ return new Date( this.getFullYear(), this.getMonth(), this.getDate() + parseInt(days, 10) ); }; // classList polyfill if (!("classList" in document.documentElement) && Object.defineProperty && typeof HTMLElement !== 'undefined') { Object.defineProperty(HTMLElement.prototype, 'classList', { get: function() { var self = this; function update(fn) { return function(value) { var classes = self.className.split(/\s+/), index = classes.indexOf(value); fn(classes, index, value); self.className = classes.join(" "); }; } var ret = { add: update(function(classes, index, value) { ~index || classes.push(value); }), remove: update(function(classes, index) { ~index && classes.splice(index, 1); }), toggle: update(function(classes, index, value) { ~index ? classes.splice(index, 1) : classes.push(value); }), contains: function(value) { return !!~self.className.split(/\s+/).indexOf(value); } }; return ret; } }); } if (typeof module !=='undefined') module.exports = flatpickr;