UNPKG

@gez/date-time-kit

Version:

306 lines (282 loc) 10.5 kB
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();