@gez/date-time-kit
Version:
306 lines (282 loc) • 10.5 kB
text/typescript
import { closestByEvent } from '../../utils';
import {
type BaseAttrs,
type BaseEmits,
type Emit2EventMap,
UiBase
} from '../web-component-base';
import styleStr from './index.css';
import html from './index.html';
import { type Weeks, getWeekInOrder, weekKey } from './weeks';
export { type Weeks, weekKey, getWeekInOrder } from './weeks';
export interface Attrs extends BaseAttrs {
/**
* 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 Date.now()
*/
'showing-time'?: string | number;
/**
* The start time of the calendar display range.
* @type {`string | number`} A value that can be passed to the Date constructor.
* @default 'showing-time'
*/
'time-start'?: string | number;
/**
* The end time of the calendar display range.
* @type {`string | number`} A value that can be passed to the Date constructor.
* @default 'time-start'
*/
'time-end'?: string | number;
/**
* 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;
/**
* Set which day of the week is the first day.
* @type `'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'`
* @default 'sun'
*/
'week-start-at'?: Weeks;
/**
* Whether to show the days of the previous and next months in the current month's calendar.
* @type {boolean}
* @default false
*/
'show-other-month'?: boolean;
}
export interface Emits extends BaseEmits {
'select-time': Date;
'hover-item': Date;
}
export type EventMap = Emit2EventMap<Emits>;
/**
* 基础的日历显示组件。仅显示星期和数字。
*/
export class Ele extends UiBase<Attrs, Emits> {
public static tagName = 'dt-calendar-base' as const;
protected static _style = styleStr;
protected static _template = html;
static get observedAttributes(): string[] {
return [
...(super.observedAttributes as (keyof BaseAttrs)[]),
'showing-time',
'time-start',
'time-end',
'min-time',
'max-time',
'week-start-at'
] satisfies (keyof Attrs)[];
}
get _staticEls() {
return {
...super._staticEls,
weeks: this.$`.week`,
items: this.$`.item`
} as const;
}
public get showingTime() {
const v = this._getAttr('showing-time', '' + Date.now());
return new Date(Number.isNaN(+v) ? v : +v);
}
public get timeStart() {
const v = this._getAttr('time-start', '' + this.showingTime);
return new Date(Number.isNaN(+v) ? v : +v);
}
public get timeEnd() {
const v = this._getAttr('time-end', '' + this.timeStart);
return new Date(Number.isNaN(+v) ? v : +v);
}
public get minTime() {
const v = this._getAttr('min-time', 'null');
return new Date(Number.isNaN(+v) ? v : +v);
}
public get maxTime() {
const v = this._getAttr('max-time', 'null');
return new Date(Number.isNaN(+v) ? v : +v);
}
private _setTimeAttr(
name: keyof Omit<
Attrs,
'week-start-at' | 'show-other-month' | keyof BaseAttrs
>,
value: number | string | Date
) {
const v = new Date(value);
if (Number.isNaN(+v)) return;
this.setAttribute(name, +v + '');
}
public set showingTime(val: number | string | Date) {
this._setTimeAttr('showing-time', val);
}
public set timeStart(val: number | string | Date) {
this._setTimeAttr('time-start', val);
}
public set timeEnd(val: number | string | Date) {
this._setTimeAttr('time-end', val);
}
public set minTime(val: number | string | Date) {
this._setTimeAttr('min-time', val);
}
public set maxTime(val: number | string | Date) {
this._setTimeAttr('max-time', val);
}
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 showOtherMonth() {
return this.hasAttribute('show-other-month');
}
public set showOtherMonth(val: boolean) {
this.setAttribute('show-other-month', '' + val);
}
public connectedCallback() {
if (!super.connectedCallback()) return;
this._onWeekStartAtChange();
this._onTimeChange();
this._bindEvt(this)('click', this._onClick);
this._bindEvt`.wrapper`('pointerover', this._onPointerOver);
}
protected _onAttrChanged(
name: string,
oldValue: string | null,
newValue: string | null
) {
super._onAttrChanged(name, oldValue, newValue);
if (name === 'week-start-at') {
this._onWeekStartAtChange();
}
if (
[
'showing-time',
'time-start',
'time-end',
'min-time',
'max-time'
].includes(name)
) {
this._onTimeChange();
}
}
private _onWeekStartAtChange = super._genRenderFn(() => {
const weekOrder = getWeekInOrder(this.weekStartAt);
this._els.weeks.forEach((ele, i) => {
ele.setAttribute('i18n-key', `date.${weekOrder[i]}`!);
});
this._onTimeChange();
});
private _onTimeChange = super._genRenderFn(() => {
const currentTime = this.showingTime as Date;
let timeStart = this.timeStart as Date;
let timeEnd = this.timeEnd as Date;
currentTime.setHours(0, 0, 0, 0);
timeStart.setHours(0, 0, 0, 0);
timeEnd.setHours(0, 0, 0, 0);
if (
Number.isNaN(+currentTime) ||
Number.isNaN(+timeStart) ||
Number.isNaN(+timeEnd)
) {
console.warn(`Invalid date attribute(s) on <${this.tagName}>`);
return;
}
if (timeStart > timeEnd) {
[timeStart, timeEnd] = [timeEnd, timeStart];
}
const minTime = this.minTime as Date;
const maxTime = this.maxTime as Date;
minTime.setHours(0, 0, 0, 0);
maxTime.setHours(0, 0, 0, 0);
if (maxTime < timeEnd) timeEnd = maxTime;
if (timeStart < minTime) timeStart = minTime;
const weekStartAt = this.weekStartAt;
const year = currentTime.getFullYear();
const month = currentTime.getMonth();
// number of day for current month
const days = new Date(year, month + 1, 0).getDate();
// number of day for previous month
const daysPrev = new Date(year, month, 0).getDate();
// first day of the week for current month (0=Sunday, 1=Monday, ..., 6=Saturday)
const firstWeekOfCurMonth = new Date(year, month, 1).getDay();
// Calculate the offset for different week start days
const weekStartOffset = weekKey.indexOf(weekStartAt);
// Adjust the first day of week according to weekStartAt
const adjustedFirstWeek =
(firstWeekOfCurMonth - weekStartOffset + 7) % 7;
let itemIdx = 0;
const items = this._els.items;
const changeItemText = (item: HTMLElement, text: string) => {
item.querySelector('span')!.textContent = text;
};
items.forEach((ele) => {
ele.className = 'item disabled';
ele.removeAttribute('data-time');
ele.setAttribute('part', 'item disabled');
changeItemText(ele, ' ');
});
// set previous month days
for (let i = daysPrev - adjustedFirstWeek + 1; i <= daysPrev; ++i) {
const ele = items[itemIdx++];
ele.classList.add('prev');
ele.part.add('prev');
changeItemText(ele, this.showOtherMonth ? this.formatter(i) : ' ');
}
// set current month days
for (let i = 1; i <= days; ++i) {
const ele = items[itemIdx++];
const time = new Date(year, month, i);
ele.classList.toggle('month-start', i === 1);
ele.classList.toggle('month-end', i === days);
ele.classList.toggle('disabled', time < minTime || time > maxTime);
ele.classList.toggle('start', +time === +timeStart);
ele.classList.toggle(
'in-range',
+time >= +timeStart && +time <= +timeEnd
);
ele.classList.toggle('end', +time === +timeEnd);
ele.setAttribute('part', ele.className);
ele.dataset.time = time.toISOString();
changeItemText(ele, this.formatter(i));
}
const inRangeItem = items.filter((e) =>
e.classList.contains('in-range')
);
if (inRangeItem.length) {
inRangeItem[0].classList.add('range-start');
inRangeItem[0].part.add('range-start');
inRangeItem[inRangeItem.length - 1].classList.add('range-end');
inRangeItem[inRangeItem.length - 1].part.add('range-end');
}
// set next month days
for (let i = 1; itemIdx < items.length; ++i) {
const ele = items[itemIdx++];
ele.classList.add('next');
ele.part.add('next');
changeItemText(ele, this.showOtherMonth ? this.formatter(i) : ' ');
}
});
private _onClick = (e: MouseEvent) => {
const item = closestByEvent(e, '.item[data-time]:not(.disabled)', this);
if (!item) return;
const time = new Date(item.dataset.time!);
super.dispatchEvent('select-time', time, true);
};
private _onPointerOver = (e: Event) => {
const item = closestByEvent(e, '.item[data-time]:not(.disabled)', this);
if (!item) return;
const time = new Date(item.dataset.time!);
super.dispatchEvent('hover-item', time, true);
};
public formatter = (i: number) => (i < 10 ? '0' : '') + i;
}
Ele.define();