@3mo/date-time-fields
Version:
Date time fields let people select dates, date-ranges, and times.
306 lines (296 loc) • 10.7 kB
JavaScript
import { __decorate } from "tslib";
import { cache, css, html, live, property, style, bind, state, query, ifDefined } from '@a11d/lit';
import { hasChanged } from '@a11d/equals';
import { InputFieldComponent } from '@3mo/field';
import { FieldDateTimePrecision } from './FieldDateTimePrecision.js';
const dateTimeConverter = (value) => {
if (value instanceof DateTime) {
return value;
}
if (value instanceof Date || typeof value === 'string' || typeof value === 'number') {
const date = new DateTime(value);
return isNaN(date.getTime()) ? undefined : date;
}
return undefined;
};
/**
* @attr open - Whether the date picker is open
* @attr pickerHidden - Hide the date picker
* @attr shortcutReferenceDate - The date to use as a reference for shortcuts
* @attr precision - The precision of the date picker. Defaults to 'minute'
* @attr min - The minimum selectable date (inclusive). Dates before this are disabled.
* @attr max - The maximum selectable date (inclusive). Dates after this are disabled.
* @attr dateDisabled - A function that determines whether a date should be disabled. Receives a DateTime object and should return a boolean.
*/
export class FieldDateTimeBase extends InputFieldComponent {
constructor() {
super(...arguments);
this.open = false;
this.pickerHidden = false;
this.shortcutReferenceDate = new DateTime();
this.precision = FieldDateTimePrecision.Minute;
this.navigationDate = new DateTime();
this.handleLanguageChange = () => {
this.navigationDate = new DateTime(this.navigationDate);
};
this.calendarIconButtonIcon = 'today';
}
connected() {
Localizer.languages.change.subscribe(this.handleLanguageChange);
}
disconnected() {
Localizer.languages.change.unsubscribe(this.handleLanguageChange);
}
get formatOptions() {
return {
calendar: this.selectedDate?.calendarId,
timeZone: this.selectedDate?.timeZoneId,
...this.precision.formatOptions,
};
}
valueUpdated() {
super.valueUpdated();
this.resetNavigationDate();
}
handleChange(value, e) {
super.handleChange(value, e);
this.inputStringValue = this.valueToInputValue(value);
}
get isActive() {
return super.isActive || this.open;
}
get placeholder() {
return '';
}
static get styles() {
return css `
${super.styles}
:host {
position: relative;
}
:host([disabled]) {
pointer-events: none;
}
mo-field {
anchor-name: --mo-field-date-time;
}
mo-popover {
position-anchor: --mo-field-date-time;
position-visibility: anchors-visible;
background: var(--mo-color-background);
box-shadow: var(--mo-shadow);
border-radius: var(--mo-border-radius);
color: var(--mo-color-foreground);
font-size: 0.875em;
}
#selector {
height: clamp(175px, 100vh, 450px);
}
.timezone {
padding: 0.4rem;
font-size: small;
text-align: center;
font-weight: 500;
color: var(--mo-color-gray);
}
input::placeholder {
color: transparent;
}
mo-field[active]:not([dense]) input::placeholder {
color: var(--mo-color-gray);
}
#presets {
background-color: var(--mo-color-transparent-gray-1);
mo-list-item {
padding-block: 0.5rem;
min-height: auto;
opacity: 0.9;
}
}
`;
}
get template() {
return html `
${super.template}
${this.popoverTemplate}
`;
}
get endSlotTemplate() {
return html `
${super.endSlotTemplate}
${this.clearIconButtonTemplate}
${this.calendarIconButtonTemplate}
`;
}
get inputTemplate() {
return html `
<input
part='input'
?readonly=${this.readonly}
?required=${this.required}
?disabled=${this.disabled}
placeholder=${this.placeholder}
.value=${live(this.inputStringValue || '')}
=${(e) => this.handleInput(this.inputValueToValue(this.inputElement.value || ''), e)}
=${(e) => this.handleChange(this.inputValueToValue(this.inputElement.value || ''), e)}
>
`;
}
get clearIconButtonTemplate() {
const clear = (e) => {
e.stopPropagation();
this.handleInput(undefined);
this.handleChange(undefined);
};
return !this.value || !this.focusController.focused ? html.nothing : html `
<mo-icon-button tabindex='-1' dense slot='end' icon='cancel'
=${clear}
${style({ color: 'var(--mo-color-gray)', fontSize: '20px', cursor: 'pointer', userSelect: 'none', marginBlockStart: '2.75px', marginInlineEnd: '5px' })}
></mo-icon-button>
`;
}
get calendarIconButtonTemplate() {
return this.pickerHidden ? html.nothing : html `
<mo-icon tabindex='-1' slot='end'
icon=${this.calendarIconButtonIcon}
${style({ color: this.isActive ? 'var(--mo-color-accent)' : 'var(--mo-color-gray)', fontSize: '22px', marginBlockStart: '2px', cursor: 'pointer', userSelect: 'none' })}
></mo-icon>
`;
}
get popoverTemplate() {
return this.pickerHidden ? html.nothing : html `
<mo-popover tabindex='-1'
.anchor=${this} target='field'
?open=${bind(this, 'open', { sourceUpdated: () => setTimeout(() => this.calendar?.setNavigatingValue(this.navigationDate)) })}
>
${cache(!this.open ? html.nothing : html `
<mo-flex direction='horizontal'>
${this.presetsTemplate === html.nothing ? html.nothing : html `
<mo-flex id='presets'>
${this.presetsTemplate}
</mo-flex>
`}
${this.popoverSelectionTemplate}
</mo-flex>
`)}
</mo-popover>
`;
}
get popoverSelectionTemplate() {
return html `
<mo-flex id='selector' direction='horizontal' style='flex: 1'>
${this.dateTemplate}
${this.timeTemplate}
</mo-flex>
`;
}
getPresetTemplate(label, value) {
const v = value();
if (this.isPresetDisabled(v)) {
return html.nothing;
}
const handlePresetClick = () => {
this.handleChange(v);
this.calendar?.setNavigatingValue(v instanceof DateTimeRange ? v.start : v instanceof DateTime ? v : undefined, 'smooth');
};
return html `<mo-list-item =${handlePresetClick}>${label}</mo-list-item>`;
}
isPresetDisabled(value) {
const dates = value instanceof DateTimeRange
? [value.start, value.end].filter((d) => !!d)
: value instanceof DateTime ? [value] : [];
const precision = this.precision > FieldDateTimePrecision.Day ? FieldDateTimePrecision.Day : this.precision;
return dates.some(date => (!!this.min && precision.isSmallerThan(date, this.min) && !precision.equals(date, this.min))
|| (!!this.max && precision.isSmallerThan(this.max, date) && !precision.equals(this.max, date))
|| (this.dateDisabled?.(date) ?? false));
}
get dateTemplate() {
return this.calendarTemplate;
}
get calendarTemplate() {
return html `
<mo-calendar
.precision=${this.precision > FieldDateTimePrecision.Day ? FieldDateTimePrecision.Day : this.precision}
.value=${this.calendarValue}
.min=${this.min}
.max=${this.max}
.dateDisabled=${this.dateDisabled}
=${(e) => this.handleSelectedDateChange(e.detail, this.precision)}
></mo-calendar>
`;
}
get timeTemplate() {
return this.precision <= FieldDateTimePrecision.Day ? html.nothing : html `
<mo-flex gap='0.5rem' style='border-inline-start: 1px solid var(--mo-color-transparent-gray-3); min-height: 0'>
<mo-flex direction='horizontal' style='flex: 1; min-height: 0'>
${this.hourListTemplate}
${this.minuteListTemplate}
${this.secondListTemplate}
</mo-flex>
<div class='timezone' title=${ifDefined(this.navigationDate?.formatToParts({ timeZoneName: 'long' }).find(x => x.type === 'timeZoneName')?.value)}>
${this.navigationDate?.formatToParts({ timeZoneName: 'shortOffset' }).find(x => x.type === 'timeZoneName')?.value}
</div>
</mo-flex>
`;
}
get hourListTemplate() {
return this.precision < FieldDateTimePrecision.Hour ? html.nothing : html `
<mo-hour-list style='flex: 1'
.navigationDate=${bind(this, 'navigationDate')}
.value=${this.selectedDate}
=${(e) => this.handleSelectedDateChange(e.detail, FieldDateTimePrecision.Hour)}
></mo-hour-list>
`;
}
get minuteListTemplate() {
return this.precision < FieldDateTimePrecision.Minute ? html.nothing : html `
<mo-minute-list style='flex: 1'
.navigationDate=${bind(this, 'navigationDate')}
.value=${this.selectedDate}
=${(e) => this.handleSelectedDateChange(e.detail, FieldDateTimePrecision.Minute)}
></mo-minute-list>
`;
}
get secondListTemplate() {
return this.precision < FieldDateTimePrecision.Second ? html.nothing : html `
<mo-second-list style='flex: 1'
.navigationDate=${bind(this, 'navigationDate')}
.value=${this.selectedDate}
=${(e) => this.handleSelectedDateChange(e.detail, FieldDateTimePrecision.Second)}
></mo-second-list>
`;
}
handleSelectedDateChange(date, precision) {
date;
precision;
this.change.dispatch(this.value);
this.requestUpdate();
}
}
__decorate([
property({ type: Boolean, reflect: true })
], FieldDateTimeBase.prototype, "open", void 0);
__decorate([
property({ type: Boolean })
], FieldDateTimeBase.prototype, "pickerHidden", void 0);
__decorate([
property({ type: Object, converter: dateTimeConverter })
], FieldDateTimeBase.prototype, "shortcutReferenceDate", void 0);
__decorate([
property({ type: String, converter: value => FieldDateTimePrecision.parse(value || undefined) })
], FieldDateTimeBase.prototype, "precision", void 0);
__decorate([
property({ type: Object, converter: dateTimeConverter })
], FieldDateTimeBase.prototype, "min", void 0);
__decorate([
property({ type: Object, converter: dateTimeConverter })
], FieldDateTimeBase.prototype, "max", void 0);
__decorate([
property({ type: Object, hasChanged })
], FieldDateTimeBase.prototype, "dateDisabled", void 0);
__decorate([
state()
], FieldDateTimeBase.prototype, "navigationDate", void 0);
__decorate([
query('mo-calendar')
], FieldDateTimeBase.prototype, "calendar", void 0);