@gez/date-time-kit
Version:
498 lines (476 loc) • 18.3 kB
text/typescript
import {
type DateGranularity,
type DateTimeGranularity,
type TimeGranularity,
closestByEvent,
granHelper
} from '../../utils';
import {
type Ele as CalendarBaseEle,
type EventMap as CalendarBaseEvent,
type Weeks,
weekKey
} from '../calendar';
import type { Ele as EchoEle } from '../echo';
import type {
DateFormatterFn,
DatetimeFormatterFn,
TimeFormatterFn
} from '../echo/utils';
import type {
Ele as HhMmSsMsSelectorEle,
EventMap as HhMmSsMsSelectorEvent
} from '../hhmmss-ms-list-grp/selector';
import type { Ele as PopoverEle } from '../popover';
import {
clearupPopEleAttrSync2Parent,
isPopoverAttrKey,
parentPopAttrSync2PopEle,
popEleAttrSync2Parent,
popoverAttrKeys,
type reExportPopoverAttrs
} from '../popover/attr-sync-helper';
import {
type BaseAttrs,
type BaseEmits,
type Emit2EventMap,
UiBase
} from '../web-component-base';
import {
Ele as YyyyMmNavEle,
type EventMap as YyyyMmNavEvent
} from '../yyyymm-nav';
import type {
Ele as YyyyMmDdSelectorEle,
EventMap as YyyyMmDdSelectorEvt
} from '../yyyymmdd-list-grp/selector';
import { GranType } from './common';
import html from './index.html';
import { styleStr } from './styleStr';
export const granularityList = granHelper.dateTime.list;
export type Granularity = DateTimeGranularity;
export type Attrs = BaseAttrs &
reExportPopoverAttrs & {
/**
* Set which day of the week is the first day.
* @type `'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'`
* @default 'sun'
*/
'week-start-at'?: Weeks;
/**
* The time of the calendar.
* @type {`string | number`} A value that can be passed to the Date constructor.
* @default Math.min('max-time', Math.max('min-time', Date.now()))
*/
'current-time'?: string | number;
/**
* The showing time, used to determine the month to show on calendar.
* @type {`string | number`} A value that can be passed to the Date constructor.
* @default 'current-time'
*/
'showing-time'?: string | number;
/**
* 选择器的粒度,表示最小可选的时间单位。默认为 millisecond。
* 例如设置为 'minute',则表示只能选择到分钟,秒和毫秒将被忽略。忽略的时间单位将被重置为 0。
*/
'min-granularity'?: DateTimeGranularity;
/**
* 选择器的粒度,表示最大可选的时间单位。默认为 year。
* 例如设置为 'day',则表示只能选择到日,年和月秒将被忽略。忽略的时间单位将被重置为 0、1972(离1970最近的闰年)。
*/
'max-granularity'?: DateTimeGranularity;
/**
* The minimum time of the calendar display range.
* @type {`string | number`} A value that can be passed to the Date constructor.
*/
'min-time'?: string | number;
/**
* The maximum time of the calendar display range.
* @type {`string | number`} A value that can be passed to the Date constructor.
*/
'max-time'?: string | number;
};
export interface Emits extends BaseEmits {
'select-time': Date;
'open-change': boolean;
}
export type EventMap = Emit2EventMap<Emits>;
/**
* 日期时间选择器(单个时间点)
* 包括日历和时分秒毫秒选择。
*
* - 存在一个 timeFormatter 方法,用于格式化时分秒毫秒显示时间。
* - 存在一个 dateFormatter 方法,用于格式化年月日显示时间。
*/
export class Ele extends UiBase<Attrs, Emits> {
public static readonly tagName = 'dt-date-time-selector' as const;
protected static _style = styleStr;
protected static _template = html;
static get observedAttributes(): string[] {
return [
...(super.observedAttributes as (keyof BaseAttrs)[]),
'week-start-at',
'current-time',
'showing-time',
'min-time',
'max-time',
'min-granularity',
'max-granularity',
...popoverAttrKeys
] satisfies (keyof Attrs)[];
}
private _getTimeAttr(name: keyof Attrs, defaultValue: string) {
const v = this._getAttr(name, defaultValue);
return new Date(Number.isNaN(+v) ? v : +v);
}
private _setTimeAttr(name: keyof Attrs, value: number | string | Date) {
const v = new Date(value);
if (Number.isNaN(+v)) return;
this.setAttribute(name, +v + '');
}
private _getMaxMinTime({
min = +this._getTimeAttr('min-time', 'NaN'),
max = +this._getTimeAttr('max-time', 'NaN')
} = {}) {
if (Number.isNaN(min)) min = Number.NEGATIVE_INFINITY;
if (Number.isNaN(max)) max = Number.POSITIVE_INFINITY;
if (min > max) [min, max] = [max, min];
return { min, max };
}
public get currentTime() {
const { min, max } = this._getMaxMinTime();
const currTime = this._getTimeAttr('current-time', '' + Date.now());
if (+currTime < min) return new Date(min);
if (+currTime > max) return new Date(max);
return currTime;
}
public set currentTime(val: number | string | Date) {
const v = new Date(val);
if (Number.isNaN(+v)) return;
const { min, max } = this._getMaxMinTime();
this._setTimeAttr('current-time', Math.min(max, Math.max(min, +v)));
}
public get showingTime() {
return this._getTimeAttr('showing-time', '' + +this.currentTime);
}
public set showingTime(val: number | string | Date) {
this._setTimeAttr('showing-time', val);
}
public get minTime() {
return this._getMaxMinTime().min;
}
public set minTime(val: number | string | Date) {
const { min, max } = this._getMaxMinTime({
min: +new Date(Number.isNaN(+val) ? val : +val)
});
this._setTimeAttr('min-time', min);
this._setTimeAttr('max-time', max);
}
public get maxTime() {
return this._getMaxMinTime().max;
}
public set maxTime(val: number | string | Date) {
const { min, max } = this._getMaxMinTime({
max: +new Date(Number.isNaN(+val) ? val : +val)
});
this._setTimeAttr('min-time', min);
this._setTimeAttr('max-time', max);
}
public get weekStartAt() {
return this._getAttr('week-start-at', 'sun');
}
public set weekStartAt(val: Weeks) {
if (!weekKey.includes(val)) return;
this.setAttribute('week-start-at', val);
}
public get minGranularity() {
return this._getAttr('min-granularity', 'millisecond');
}
public set minGranularity(val: DateTimeGranularity) {
if (!granHelper.dateTime.has(val)) return;
this.setAttribute('min-granularity', val);
}
public get maxGranularity() {
return this._getAttr('max-granularity', 'year');
}
public set maxGranularity(val: DateTimeGranularity) {
if (!granHelper.dateTime.has(val)) return;
this.setAttribute('max-granularity', val);
}
get _staticEls() {
return {
...super._staticEls,
hostWrapper: this.$0`.host-wrapper`!,
nav: this.$0<YyyyMmNavEle>`dt-yyyymm-nav`!,
calendar: this.$0<CalendarBaseEle>`dt-calendar-base`!,
timeSelectorInCalendar: this
.$0<HhMmSsMsSelectorEle>`dt-popover dt-hhmmss-ms-selector`!,
timeSelectorOnly: this
.$0<HhMmSsMsSelectorEle>`dt-hhmmss-ms-selector.timeOnly`!,
dateSelector: this.$0<YyyyMmDdSelectorEle>`dt-yyyymmdd-selector`!,
timeSelectorInDate: this
.$0<HhMmSsMsSelectorEle>`dt-yyyymmdd-selector dt-hhmmss-ms-selector`!,
popover: this.$0<PopoverEle>`dt-popover`!,
slots: this.$<HTMLSlotElement>`slot`!,
echoInDate: this.$0<EchoEle>`dt-yyyymmdd-selector dt-echo`!,
echoInPopover: this.$0<EchoEle>`dt-popover dt-echo`!
} as const;
}
private get _minmaxGran() {
const [min, max] = granHelper.dateTime.minmax(
this.minGranularity,
this.maxGranularity
);
return { min, max };
}
private get _granType() {
const { isDateGran, isTimeGran } = granHelper.dateTime;
const { min, max } = this._minmaxGran;
if (max === 'year' && min === 'day') {
return GranType.Calendar;
} else if (isTimeGran(max) && isTimeGran(min)) {
return GranType.Time;
} else if (isDateGran(max) && isDateGran(min)) {
return GranType.Date;
} else if ((max === 'month' || max === 'day') && isTimeGran(min)) {
return GranType.DateTime;
} else {
return GranType.CalendarTime;
}
}
private _updateSlot() {
const { _els, _granType } = this;
const hasSlotTrigger = !!this.querySelector('[slot="trigger"]');
_els.slots.forEach((slot) => {
if (slot.matches(`[data-type~='${_granType}']`)) {
slot.setAttribute('name', 'trigger');
} else {
slot.removeAttribute('name');
}
if (slot.matches(`[data-type~='${GranType.Time}']`)) {
if (hasSlotTrigger) {
slot.setAttribute('slot', 'trigger');
} else {
slot.removeAttribute('slot');
}
}
});
}
private _updateOpenState(force = this.hasAttribute('pop-open')) {
const { _els, _granType } = this;
const isDateGran =
_granType === GranType.Date || _granType === GranType.DateTime;
const isCalendarGran =
_granType === GranType.Calendar ||
_granType === GranType.CalendarTime;
const isTimeGran = _granType === GranType.Time;
// use toggleAttribute to avoid element not connected yet
_els.popover.toggleAttribute('open', force && isCalendarGran);
_els.echoInPopover.toggleAttribute('active', force && isCalendarGran);
_els.dateSelector.toggleAttribute('pop-open', force && isDateGran);
_els.echoInDate.toggleAttribute('active', force && isDateGran);
_els.timeSelectorOnly.toggleAttribute('pop-open', force && isTimeGran);
this.toggleAttribute('pop-open', force);
}
public get open() {
return this.hasAttribute('pop-open');
}
public set open(v: boolean) {
this._updateOpenState(v);
}
private _ob: MutationObserver | null = null;
public connectedCallback() {
if (!super.connectedCallback()) return;
const { _els } = this;
this._render();
popEleAttrSync2Parent(this, _els.popover);
this._bindEvt(_els.calendar)('select-time', this._onCalendarSelect);
this._bindEvt(_els.nav)('change', this._onNavChange);
this._bindEvt(_els.nav)('popover-open-change', this._onNavOpenToggle);
this._bindEvt([_els.timeSelectorInCalendar, _els.timeSelectorOnly])(
'select-time',
this._onTimeSelectorChange
);
this._bindEvt([_els.timeSelectorInCalendar, _els.timeSelectorInDate])(
'open-change',
this._stopEvent
);
this._bindEvt(_els.dateSelector)('open-change', (e) => {
if (!(this.open = e.detail))
_els.timeSelectorInDate.currentTime = this.currentTime;
});
this._bindEvt(_els.dateSelector)(
'select-time',
this._onDateSelectorSelect
);
this._bindEvt(_els.timeSelectorOnly)(
'open-change',
(e) => (this.open = e.detail)
);
this._bindEvt<HTMLButtonElement>`.confirmBtn`(
'click',
this._onConfirmBtnClick
);
this._ob = new MutationObserver(() => this._updateSlot());
this._ob.observe(this, { childList: true });
this.dispatchEvent('select-time', this.currentTime as Date);
}
public disconnectedCallback() {
clearupPopEleAttrSync2Parent(this);
this._ob?.disconnect();
this._ob = null;
return super.disconnectedCallback();
}
protected _onAttrChanged(
name: string,
oldValue: string | null,
newValue: string | null
) {
super._onAttrChanged(name, oldValue, newValue);
const { _els } = this;
if (isPopoverAttrKey(name)) {
if (oldValue === newValue) return;
if (name === 'pop-open') {
this._updateOpenState();
return;
}
parentPopAttrSync2PopEle(name, oldValue, newValue, _els.popover);
if (newValue === null) {
_els.timeSelectorOnly.removeAttribute(name);
_els.dateSelector.removeAttribute(name);
return;
}
_els.timeSelectorOnly.setAttribute(name, newValue);
_els.dateSelector.setAttribute(name, newValue);
return;
}
this._render();
if (
name === 'current-time' &&
this._granType !== GranType.CalendarTime
) {
this.dispatchEvent('select-time', this.currentTime as Date);
}
}
private _render = super._genRenderFn(() => {
this._updateOpenState();
const currentTime = this.currentTime as Date;
const { _els, _granType } = this;
_els.hostWrapper.dataset.type = _granType;
const { min: minGran, max: maxGran } = this._minmaxGran;
const gen = <T = DateTimeGranularity>() => ({
minGranularity: minGran as T,
maxGranularity: maxGran as T,
currentTime
});
if (_granType !== GranType.Time) {
Object.assign(_els.echoInPopover, gen());
Object.assign(_els.echoInDate, gen());
}
if (
_granType === GranType.CalendarTime ||
_granType === GranType.DateTime
) {
Object.assign(_els.timeSelectorInCalendar, {
minGranularity: minGran as TimeGranularity,
currentTime
});
}
if (
_granType === GranType.Calendar ||
_granType === GranType.CalendarTime
) {
_els.nav.millisecond = +currentTime;
const { min, max } = this._getMaxMinTime();
Object.assign(_els.calendar, {
weekStartAt: this.weekStartAt,
timeStart: +currentTime,
timeEnd: +currentTime,
showingTime: this.showingTime,
minTime: min,
maxTime: max
});
} else if (_granType === GranType.Time) {
Object.assign(_els.timeSelectorOnly, gen<TimeGranularity>());
} else if (_granType === GranType.Date) {
Object.assign(_els.dateSelector, gen<DateGranularity>());
} else if (_granType === GranType.DateTime) {
Object.assign(_els.dateSelector, {
maxGranularity: maxGran as DateGranularity,
minGranularity: 'day',
currentTime
});
Object.assign(_els.timeSelectorInDate, {
maxGranularity: 'hour',
minGranularity: minGran as TimeGranularity,
currentTime
});
}
this._updateSlot();
});
private _onCalendarSelect = (e: CalendarBaseEvent['select-time']) => {
e.stopPropagation();
this.currentTime =
+e.detail +
(this._minmaxGran.min === 'day'
? 0
: this._els.timeSelectorInCalendar.millisecond);
};
private _onNavChange = (e: YyyyMmNavEvent['change']) => {
e.stopPropagation();
if (!closestByEvent(e, '.wrapper')) return;
this._els.calendar.showingTime = +e.detail.newTime;
};
private _onNavOpenToggle = (e: YyyyMmNavEvent['popover-open-change']) => {
if (!(e.target instanceof YyyyMmNavEle)) return;
e.stopPropagation();
e.target.nextElementSibling?.classList.toggle('hide', e.detail);
};
private _onTimeSelectorChange = (
e: HhMmSsMsSelectorEvent['select-time']
) => {
this.currentTime = e.detail;
};
private _onDateSelectorSelect = (e: YyyyMmDdSelectorEvt['select-time']) => {
e.stopPropagation();
const time = new Date(e.detail);
time.setHours(0, 0, 0, 0);
time.setMilliseconds(this._els.timeSelectorInDate.millisecond);
this.currentTime = +time;
};
private _onConfirmBtnClick = () => {
this.dispatchEvent('select-time', this.currentTime as Date);
this.open = false;
};
/** 时分秒毫秒回显格式化函数。设置为 `null` 则重置为默认值 */
public get timeFormatter(): TimeFormatterFn {
return this._els.timeSelectorInCalendar.timeFormatter;
}
public set timeFormatter(fn: TimeFormatterFn | null) {
const { _els } = this;
_els.timeSelectorInCalendar.timeFormatter =
_els.timeSelectorOnly.timeFormatter =
_els.timeSelectorInDate.timeFormatter =
_els.echoInDate.timeFormatter =
_els.echoInPopover.timeFormatter =
fn;
}
/** 年月日回显格式化函数。设置为 `null` 则重置为默认值 */
public get dateFormatter(): DateFormatterFn {
return this._els.dateSelector.dateFormatter;
}
public set dateFormatter(fn: DateFormatterFn | null) {
const { _els } = this;
_els.echoInDate.dateFormatter = _els.echoInPopover.dateFormatter = fn;
}
/** 日期时间回显格式化函数。设置为 `null` 则重置为默认值 */
public get dateTimeFormatter(): DatetimeFormatterFn {
return this._els.echoInDate.dateTimeFormatter;
}
public set dateTimeFormatter(fn: DatetimeFormatterFn | null) {
const { _els } = this;
_els.echoInDate.dateTimeFormatter =
_els.echoInPopover.dateTimeFormatter = fn;
}
}
Ele.define();