角度材料中的多选日期范围

问题描述 投票:0回答:1

Angular 中多日期范围选择器的实现,类似于 react-multi-date

angular angular-material datepicker
1个回答
0
投票

组件

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;
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.