bulma-calendar
Version:
Display a calendar for date/time selection, in different colors and sizes
856 lines (690 loc) • 28.3 kB
JavaScript
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');
}
}
}