@doubletrade/lit-datepicker
Version:
lit-datepicker provides a simple datepicker with range.
541 lines (481 loc) • 18.5 kB
JavaScript
import { LitElement, html, css } from 'lit';
import '@polymer/paper-dropdown-menu/paper-dropdown-menu';
import '@polymer/paper-listbox/paper-listbox';
import '@polymer/paper-item/paper-item';
import '@polymer/paper-icon-button/paper-icon-button';
import '@polymer/iron-icons/iron-icons';
import '@polymer/paper-styles/paper-styles';
import format from 'date-fns/esm/format';
import parse from 'date-fns/esm/parse';
import addDays from 'date-fns/esm/addDays';
import endOfMonth from 'date-fns/esm/endOfMonth';
import startOfDay from 'date-fns/esm/startOfDay';
import getDay from 'date-fns/esm/getDay';
import getMonth from 'date-fns/esm/getMonth';
import getYear from 'date-fns/esm/getYear';
import addMonths from 'date-fns/esm/addMonths';
import addYears from 'date-fns/esm/addYears';
import subMonths from 'date-fns/esm/subMonths';
import subYears from 'date-fns/esm/subYears';
import enUS from 'date-fns/esm/locale/en-US';
import fr from 'date-fns/esm/locale/fr';
import { ironFlexLayoutAlignTheme, ironFlexLayoutTheme } from './iron-flex-import';
import './lit-datepicker-cell';
const locales = { en: enUS, fr };
class LitDatepickerCalendar extends LitElement {
static get styles() {
const mainStyle = css`
:host {
font-family: Roboto;
display: block;
width: 266px;
}
:host>div {
overflow: hidden;
}
div.table {
display: table;
border-collapse: collapse;
table-layout: fixed;
margin-top: 10px;
}
div.th {
display: table-cell;
line-height: 20px;
font-weight: 400;
color: var(--lit-datepicker-day-names-text, rgb(117, 117, 117));
font-size: 11px;
width: 38px;
padding: 0;
margin: 0;
text-align: center;
}
div.tr {
display: table-row;
height: 38px;
}
div.td {
display: table-cell;
padding: 0;
width: 38px;
margin: 0;
}
.monthName {
font-size: 20px;
font-weight: 500;
line-height: 28px;
width: 266px;
margin: 10px 0 0 0;
text-align: center;
color: var(--lit-datepicker-month-text);
height: 32px;
overflow: hidden;
}
.monthName::first-letter {
text-transform: uppercase;
}
.monthName>div>div {
margin-right: 8px;
height: 30px;
}
paper-listbox {
max-height: 200px;
}
div.tbody {
transition: all 0ms;
transform: translateX(0);
height: 235px;
}
.withTransition {
transition: all 100ms;
}
.moveToLeft {
transform: translateX(-274px);
}
.moveToRight {
transform: translateX(274px);
}
.withTransition td,
.moveToLeft td,
.moveToRight td {
border: none;
}
paper-dropdown-menu {
width: 75px;
padding: 0;
height: auto;
}
.go-today {
text-align: center;
text-decoration: underline;
font-size: 10px;
color: var(--lit-datepicker-today-shortcut, rgb(0, 150, 136));
height: 11px;
}
.go-today span {
cursor: pointer;
}
.month-change {
min-width: 130px;
}
.monthName paper-icon-button {
width: 30px;
height: 30px;
padding: 0px;
}
`;
return [mainStyle, ironFlexLayoutTheme, ironFlexLayoutAlignTheme];
}
render() {
return html`
<div>
<div class="monthName layout horizontal center">
${this.prev || this.narrow || this.enableYearChange ? html`<paper-icon-button icon="chevron-left" @tap="${this.handlePrevMonth.bind(this)}"></paper-icon-button>` : null}
<div class="flex layout horizontal center center-justified">
${this.enableMonthChange ? html`
<paper-dropdown-menu class="month-change" no-label-float>
<paper-listbox slot="dropdown-content" selected="${this.month}" @selected-changed="${this.handleMonthListChanged.bind(this)}" attr-for-selected="data-name">
${this.monthsList && this.monthsList.map((monthList) => html`<paper-item data-name="${monthList}">${this.computeCurrentMonthName(monthList, this.year)}</paper-item>`)}
</paper-listbox>
</paper-dropdown-menu>` : html`<div>${this.computeCurrentMonthName(this.month, this.year)}</div>`}
${this.enableYearChange ? html`
<paper-dropdown-menu class="year-change" no-label-float>
<paper-listbox slot="dropdown-content" selected="${this.year}" @selected-changed="${this.handleYearListChanged.bind(this)}" attr-for-selected="data-name">
${this.yearsList && this.yearsList.map((yearList) => html`<paper-item data-name="${yearList}">${yearList}</paper-item>`)}
</paper-listbox>
</paper-dropdown-menu>` : html`${this.year}`}
</div>
${this.next || this.narrow || this.enableYearChange ? html`<paper-icon-button icon="chevron-right" @tap="${this.handleNextMonth.bind(this)}"></paper-icon-button>` : null}
</div>
<div class="go-today">
${this.shouldDisplayGoToday(this.displayGoToday, this.month, this.year) ? html`
<span @tap=${this.goToday}>Aujourd'hui</span>
` : null}
</div>
<div class="table">
<div class="thead">
<div class="tr">
${this.dayNamesOfTheWeek && this.dayNamesOfTheWeek.map((dayNameOfWeek) => html`<div class="th">${dayNameOfWeek}</div>`)}
</div>
</div>
<div class="tbody">
${this.daysOfMonth && this.daysOfMonth.map((week) => html`
<div class="tr">
${week && week.map((dayOfMonth) => html`
<div class="td ${this.tdIsEnabled(dayOfMonth)}">
${dayOfMonth ? html`
<lit-datepicker-cell
.disabledDays="${this.disabledDays}"
.min="${this.min}"
.max="${this.max}"
.month="${this.month}"
.hoveredDate="${this.hoveredDate}"
.dateTo="${this.dateTo}"
.dateFrom="${this.dateFrom}"
.day="${dayOfMonth}"
?isCurrentDate="${this.isCurrentDate(dayOfMonth)}"
@date-is-hovered="${this.handleDateHovered.bind(this)}"
@date-is-selected="${this.handleDateSelected.bind(this)}"></lit-datepicker-cell>
` : null}
</div>`)}
</div>`)}
</div>
</div>
</div>
`;
}
constructor() {
super();
this.dayNamesOfTheWeek = [];
this.locale = 'en';
this.narrow = false;
this.yearsList = [];
this.enableYearChange = false;
this.enableMonthChange = false;
this.noRange = false;
this.maxRange = 0;
this.currentDate = parseInt(format(startOfDay(Date.now()), 't'), 10);
this.displayGoToday = false;
this.defaultAs = 'today';
}
isCurrentDate(dayOfMonth) {
const dayDate = dayOfMonth.date;
return dayDate === this.currentDate;
}
updated(properties) {
if (properties.has('locale')) {
this.localeChanged();
}
if (properties.has('enableYearChange')) {
this.enableYearChangeChanged(this.enableYearChange);
}
if (properties.has('year')) {
this.dispatchEvent(new CustomEvent('year-changed', { detail: { value: this.year } }));
}
if (properties.has('year') || properties.has('month')) {
this.yearAndMonthChanged(this.year, this.month);
}
}
static get properties() {
return {
month: { type: String },
prev: { type: Boolean },
next: { type: Boolean },
min: { type: Number },
max: { type: Number },
disabledDays: { type: Array },
year: { type: String },
dayNamesOfTheWeek: { type: Array },
daysOfMonth: { type: Array },
locale: { type: String },
dateTo: { type: Number },
dateFrom: { type: Number },
hoveredDate: { type: Number },
noRange: { type: Boolean },
maxRange: { type: Number },
narrow: { type: Boolean },
yearsList: { type: Array },
monthsList: { type: Array },
enableYearChange: { type: Boolean },
enableMonthChange: { type: Boolean },
displayGoToday: { type: Boolean },
defaultAs: { type: String },
};
}
localeChanged() {
if (locales[this.locale]) {
const dayNamesOfTheWeek = [];
let i = 0;
for (i; i < 7; i += 1) {
dayNamesOfTheWeek.push(locales[this.locale].localize.day(i, { width: 'short' }));
}
const firstDayOfWeek = locales[this.locale].options.weekStartsOn;
const tmp = dayNamesOfTheWeek.slice().splice(0, firstDayOfWeek);
const newDayNamesOfTheWeek = dayNamesOfTheWeek
.slice()
.splice(firstDayOfWeek, dayNamesOfTheWeek.length)
.concat(tmp);
this.dayNamesOfTheWeek = newDayNamesOfTheWeek;
}
}
yearAndMonthChanged(year, month) {
if (year && month) {
let monthMinus = month;
monthMinus = monthMinus.substring(monthMinus.length - 2);
let startDateString = `01/${monthMinus}/${year}`;
let startDateFn = parse(startDateString, 'dd/MM/yyyy', new Date(), { awareOfUnicodeTokens: true });
const endDateFn = endOfMonth(startDateFn);
const endDateString = format(endDateFn, 'dd/MM/yyyy', { awareOfUnicodeTokens: true });
const firstDayOfWeek = locales[this.locale].options.weekStartsOn;
const rows = [];
let columns = [];
const lastDayOfWeek = 6;
while (startDateString !== endDateString) {
let dayNumberFn = getDay(startDateFn) - firstDayOfWeek;
if (dayNumberFn < 0) {
dayNumberFn = 6;
}
const columnFn = {
hover: false,
date: parseInt(format(startDateFn, 't'), 10),
title: parseInt(format(startDateFn, 'd', { awareOfUnicodeTokens: true }), 10),
};
columns.push(columnFn);
if (dayNumberFn === lastDayOfWeek) {
for (let i = columns.length; i < lastDayOfWeek + 1; i += 1) {
columns.unshift(0);
}
rows.push(columns.slice());
columns = [];
}
startDateFn = addDays(startDateFn, 1);
startDateString = format(startDateFn, 'dd/MM/yyyy', { awareOfUnicodeTokens: true });
if (startDateString === endDateString) {
const endColumnFn = {
hover: false,
date: parseInt(format(startDateFn, 't'), 10),
title: parseInt(format(startDateFn, 'd', { awareOfUnicodeTokens: true }), 10),
};
columns.push(endColumnFn);
for (let i = columns.length; i <= lastDayOfWeek; i += 1) {
columns.push(0);
}
rows.push(columns.slice());
columns = [];
}
}
this.daysOfMonth = rows;
}
}
computeCurrentMonthName(month, year) {
if (month && year) {
const dateFn = parse(`${month}/${year}`, 'MM/yyyy', new Date());
return format(dateFn, 'MMMM', { locale: locales[this.locale] });
}
return '';
}
tdIsEnabled(day) {
if (day) {
return 'enabled';
}
return '';
}
handleDateSelected({ detail }) {
const { date } = detail;
if (!this.noRange) {
if (this.dateFrom && this.dateTo) {
this.dateFrom = date;
this.dateTo = null;
this.hoveredDate = undefined;
this.dispatchEvent(new CustomEvent('hovered-date-changed', { detail: { value: this.hoveredDate } }));
} else if (!this.dateFrom || (this.dateFrom && date < this.dateFrom) || (this.maxRange > 0 && date - this.dateFrom > this.maxRange * 24 * 3600)) {
this.dateFrom = date;
} else if (!this.dateTo || (this.dateTo && date > this.dateTo)) {
this.dateTo = date;
}
} else {
this.dateFrom = date;
}
this.dispatchEvent(new CustomEvent('date-from-changed', { detail: { value: this.dateFrom } }));
this.dispatchEvent(new CustomEvent('date-to-changed', { detail: { value: this.dateTo } }));
}
handleDateHovered(event) {
if (!this.noRange) {
this.hoveredDate = event.detail.date;
this.dispatchEvent(new CustomEvent('hovered-date-changed', { detail: { value: this.hoveredDate } }));
}
}
handleNextMonth() {
const tbody = this.shadowRoot.querySelector('.tbody');
const monthName = this.shadowRoot.querySelector('.monthName > div');
tbody.classList.add('withTransition');
tbody.classList.add('moveToLeft');
monthName.classList.add('withTransition');
monthName.classList.add('moveToLeft');
const month = parse(this.month, 'MM', new Date());
const monthPlusDate = addMonths(month, 1);
const monthPlusString = format(monthPlusDate, 'MM', { locale: locales[this.locale] });
this.month = monthPlusString;
if (this.month === '01') {
const year = parse(this.year, 'yyyy', new Date());
const yearPlusDate = addYears(year, 1);
const yearPlusString = format(yearPlusDate, 'yyyy', { locale: locales[this.locale] });
this.year = yearPlusString;
}
this.dispatchEvent(new CustomEvent('next-month'));
setTimeout(() => {
tbody.classList.remove('withTransition');
tbody.classList.add('moveToRight');
tbody.classList.remove('moveToLeft');
monthName.classList.remove('withTransition');
monthName.classList.add('moveToRight');
monthName.classList.remove('moveToLeft');
setTimeout(() => {
tbody.classList.add('withTransition');
tbody.classList.remove('moveToRight');
monthName.classList.add('withTransition');
monthName.classList.remove('moveToRight');
setTimeout(() => {
tbody.classList.remove('withTransition');
monthName.classList.remove('withTransition');
}, 100);
}, 100);
}, 100);
}
handlePrevMonth() {
const tbody = this.shadowRoot.querySelector('.tbody');
const monthName = this.shadowRoot.querySelector('.monthName > div');
tbody.classList.add('withTransition');
tbody.classList.add('moveToRight');
monthName.classList.add('withTransition');
monthName.classList.add('moveToRight');
const month = parse(this.month, 'MM', new Date());
const monthMinusDate = subMonths(month, 1);
const monthMinusString = format(monthMinusDate, 'MM', { locale: locales[this.locale] });
this.month = monthMinusString;
if (this.month === '12') {
const year = parse(this.year, 'yyyy', new Date());
const yearMinusDate = subYears(year, 1);
const yearMinusString = format(yearMinusDate, 'yyyy', { locale: locales[this.locale] });
this.year = yearMinusString;
}
this.dispatchEvent(new CustomEvent('prev-month'));
setTimeout(() => {
tbody.classList.remove('withTransition');
tbody.classList.add('moveToLeft');
tbody.classList.remove('moveToRight');
monthName.classList.remove('withTransition');
monthName.classList.add('moveToLeft');
monthName.classList.remove('moveToRight');
setTimeout(() => {
tbody.classList.add('withTransition');
tbody.classList.remove('moveToLeft');
monthName.classList.add('withTransition');
monthName.classList.remove('moveToLeft');
setTimeout(() => {
monthName.classList.remove('withTransition');
monthName.classList.remove('withTransition');
}, 100);
}, 100);
}, 100);
}
setYears(from, to) {
const yearsList = [];
for (let i = from; i <= to; i += 1) {
yearsList.push(i);
}
this.yearsList = yearsList;
}
async firstUpdated() {
this.monthsList = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
setTimeout(() => { this.setYears(1930, 2100); });
await this.updateComplete;
if (this.enableYearChange) {
const paperDropdownMenu = this.shadowRoot.querySelector('.year-change');
paperDropdownMenu.updateStyles({
'--paper-input-container-underline_-_display': 'none',
'--paper-input-container-shared-input-style_-_font-weight': '500',
'--paper-input-container-shared-input-style_-_text-align': 'right',
'--paper-input-container-shared-input-style_-_font-size': '20px',
'--paper-input-container_-_width': '75px',
'--paper-input-container_-_padding': '0',
'--paper-input-container-shared-input-style_-_color': 'var(--paper-datatable-navigation-bar-text-color, rgba(0, 0, 0, .54))',
'--paper-input-container-input-color': 'var(--paper-datatable-navigation-bar-text-color, rgba(0, 0, 0, .54))',
'--disabled-text-color': 'var(--paper-datatable-navigation-bar-text-color, rgba(0, 0, 0, .54))',
});
}
if (this.enableMonthChange) {
const paperDropdownMenu = this.shadowRoot.querySelector('.month-change');
paperDropdownMenu.updateStyles({
'--paper-input-container-underline_-_display': 'none',
'--paper-input-container-shared-input-style_-_font-weight': '500',
'--paper-input-container-shared-input-style_-_text-align': 'center',
'--paper-input-container-shared-input-style_-_font-size': '20px',
'--paper-input-container_-_width': '75px',
'--paper-input-container_-_padding': '0',
'--paper-input-container-shared-input-style_-_color': 'var(--paper-datatable-navigation-bar-text-color, rgba(0, 0, 0, .54))',
'--paper-input-container-input-color': 'var(--paper-datatable-navigation-bar-text-color, rgba(0, 0, 0, .54))',
'--disabled-text-color': 'var(--paper-datatable-navigation-bar-text-color, rgba(0, 0, 0, .54))',
});
}
}
enableYearChangeChanged(enableYearChange) {
this.enableYearChange = enableYearChange;
}
handleYearListChanged({ detail }) {
this.year = detail.value;
}
handleMonthListChanged({ detail }) {
this.month = detail.value;
}
goToday() {
this.month = `0${getMonth(new Date()) + 1}`.slice(-2);
this.year = getYear(new Date());
}
shouldDisplayGoToday(displayGoToday, month, year) {
return displayGoToday
&& (parseInt(month, 10) !== getMonth(new Date()) + 1
|| parseInt(year, 10) !== getYear(new Date()));
}
}
window.customElements.define('lit-datepicker-calendar', LitDatepickerCalendar);