UNPKG

@3mo/date-time-fields

Version:

Date time fields let people select dates, date-ranges, and times.

306 lines (296 loc) 10.7 kB
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 || '')} @input=${(e) => this.handleInput(this.inputValueToValue(this.inputElement.value || ''), e)} @change=${(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' @click=${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 @click=${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} @dateClick=${(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} @change=${(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} @change=${(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} @change=${(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);