Angular 中多日期范围选择器的实现,类似于 react-multi-date
组件
import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
import { DateRange, MatCalendar } from "@angular/material/datepicker";
type RangeDateType = string | Date | null;
export type DateRangeType = {
start: RangeDateType;
end: RangeDateType;
};
@Component({
selector: "sn-multi-daterange-picker",
templateUrl: "./multi-daterange-picker.component.html",
styleUrls: ["./multi-daterange-picker.component.scss"],
})
export class MultiDaterangePickerComponent {
@ViewChild(MatCalendar) calendar: MatCalendar<Date>;
selectedRangeValue: DateRange<Date> | null;
@Output() selectedRanges = new EventEmitter<Array<DateRangeType>>();
@Input() ranges: Array<DateRangeType> = [];
private _disAllowed: Array<DateRangeType> = [];
@Input()
public get disAllowed(): Array<DateRangeType> {
return this._disAllowed;
}
public set disAllowed(value: Array<DateRangeType>) {
this.disallowedMap = {};
this._disAllowed = value;
}
private disallowedMap: { [key: string]: number } = {};
private readonly RANGE_SELECTED = "selected";
private readonly RANGE_DISALLOWED = "disallowed";
private readonly RANGE_DISALLOWED_DEFAULT = "disallowed-default";
private readonly RANGE_INCLUDE = `mat-calendar-body-in-range`;
private readonly RANGE_START = `${this.RANGE_SELECTED} ${this.RANGE_INCLUDE} mat-calendar-body-range-start`;
private readonly RANGE_END = `${this.RANGE_SELECTED} ${this.RANGE_INCLUDE} mat-calendar-body-range-end`;
selectedChange(selectedDate: any) {
if (!this.selectedRangeValue?.start || this.selectedRangeValue?.end) {
this.selectedRangeValue = new DateRange<Date>(selectedDate, null);
} else {
const start = this.selectedRangeValue.start;
const end = selectedDate;
if (end < start) {
this.selectedRangeValue = new DateRange<Date>(end, start);
} else {
this.selectedRangeValue = new DateRange<Date>(start, end);
}
}
this.disallowedMap = {};
this.processSelectedRange();
this.calendar.updateTodaysDate();
}
public dateClass = (date: Date | null) => {
const expandedRanges: Array<Date[]> = this.getExpandedRanges();
const isDateDisallowed = (className: string) => {
if (date) {
const result = this.isDisallowed(date);
if (result) {
const dateTime = date.getTime();
this.disallowedMap[dateTime] = dateTime;
}
return result ? `${className} ${this.RANGE_DISALLOWED}` : className;
}
return className;
};
for (let exRange = 0; exRange < expandedRanges.length; exRange++) {
const expandedRange = expandedRanges[exRange];
if (expandedRange.length) {
const expandedRangeTimes = expandedRange.map((x) => x.getTime());
const dateTime = date?.getTime();
if (dateTime) {
if (expandedRangeTimes[0] === dateTime) {
if (expandedRangeTimes.length === 1)
return [isDateDisallowed(this.RANGE_SELECTED)];
else return [isDateDisallowed(this.RANGE_START)];
} else if (expandedRangeTimes[expandedRangeTimes.length - 1] === dateTime)
return [isDateDisallowed(this.RANGE_END)];
else if (expandedRangeTimes.includes(dateTime))
return [isDateDisallowed(this.RANGE_INCLUDE)];
}
}
}
const result = this.isDisallowed(date);
if (result) return [this.RANGE_DISALLOWED_DEFAULT];
return [];
};
public getSelectedDisallowed() {
return Object.keys(this.disAllowed).map((x) => new Date(x));
}
public remove(index: number, _data: DateRangeType) {
this.ranges = this.ranges.filter((_,i) => i !== index);
this.selectedRangeValue = null;
this.calendar.updateTodaysDate();
}
private processSelectedRange() {
if (this.selectedRangeValue?.start && this.selectedRangeValue?.end) {
if (this.ranges.length) {
const newRanges: (DateRangeType | null)[] = this.ranges.map((x) => ({ ...x }));
const expandedRanges = this.getExpandedRanges();
const expandedSelectedDatesRange = this.getExpandedRanges([
this.selectedRangeValue,
]);
const expandedSelectedDatesRangeTime = expandedSelectedDatesRange
.flatMap((x) => x)
.map((x) => x.getTime());
for (let rangeIndex = 0; rangeIndex < expandedRanges.length; rangeIndex++) {
const expandedRange = expandedRanges[rangeIndex];
if (
expandedRange.length &&
this.selectedRangeValue?.start &&
this.selectedRangeValue.end
) {
const savedRangeTimes = expandedRange.map((x) => x.getTime());
if (
savedRangeTimes.some((x) => expandedSelectedDatesRangeTime.includes(x))
) {
newRanges[rangeIndex] = null;
}
}
}
this.ranges = (newRanges as any).filter((x: any) => x);
this.ranges.push({ ...this.selectedRangeValue });
} else this.ranges.push({ ...this.selectedRangeValue });
this.selectedRanges.emit(this.ranges);
}
}
private isDisallowed(date: Date | null) {
if (this.disAllowed?.length && date) {
const expandedNotAllowedDatesRange = this.getExpandedRanges(this.disAllowed);
const expandedNotAllowedDatesRangeTime = expandedNotAllowedDatesRange
.flatMap((x) => x)
.map((x) => x.getTime());
return expandedNotAllowedDatesRangeTime.includes(date.getTime());
}
return false;
}
private getExpandedRanges(ranges = this.ranges) {
const expandedRanges: Array<Date[]> = [];
for (const range in ranges) {
if (Object.prototype.hasOwnProperty.call(ranges, range)) {
const rangeObj = ranges[range];
const expandedRange = this.getDateRange(rangeObj.start, rangeObj.end);
expandedRanges.push(expandedRange);
}
}
return expandedRanges;
}
private getDateRange(startDate: RangeDateType, endDate: RangeDateType) {
const dateArray = [];
if (startDate && endDate) {
let currentDate = new Date(startDate);
while (currentDate <= new Date(endDate)) {
dateArray.push(new Date(currentDate));
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
}
}
return dateArray;
}
}
HTML
<div class="multidaterange">
<small *ngIf="ranges.length" class="multidaterange__title">Selected Dates</small>
<mat-chip-list #chipList>
<mat-chip *ngFor="let range of ranges; let i = index" [selectable]="false" [removable]="true"
(removed)="remove(i, range)">
{{ range | rangeformat }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
<mat-calendar [dateClass]="dateClass" [selected]="selectedRangeValue" (selectedChange)="selectedChange($event)">
</mat-calendar>
</div>
CSS
::ng-deep mat-form-field {
width: 100%;
}
::ng-deep .mat-calendar-body-cell.selected>.mat-calendar-body-cell-content,
::ng-deep .mat-calendar-body-cell.selected:hover>.mat-calendar-body-cell-content,
::ng-deep .mat-calendar-body-cell.selected>.mat-calendar-body-cell-content:hover {
background-color: #745C00;
color: #fff;
}
::ng-deep .mat-calendar-body-cell.disallowed-default>.mat-calendar-body-cell-content,
::ng-deep .mat-calendar-body-cell.disallowed-default:hover>.mat-calendar-body-cell-content,
::ng-deep .mat-calendar-body-cell.disallowed-default>.mat-calendar-body-cell-content:hover {
opacity: .5;
}
::ng-deep .mat-calendar-body-cell.disallowed>.mat-calendar-body-cell-content,
::ng-deep .mat-calendar-body-cell.disallowed:hover>.mat-calendar-body-cell-content,
::ng-deep .mat-calendar-body-cell.disallowed>.mat-calendar-body-cell-content:hover {
background-color: #740000 !important;
color: #fff !important;
}
.multidaterange {
margin-top: 10px;
&__title {
color: #735C00;
}
}
范围格式管道
import { Pipe, PipeTransform } from "@angular/core";
import { DateRangeType } from "./multi-daterange-picker.component";
@Pipe({
name: "rangeformat",
})
export class RangeformatPipe implements PipeTransform {
transform(value: DateRangeType, _args?: any): any {
if (value && value.start && value.end) {
const { start, end } = value;
const startDate = new Date(start);
const endDate = new Date(end);
if (startDate.getTime() === endDate.getTime())
return startDate.toLocaleDateString();
return `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`;
}
return value;
}
}