UNPKG

bulma-calendar

Version:

Display a calendar for date/time selection, in different colors and sizes

856 lines (690 loc) 28.3 kB
import * as utils from '../utils'; import * as type from '../utils/type'; import * as dateFns from 'date-fns'; import EventEmitter from '../utils/events'; import template from './templates/datepicker'; import templateDays from './templates/days'; import templateWeekdays from './templates/weekdays'; import templateMonths from './templates/months'; import templateYears from './templates/years'; import defaultOptions from './defaultOptions'; export default class datePicker extends EventEmitter { constructor(options = {}) { super(); this.options = { ...defaultOptions, ...options, }; this._clickEvents = ['click', 'touch']; this._supportsPassive = utils.detectSupportsPassive(); this._id = utils.uuid('datePicker'); this.node = null; this.onPreviousDatePicker = this.onPreviousDatePicker.bind(this); this.onNextDatePicker = this.onNextDatePicker.bind(this); this.onSelectMonthDatePicker = this.onSelectMonthDatePicker.bind(this); this.onMonthClickDatePicker = this.onMonthClickDatePicker.bind(this); this.onSelectYearDatePicker = this.onSelectYearDatePicker.bind(this); this.onYearClickDatePicker = this.onYearClickDatePicker.bind(this); this.onDateClickDatePicker = this.onDateClickDatePicker.bind(this); this._init(); } /**************************************************** * * * GETTERS and SETTERS * * * ****************************************************/ /** * Get id of current datePicker */ get id() { return this._id; } set date(date) { if (type.isObject(date) && date.start && date.end) { this._date = date; } return this; } get date() { return ( this._date || { start: undefined, end: undefined, } ); } // Set datePicker language set lang(lang) { try { this._locale = require(`date-fns/locale/${lang}/index.js`); } catch (e) { lang = 'en-US'; this._locale = require(`date-fns/locale/${lang}/index.js`); } finally { this._lang = lang; return this; } } // Get current datePicker language get lang() { return this._lang; } get locale() { return this._locale; } set start(date) { if (!date) { this._date.start = undefined; return this; } date = utils.newDate(date, this.format, 'yyyy-MM-dd'); if (this._isValidDate(date, this.min, this.max)) { this._date.start = dateFns.startOfDay(date); } return this; } get start() { return this._date.start; } set end(date) { if (!date) { this._date.end = undefined; return this; } date = utils.newDate(date, this.format, 'yyyy-MM-dd'); if (this._isValidDate(date, this.min, this.max)) { this._date.end = dateFns.endOfDay(date); } return this; } get end() { return this._date.end; } // Set min set min(date) { this._min = utils.newDate(date, this.format, 'yyyy-MM-dd'); return this; } // Get min get min() { return this._min; } // Set max set max(date) { this._max = utils.newDate(date, this.format, 'yyyy-MM-dd'); return this; } // Get max get max() { return this._max; } // Set date format (set to MM/dd/yyyy by default) set format(format) { this._format = format; return this; } // Get date format get format() { return this._format; } /**************************************************** * * * EVENTS FUNCTIONS * * * ****************************************************/ onPreviousDatePicker(e) { if (!this._supportsPassive) { e.preventDefault(); } e.stopPropagation(); const prevMonth = dateFns.lastDayOfMonth(dateFns.subMonths(new Date(dateFns.getYear(this._visibleDate), dateFns.getMonth(this._visibleDate)), 1)); const day = Math.min(dateFns.getDaysInMonth(prevMonth), dateFns.getDate(this._visibleDate)); this._setVisibleDate(dateFns.setDate(prevMonth, day)); this.refresh(); } onNextDatePicker(e) { if (!this._supportsPassive) { e.preventDefault(); } e.stopPropagation(); const nextMonth = dateFns.addMonths(this._visibleDate, 1); const day = Math.min(dateFns.getDaysInMonth(nextMonth), dateFns.getDate(this._visibleDate)); this._setVisibleDate(dateFns.setDate(nextMonth, day)); this.refresh(); } onSelectMonthDatePicker(e) { if (!this._supportsPassive) { e.preventDefault(); } e.stopPropagation(); if (this.options.enableMonthSwitch) { this._ui.body.dates.classList.remove('is-active'); this._ui.body.years.classList.remove('is-active'); this._ui.body.months.classList.add('is-active'); this._toggleNextButton(false); this._togglePreviousButton(false); } } onSelectYearDatePicker(e) { if (!this._supportsPassive) { e.preventDefault(); } e.stopPropagation(); if (this.options.enableYearSwitch) { this._ui.body.dates.classList.remove('is-active'); this._ui.body.months.classList.remove('is-active'); this._ui.body.years.classList.add('is-active'); this._toggleNextButton(false); this._togglePreviousButton(false); const currentYear = this._ui.body.years.querySelector('.calendar-year.is-active'); if (currentYear) { this._ui.body.years.scrollTop = currentYear.offsetTop - this._ui.body.years.offsetTop - this._ui.body.years.clientHeight / 2; } } } onMonthClickDatePicker(e) { if (!this._supportsPassive) { e.preventDefault(); } e.stopPropagation(); const newDate = dateFns.setMonth(this._visibleDate, parseInt(e.currentTarget.dataset.month) - 1); this._visibleDate = this.min ? dateFns.max(newDate, this.min) : newDate; this._visibleDate = this.max ? dateFns.min(this._visibleDate, this.max) : this._visibleDate; this.refresh(); } onYearClickDatePicker(e) { if (!this._supportsPassive) { e.preventDefault(); } e.stopPropagation(); const newDate = dateFns.setYear(this._visibleDate, parseInt(e.currentTarget.dataset.year)); this._visibleDate = this.min ? dateFns.max(newDate, this.min) : newDate; this._visibleDate = this.max ? dateFns.min(this._visibleDate, this.max) : this._visibleDate; this.refresh(); } onDateClickDatePicker(e) { if (!this._supportsPassive) { e.preventDefault(); } e.stopPropagation(); if (!e.currentTarget.classList.contains('is-disabled')) { this._select(new Date(e.currentTarget.dataset.date)); this.refresh(); } } /**************************************************** * * * PUBLIC FUNCTIONS * * * ****************************************************/ isRange() { return this.options.isRange; } enableDate(date = undefined) { if (!Array.isArray(this.disabledDates)) return; const index = this.disabledDates.findIndex((disableDate) => dateFns.isEqual(disableDate, date)); if (index > -1) { unset(this.disabledDates[index]); } } disableDate(date = undefined) { if (!Array.isArray(this.disabledDates)) return; const index = this.disabledDates.findIndex((disableDate) => dateFns.isEqual(disableDate, date)); if (index > -1) { this.disabledDates.push(date); } } highlightDate(date = undefined) { if (!Array.isArray(this.highlightedDates)) return; const index = this.highlightedDates.findIndex((highlightDate) => dateFns.isEqual(highlightDate, date)); if (index > -1) { unset(this.highlightedDates[index]); } } unhighlightDate(date = undefined) { if (!Array.isArray(this.highlightedDates)) return; const index = this.highlightedDates.findIndex((highlightDate) => dateFns.isEqual(highlightDate, date)); if (index > -1) { this.highlightedDates.push(date); } } enableWeekDay(day) { const index = this.disabledWeekDays.findIndex((disabledWeekDay) => dateFns.isEqual(disabledWeekDay, day)); if (index > -1) { unset(this.disabledWeekDays[index]); } } disableWeekDay(day) { const index = this.disabledWeekDays.findIndex((disabledWeekDay) => dateFns.isEqual(disabledWeekDay, date)); if (index > -1) { this.disabledWeekDays.push(day); } } show() { if (this._open) return; this.refresh(); this._ui.container.classList.add('is-active'); this._open = true; this._focus = true; this.emit('show', this); } hide() { if (!this._open) return; this._open = false; this._focus = false; this._ui.container.classList.remove('is-active'); this.emit('hide', this); } toggle() { if (!this._open) { this.show(); } else { this.hide(); } } /** * Get / Set datePicker value * @param {null|Date|Object|String} date optional if null then return the current date as String */ value(value = null) { // Set if (value) { if (this.options.isRange) { if (type.isString(value)) { value = value.split(' - '); } if (Array.isArray(value)) { if (value.length) this.start = utils.newDate(value[0], this.format, 'yyyy-MM-dd'); if (value.length === 2) this.end = utils.newDate(value[1], this.format, 'yyyy-MM-dd'); } } else { this._select(value, false); } this.refresh(); return; } // Get let string = this._isValidDate(this.start) ? dateFns.format(this.start, this.format, { locale: this.locale }) : ''; if (this.options.isRange && this._isValidDate(this.end)) { string += ` - ${dateFns.format(this.end, this.format, { locale: this.locale })}`; } return string; } /** * Refresh datepicker with new year/month days * @method _refreshdatepicker * @return {[type]} [description] */ refresh() { this._ui.body.dates.innerHTML = ''; // the 12 months of the year const monthLabels = new Array(12).fill(dateFns.startOfYear(this._visibleDate)).map((d, i) => dateFns.addMonths(d, i)); this._ui.body.months.innerHTML = ''; this._ui.body.months.appendChild( document.createRange().createContextualFragment( templateMonths({ months: monthLabels, locale: this.locale, }) ) ); const months = this._ui.body.months.querySelectorAll('.datepicker-month') || []; months.forEach((month) => { this._clickEvents.forEach((clickEvent) => { month.addEventListener(clickEvent, this.onMonthClickDatePicker); }); month.classList.remove('is-active'); if (month.dataset.month === dateFns.format(this._visibleDate, 'MM', { locale: this.locale })) { month.classList.add('is-active'); } }); // years const yearLabels = new Array(this.options.displayYearsCount * 2).fill(dateFns.subYears(this._visibleDate, this.options.displayYearsCount)).map((d, i) => dateFns.addYears(d, i) ); this._ui.body.years.innerHTML = ''; this._ui.body.years.appendChild( document.createRange().createContextualFragment( templateYears({ visibleDate: this._visibleDate, years: yearLabels, }) ) ); const years = this._ui.body.years.querySelectorAll('.datepicker-year') || []; years.forEach((year) => { this._clickEvents.forEach((clickEvent) => { year.addEventListener(clickEvent, this.onYearClickDatePicker); }); year.classList.remove('is-active'); if (year.dataset.year === dateFns.format(this._visibleDate, 'yyyy', { locale: this.locale })) { year.classList.add('is-active'); } }); // the 7 days of the week (Sun-Sat) const weekdayLabels = new Array(7).fill(dateFns.startOfWeek(this._visibleDate, {weekStartsOn: this.options.weekStart})).map((d, i) => dateFns.format(dateFns.addDays(d, i), 'EEE', { locale: this.locale, }) ); this._ui.body.dates.appendChild( document.createRange().createContextualFragment( templateWeekdays({ weekdays: weekdayLabels, }) ) ); if (this.min && dateFns.differenceInCalendarMonths(this._visibleDate, this.min) === 0) { this._togglePreviousButton(false); } else { this._togglePreviousButton(); } if (this.max && dateFns.differenceInCalendarMonths(this._visibleDate, this.max) === 0) { this._toggleNextButton(false); } else { this._toggleNextButton(); } this._ui.navigation.month.innerHTML = dateFns.format(this._visibleDate, this.options.navigationMonthFormat, { locale: this.locale, }); this._ui.navigation.year.innerHTML = dateFns.format(this._visibleDate, this.options.navigationYearFormat, { locale: this.locale, }); this._renderDays(); this._ui.body.dates.classList.add('is-active'); this._ui.body.months.classList.remove('is-active'); this._ui.body.years.classList.remove('is-active'); return this; } clear() { const today = new Date(); this._date = { start: undefined, end: undefined, }; this._setVisibleDate(today); this.refresh(); } render() { this.refresh(); return this.node; } /**************************************************** * * * PRIVATE FUNCTIONS * * * ****************************************************/ _init() { const today = new Date(); this._open = false; this.lang = this.options.lang; this.format = this.options.dateFormat || 'MM/dd/yyyy'; this.disabledDates = type.isFunction(this.options.disabledDates) ? this.options.disabledDates : []; this.disabledWeekDays = type.isString(this.options.disabledWeekDays) ? this.options.disabledWeekDays.split(',') : Array.isArray(this.options.disabledWeekDays) ? this.options.disabledWeekDays : []; this.highlightedDates = type.isFunction(this.options.highlightedDates) ? this.options.highlightedDates : []; if (Array.isArray(this.options.disabledDates)) { for (let i = 0; i < this.options.disabledDates.length; i++) { this.disabledDates.push(utils.newDate(this.options.disabledDates[i], this.format, 'yyyy-MM-dd')); } } if (Array.isArray(this.options.highlightedDates)) { for (let i = 0; i < this.options.highlightedDates.length; i++) { this.highlightedDates.push(utils.newDate(this.options.highlightedDates[i], this.format, 'yyyy-MM-dd')); } } this.min = this.options.minDate; this.max = this.options.maxDate; this._date = { start: utils.newDate(this.options.startDate, this.format, 'yyyy-MM-dd'), end: this.options.isRange ? utils.newDate(this.options.endDate, this.format, 'yyyy-MM-dd') : undefined, }; this._visibleDate = this._isValidDate(this.start) ? this.start : this._isValidDate(today, this.min, this.max) ? today : this.min; this._build(); this._bindEvents(); this.emit('ready', this); } _build() { this.node = document.createRange().createContextualFragment( template({ locale: this.locale, visibleDate: this._visibleDate, icons: this.options.icons, }) ); this._ui = { container: this.node.firstChild, navigation: { container: this.node.querySelector('.datepicker-nav'), previous: this.node.querySelector('.datepicker-nav-previous'), next: this.node.querySelector('.datepicker-nav-next'), month: this.node.querySelector('.datepicker-nav-month'), year: this.node.querySelector('.datepicker-nav-year'), }, body: { dates: this.node.querySelector('.datepicker-dates'), days: this.node.querySelector('.datepicker-days'), weekdays: this.node.querySelector('.datepicker-weekdays'), months: this.node.querySelector('.datepicker-months'), years: this.node.querySelector('.datepicker-years'), }, }; } _bindEvents() { document.addEventListener('keydown', (e) => { if (this._focus) { switch (e.keyCode || e.which) { case 37: this.onPreviousDatePicker(e); break; case 39: this.onNextDatePicker(e); break; } } }); // Bind year navigation events if (this._ui.navigation.previous) { this._clickEvents.forEach((clickEvent) => { this._ui.navigation.previous.addEventListener(clickEvent, this.onPreviousDatePicker); }); } if (this._ui.navigation.next) { this._clickEvents.forEach((clickEvent) => { this._ui.navigation.next.addEventListener(clickEvent, this.onNextDatePicker); }); } if (this._ui.navigation.month) { this._clickEvents.forEach((clickEvent) => { this._ui.navigation.month.addEventListener(clickEvent, this.onSelectMonthDatePicker); }); } if (this._ui.navigation.year) { this._clickEvents.forEach((clickEvent) => { this._ui.navigation.year.addEventListener(clickEvent, this.onSelectYearDatePicker); }); } const months = this._ui.body.months.querySelectorAll('.calendar-month') || []; months.forEach((month) => { this._clickEvents.forEach((clickEvent) => { month.addEventListener(clickEvent, this.onMonthClickDatePicker); }); }); const years = this._ui.body.years.querySelectorAll('.calendar-year') || []; years.forEach((year) => { this._clickEvents.forEach((clickEvent) => { year.addEventListener(clickEvent, this.onYearClickDatePicker); }); }); } /** * Bind events on each Day item * @method _bindDaysEvents * @return {void} */ _bindDaysEvents() { [].forEach.call(this._ui.days, (day) => { this._clickEvents.forEach((clickEvent) => { // if not in range, no click action // if in this month, select the date // if out of this month, jump to the date const onClick = !this._isValidDate(new Date(day.dataset.date), this.min, this.max) ? null : this.onDateClickDatePicker; day.addEventListener(clickEvent, onClick); }); day.addEventListener('hover', (e) => { e.preventDEfault(); }); }); } _renderDays() { const start = dateFns.startOfWeek(dateFns.startOfMonth(this._visibleDate), { weekStartsOn: this.options.weekStart }); // first day of current month view const end = dateFns.endOfWeek(dateFns.endOfMonth(this._visibleDate), { weekStartsOn: this.options.weekStart }); // last day of current month view // get all days and whether they are within the current month and range const days = new Array(dateFns.differenceInDays(end, start) + 1).fill(start).map((s, i) => { const theDate = dateFns.startOfDay(dateFns.addDays(s, i)); const isThisMonth = dateFns.isSameMonth(this._visibleDate, theDate); const isInRange = this.options.isRange && this.end && dateFns.isWithinInterval(theDate, { start: dateFns.startOfDay(this.start), end: dateFns.endOfDay(this.end) }); let isDisabled = this.max ? dateFns.isAfter(dateFns.startOfDay(theDate), dateFns.endOfDay(this.max)) : false; isDisabled = !isDisabled && this.min ? dateFns.isBefore(dateFns.startOfDay(theDate), dateFns.startOfDay(this.min)) : isDisabled; let isHighlighted = false; // Disabled Dates if (Array.isArray(this.disabledDates)) { for (let j = 0; j < this.disabledDates.length; j++) { if (theDate.toDateString() === this.disabledDates[j].toDateString()) { isDisabled = true; } } } else { if (type.isFunction(this.disabledDates)) { if (this.disabledDates(this, theDate)) { isDisabled = true; } } } // Highlighted Dates if (Array.isArray(this.highlightedDates)) { for (let j = 0; j < this.highlightedDates.length; j++) { if (theDate.toDateString() === this.highlightedDates[j].toDateString()) { isHighlighted = true; } } } else { if (type.isFunction(this.highlightedDates)) { if (this.highlightedDates(this, theDate)) { isHighlighted = true; } } } // Disabled Weekdays if (this.disabledWeekDays) { this.disabledWeekDays.forEach((day) => { if (dateFns.getDay(theDate) == day) { isDisabled = true; } }); } return { date: theDate, isRange: this.options.isRange, isToday: dateFns.isToday(theDate), isStartDate: dateFns.isEqual(dateFns.startOfDay(this.start), theDate), isEndDate: dateFns.isEqual(dateFns.startOfDay(this.end), theDate), isDisabled: isDisabled, isThisMonth, isHighlighted: isHighlighted, isInRange, }; }); this._ui.body.dates.appendChild(document.createRange().createContextualFragment(templateDays(days))); this._ui.days = this._ui.body.dates.querySelectorAll('.datepicker-date'); this._bindDaysEvents(); } _select(date = undefined, emit = true) { if (!type.isDate(date)) { date = utils.newDate(date, this.format, 'yyyy-MM-dd'); } if (this.options.isRange && (!this._isValidDate(this.start) || (this._isValidDate(this.start) && this._isValidDate(this.end)))) { this.start = date; this.end = undefined; if (emit) this.emit('select:start', this); } else if (this.options.isRange && !this._isValidDate(this.end)) { if (dateFns.isBefore(date, this.start)) { this.end = this.start; this.start = date; if (emit) this.emit('select', this); } else if (dateFns.isAfter(date, this.start)) { this.end = date; if (emit) this.emit('select', this); } else if (this.options.allowSameDayRange) { this.end = date; if (emit) this.emit('select', this); } else { this.start = date; this.end = undefined; if (emit) this.emit('select:start', this); } } else { this.start = date; this.end = undefined; if (emit) this.emit('select', this); } this._setVisibleDate(this.start); if (this.options.isRange && this._isValidDate(this.start) && this._isValidDate(this.end)) { new Array(dateFns.differenceInDays(this.end, this.start) + 1).fill(this.start).map((s, i) => { const theDate = dateFns.addDays(s, i); const dateElement = this._ui.body.dates.querySelector(`.datepicker-date[data-date="${theDate.toString()}"]`); if (dateElement) { if (dateFns.isEqual(this.start, theDate)) { dateElement.classList.add('datepicker-range-start'); } if (dateFns.isEqual(this.end, theDate)) { dateElement.classList.add('datepicker-range-end'); } dateElement.classList.add('datepicker-range'); } }); } } _isValidDate(date, min, max) { try { if (!date) { return false; } if (dateFns.isValid(date)) { if (!min && !max) { return true; } if (min && max) { return dateFns.isWithinInterval(date, { start: min, end: max }); } if (max) { return dateFns.isBefore(date, max) || dateFns.isEqual(date, max); } return dateFns.isAfter(date, min) || dateFns.isEqual(date, min); } else { return false; } } catch (e) { return false; } } _setVisibleDate(date) { if (!date) return; if (this.min) date = dateFns.max([date, this.min]); if (this.max) date = dateFns.min([date, this.max]); this._visibleDate = date; } _togglePreviousButton(active = true) { if (!active) { this._ui.navigation.previous.setAttribute('disabled', 'disabled'); } else { this._ui.navigation.previous.removeAttribute('disabled'); } } _toggleNextButton(active = true) { if (!active) { this._ui.navigation.next.setAttribute('disabled', 'disabled'); } else { this._ui.navigation.next.removeAttribute('disabled'); } } }