@3mo/date-time-fields
Version:
Date time fields let people select dates, date-ranges, and times.
361 lines (335 loc) • 11.8 kB
JavaScript
var Calendar_1;
import { __decorate } from "tslib";
import { Component, css, component, html, property, event, repeat, state, unsafeCSS } from '@a11d/lit';
import { CalendarDatesController } from './CalendarDatesController.js';
import { FieldDateTimePrecision } from '../FieldDateTimePrecision.js';
/**
* @fires dateClick - Dispatched when a date is clicked, with the clicked date as detail.
*/
let Calendar = Calendar_1 = class Calendar extends Component {
constructor() {
super(...arguments);
this.includeWeek = false;
this.view = FieldDateTimePrecision.Day;
this.datesController = new CalendarDatesController(this);
this.handleItemClick = (date, precision) => {
return () => {
if (this.precision === precision) {
this.dateClick.dispatch(date);
this.setNavigatingValue(date, 'smooth');
}
else {
const nextView = this.view !== precision
? precision
: this.view === FieldDateTimePrecision.Year
? FieldDateTimePrecision.Month
: this.view === FieldDateTimePrecision.Month
? FieldDateTimePrecision.Day
: this.view;
this.setView(nextView, date);
}
};
};
}
get navigationDate() { return this.datesController.navigationDate; }
async setNavigatingValue(date, behavior = 'instant') {
this.datesController.disableObservers = true;
this.datesController.navigationDate = date;
await this.updateComplete;
await new Promise(r => setTimeout(r, 10));
this.renderRoot.querySelector(`.${this.view}[data-navigating]`)
?.scrollIntoView({ block: 'center', behavior });
await new Promise(r => setTimeout(r, behavior === 'instant' ? 10 : 100));
this.datesController.disableObservers = false;
}
setView(view, navigationDate = this.datesController.navigationDate) {
this.view = view;
this.setNavigatingValue(navigationDate);
}
static get styles() {
const year = unsafeCSS(FieldDateTimePrecision.Year.key);
const month = unsafeCSS(FieldDateTimePrecision.Month.key);
const day = unsafeCSS(FieldDateTimePrecision.Day.key);
return css `
:host {
--mo-calendar-item-size: 2.25rem;
flex: 1;
}
.scroller {
height: min(450px, 100vh);
display: grid;
position: relative;
scrollbar-width: none;
overflow-x: hidden;
scroll-behavior: smooth;
&[data-view=${day}] {
grid-template-columns: repeat(1, 1fr);
}
&[data-view=${month}] {
grid-template-columns: repeat(3, 1fr);
}
&[data-view=${year}] {
grid-template-columns: repeat(5, 1fr);
}
}
.year, .month, .day {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
transition: 0.2s;
user-select: none;
font-weight: 500;
border-radius: var(--mo-border-radius);
&:hover {
background: var(--mo-color-transparent-gray-3);
}
&[data-now] {
outline: 2px dashed var(--mo-color-gray-transparent);
}
&[data-start], &[data-end] {
background: var(--mo-color-accent-transparent);
opacity: 1;
color: color-mix(in srgb, var(--mo-color-accent), var(--mo-color-foreground)) !important;
}
&[data-in-range] {
background: color-mix(in srgb, var(--mo-color-accent), transparent 92%);
}
}
/* Headings */
.year[data-view=${month}], .month[data-view=${day}] {
font-size: 1.125rem;
font-weight: 500;
height: 2rem;
padding: 0.5rem;
}
/* Selection */
.year[data-view=${year}], .month[data-view=${month}], .day {
opacity: 0.875;
font-size: 0.94rem;
cursor: pointer;
height: var(--mo-calendar-item-size);
}
.year {
&[data-view=${day}] {
display: none;
}
&:not([data-view=${year}]) {
grid-column: -1 / 1;
border-block-start: 1px solid var(--mo-color-transparent-gray-3);
}
padding-inline: 0.5rem;
}
.month-container {
width: 100%;
position: relative;
&[data-view=${day}] {
&::before {
content: ' ';
position: absolute;
display: inline-block;
width: 1px;
height: 100%;
inset-block: 0;
background: var(--mo-color-transparent-gray-3);
}
&::after {
content: ' ';
position: absolute;
display: inline-block;
height: 1px;
width: 100%;
inset-inline: 0;
background: var(--mo-color-transparent-gray-3);
}
}
& > mo-grid {
padding-inline: 0.5rem;
padding-block-end: 0.5rem;
margin-block-start: -0.375rem;
}
&[data-view=${month}] {
& > mo-grid {
display: none;
}
}
}
.weekdays {
text-align: center;
opacity: 0.5;
font-size: 0.75rem;
font-weight: 500;
grid-column: -1 / 1;
user-select: none;
span {
display: flex;
align-items: center;
justify-content: center;
}
}
.month {
padding-inline: 0.5rem;
}
.week {
opacity: 0.5;
display: flex;
align-items: center;
justify-content: center;
}
.week-container {
grid-column: 1 / -1;
}
.day {
width: var(--mo-calendar-item-size);
height: var(--mo-calendar-item-size);
}
`;
}
get columns() {
return [
!this.includeWeek ? undefined : '[week] var(--mo-calendar-item-size)',
...CalendarDatesController.sampleWeek.map(day => `[${this.getColumnName(day)}] var(--mo-calendar-item-size)`),
].join(' ');
}
getColumnName(date) {
return `day-${date.dayOfWeek}`;
}
get template() {
return html `
<div class='scroller' data-view=${this.view.key}>
${repeat(this.datesController.data, d => d.toString(), d => this.getYearTemplate(d))}
</div>
`;
}
getYearTemplate(date) {
return html `
${date.dayOfYear !== 1 ? html.nothing : html `
<div class='year' role='button'
data-view=${this.view.key}
?data-navigating=${this.isNavigating(date, FieldDateTimePrecision.Year)}
?data-now=${this.isNow(date, FieldDateTimePrecision.Year)}
?data-start=${this.isStart(date, FieldDateTimePrecision.Year)}
?data-end=${this.isEnd(date, FieldDateTimePrecision.Year)}
?data-in-range=${this.isInRange(date, FieldDateTimePrecision.Year)}
=${this.handleItemClick(date, FieldDateTimePrecision.Year)}
${this.datesController.observerIntersectionNavigation(date, FieldDateTimePrecision.Month, FieldDateTimePrecision.Year)}
>
${date.format({ year: 'numeric' })}
</div>
`}
${this.view < FieldDateTimePrecision.Month || date.day !== 1 ? html.nothing : this.getMonthTemplate(date)}
`;
}
static get weekDaysTemplate() {
return html `
<mo-grid class='weekdays' columns='subgrid'>
${CalendarDatesController.sampleWeek.map((day, index) => {
const { narrow, short, long } = {
narrow: day.format({ weekday: 'narrow' }),
short: day.format({ weekday: 'short' }),
long: day.format({ weekday: 'long' }),
};
return html `
<span style='grid-column: day-${index + 1}' title=${long}>
${long === short ? narrow : short}
</span>
`;
})}
</mo-grid>
`;
}
getMonthTemplate(date) {
return html `
<mo-flex class='month-container' data-view=${this.view.key}>
<div class='month' role='button'
data-view=${this.view.key}
?data-navigating=${this.isNavigating(date, FieldDateTimePrecision.Month)}
?data-now=${this.isNow(date, FieldDateTimePrecision.Month)}
?data-start=${this.isStart(date, FieldDateTimePrecision.Month)}
?data-end=${this.isEnd(date, FieldDateTimePrecision.Month)}
?data-in-range=${this.isInRange(date, FieldDateTimePrecision.Month)}
=${this.handleItemClick(date, FieldDateTimePrecision.Month)}
${this.datesController.observerIntersectionNavigation(date, FieldDateTimePrecision.Day)}
>
${date.format(this.view > FieldDateTimePrecision.Month ? { year: 'numeric', month: 'long' } : { month: 'long' })}
</div>
${this.view < FieldDateTimePrecision.Day ? html.nothing : html `
<mo-grid justifyContent='center' autoRows='var(--mo-calendar-item-size)' columns=${this.view > FieldDateTimePrecision.Month ? this.columns : 'auto'}>
${Calendar_1.weekDaysTemplate}
${this.datesController.data.filter(d => d.year === date.year && d.month === date.month).map((day, _, month) => this.getWeekTemplate(day, month))}
</mo-grid>
`}
</mo-flex>
`;
}
getWeekTemplate(date, month) {
if (date.yearOfWeek === undefined || date.weekOfYear === undefined) {
return this.getDayTemplate(date);
}
if (date !== month.find(d => d.weekOfYear === date.weekOfYear && d.yearOfWeek === date.yearOfWeek)) {
return html.nothing;
}
return html `
<mo-grid class='week-container' columns='subgrid' data-view=${this.view.key}>
${month.filter(d => d.weekOfYear === date.weekOfYear && d.yearOfWeek === date.yearOfWeek).map(day => this.getDayTemplate(day))}
</mo-grid>
`;
}
getDayTemplate(day) {
return html `
${this.includeWeek === false || day.dayOfWeek !== 1 ? html.nothing : html `
<div class='week' style='grid-column: week'>${day.weekOfYear}</div>
`}
<div tabindex='0' role='button' class='day'
style='grid-column: ${this.getColumnName(day)}'
?data-navigating=${this.isNavigating(day, FieldDateTimePrecision.Day)}
?data-now=${this.isNow(day, FieldDateTimePrecision.Day)}
?data-start=${this.isStart(day, FieldDateTimePrecision.Day)}
?data-end=${this.isEnd(day, FieldDateTimePrecision.Day)}
?data-in-range=${this.isInRange(day, FieldDateTimePrecision.Day)}
=${this.handleItemClick(day, FieldDateTimePrecision.Day)}
>
${day.format({ day: 'numeric' })}
</div>
`;
}
isNavigating(date, precision) {
return this.view === precision
&& this.navigationDate.year === date.year
&& (precision < FieldDateTimePrecision.Month || this.navigationDate.month === date.month)
&& (precision < FieldDateTimePrecision.Day || this.navigationDate.day === date.day);
}
isNow(date, precision) {
return this.view === precision && precision.equals(date, CalendarDatesController.today);
}
isStart(date, precision) {
return precision === this.precision && !!this.value?.start && precision.equals(date, this.value?.start);
}
isEnd(date, precision) {
return precision === this.precision && !!this.value?.end && precision.equals(date, this.value?.end);
}
isInRange(date, precision) {
return precision === this.precision && !!this.value?.start && !!this.value?.end
&& precision.isSmallerThan(this.value.start, date)
&& precision.isSmallerThan(date, this.value.end);
}
};
__decorate([
event()
], Calendar.prototype, "dateClick", void 0);
__decorate([
property({ type: Object })
], Calendar.prototype, "value", void 0);
__decorate([
property({ type: Object, updated() { this.setView(this.precision); } })
], Calendar.prototype, "precision", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], Calendar.prototype, "includeWeek", void 0);
__decorate([
state()
], Calendar.prototype, "view", void 0);
Calendar = Calendar_1 = __decorate([
component('mo-calendar')
], Calendar);
export { Calendar };