v-calendar
Version:
A calendar and date picker plugin for Vue.js.
271 lines (239 loc) • 6.99 kB
text/typescript
import { type DateRepeatConfig, DateRepeat } from './repeat';
import { type DateParts, type DayParts, addDays, MS_PER_DAY } from './helpers';
import { isDate, isArray, isObject } from '../helpers';
import type { CalendarDay } from '../page';
import Locale from '../locale';
type DateRangeDate = Date | string | number | null;
interface DateRangeConfig {
start: DateRangeDate;
end: DateRangeDate;
span: number;
order: number;
repeat: Partial<DateRepeatConfig>;
}
export type DateRangeSource =
| DateRange
| DateRangeDate
| [DateRangeDate, DateRangeDate]
| Partial<DateRangeConfig>;
export interface SimpleDateRange {
start: Date;
end: Date;
}
export class DateRange {
order: number;
locale: Locale;
start: DateParts | null = null;
end: DateParts | null = null;
repeat: DateRepeat | null = null;
static fromMany(ranges: DateRangeSource | DateRangeSource[], locale: Locale) {
// Assign dates
return (isArray(ranges) ? ranges : [ranges])
.filter(d => d)
.map(d => DateRange.from(d, locale));
}
static from(source: DateRangeSource, locale: Locale) {
if (source instanceof DateRange) return source;
const config: Partial<DateRangeConfig> = {
start: null,
end: null,
};
if (source != null) {
if (isArray(source)) {
config.start = source[0] ?? null;
config.end = source[1] ?? null;
} else if (isObject(source)) {
Object.assign(config, source);
} else {
config.start = source;
config.end = source;
}
}
if (config.start != null) config.start = new Date(config.start);
if (config.end != null) config.end = new Date(config.end);
return new DateRange(config, locale);
}
private constructor(config: Partial<DateRangeConfig>, locale = new Locale()) {
this.locale = locale;
const { start, end, span, order, repeat } = config;
if (isDate(start)) {
this.start = locale.getDateParts(start);
}
if (isDate(end)) {
this.end = locale.getDateParts(end);
} else if (this.start != null && span) {
this.end = locale.getDateParts(addDays(this.start.date, span - 1));
}
this.order = order ?? 0;
if (repeat) {
this.repeat = new DateRepeat(
{
from: this.start?.date,
...repeat,
},
{
locale: this.locale,
},
);
}
}
get opts() {
const { order, locale } = this;
return { order, locale };
}
get hasRepeat() {
return !!this.repeat;
}
get isSingleDay() {
const { start, end } = this;
return (
start &&
end &&
start.year === end.year &&
start.month === end.month &&
start.day === end.day
);
}
get isMultiDay() {
return !this.isSingleDay;
}
get daySpan() {
if (this.start == null || this.end == null) {
if (this.hasRepeat) return 1;
return Infinity;
}
return this.end.dayIndex - this.start.dayIndex;
}
startsOnDay(dayParts: DayParts) {
return (
this.start?.dayIndex === dayParts.dayIndex ||
!!this.repeat?.passes(dayParts)
);
}
intersectsDay(dayIndex: number) {
return this.intersectsDayRange(dayIndex, dayIndex);
}
intersectsRange(range: DateRange) {
return this.intersectsDayRange(
range.start?.dayIndex ?? -Infinity,
range.end?.dayIndex ?? Infinity,
);
}
intersectsDayRange(startDayIndex: number, endDayIndex: number) {
if (this.start && this.start.dayIndex > endDayIndex) return false;
if (this.end && this.end.dayIndex < startDayIndex) return false;
return true;
}
}
interface DataRange {
startDay: number;
startTime: number;
endDay: number;
endTime: number;
}
export interface RangeData {
key: string | number;
order?: number;
}
interface DataRanges {
ranges: DataRange[];
data: RangeData;
}
export interface DateRangeCell<T extends RangeData> extends DataRange {
data: T;
onStart: boolean;
onEnd: boolean;
startTime: number;
startDate: Date;
endTime: number;
endDate: Date;
allDay: boolean;
order: number;
}
export class DateRangeContext {
private records: Record<string, DataRanges> = {};
render(data: RangeData, range: DateRange, days: DayParts[]) {
let result = null;
const startDayIndex = days[0].dayIndex;
const endDayIndex = days[days.length - 1].dayIndex;
if (range.hasRepeat) {
days.forEach(day => {
if (range.startsOnDay(day)) {
const span = range.daySpan < Infinity ? range.daySpan : 1;
result = {
startDay: day.dayIndex,
startTime: range.start?.time ?? 0,
endDay: day.dayIndex + span - 1,
endTime: range.end?.time ?? MS_PER_DAY,
};
this.getRangeRecords(data).push(result);
}
});
} else if (range.intersectsDayRange(startDayIndex, endDayIndex)) {
result = {
startDay: range.start?.dayIndex ?? -Infinity,
startTime: range.start?.time ?? -Infinity,
endDay: range.end?.dayIndex ?? Infinity,
endTime: range.end?.time ?? Infinity,
};
this.getRangeRecords(data).push(result);
}
return result;
}
private getRangeRecords(data: RangeData) {
let record = this.records[data.key];
if (!record) {
record = {
ranges: [],
data,
};
this.records[data.key] = record;
}
return record.ranges;
}
getCell(key: string | number, day: CalendarDay) {
const cells = this.getCells(day);
const result = cells.find(cell => cell.data.key === key);
return result;
}
cellExists(key: string | number, dayIndex: number) {
const records = this.records[key];
if (records == null) return false;
return records.ranges.some(
r => r.startDay <= dayIndex && r.endDay >= dayIndex,
);
}
getCells(day: CalendarDay) {
const records = Object.values(this.records);
const result: DateRangeCell<any>[] = [];
const { dayIndex } = day;
records.forEach(({ data, ranges }) => {
ranges
.filter(r => r.startDay <= dayIndex && r.endDay >= dayIndex)
.forEach(range => {
const onStart = dayIndex === range.startDay;
const onEnd = dayIndex === range.endDay;
const startTime = onStart ? range.startTime : 0;
const startDate = new Date(day.startDate.getTime() + startTime);
const endTime = onEnd ? range.endTime : MS_PER_DAY;
const endDate = new Date(day.endDate.getTime() + endTime);
const allDay = startTime === 0 && endTime === MS_PER_DAY;
const order = data.order || 0;
result.push({
...range,
data,
onStart,
onEnd,
startTime,
startDate,
endTime,
endDate,
allDay,
order,
});
});
});
result.sort((a, b) => a.order - b.order);
return result;
}
}