如何在 Angular 中实现带有可拖动项的多行时间轴
How to implement a multi-row timeline with dragable items in Angular
我想创建一个类似于此的时间轴网格:https://demo.mobiscroll.com/angular/timeline/month-view#drag-drop=true&themeVariant=light
目标是在 x 轴上有一个时间轴,在 y 轴上有一个可扩展的行数(基本上是一个常规的日历周视图,但天数不固定)。我希望能够沿 x 轴(精确到分钟)在细粒度级别上拖放项目,以及将它们拖放到新行。如何在 Angular 中实现?
我尝试使用 Angular Material's Drag and Drop feature 提出一个解决方案,但无法真正弄清楚我将如何表示细粒度的 x 轴,而不创建一个包含代表每一分钟的项目的长列表当天。这是一种可行的技术,还是有更简单的方法?
我一直在创建 similar component lately, but I haven't implemented the timeline mode yet. A demo can be found here。
我做的是
将资源组保持在 BehaviorSubject
resources$ = new BehaviorSubject<(Resource | ResourceGroup)[]>([]);
接口:
export interface ResourceGroup {
description: string;
children: (ResourceGroup | Resource)[];
}
export interface Resource {
description: string;
events: SchedulerEvent[];
}
export interface SchedulerEvent {
start: Date;
end: Date;
color: string;
description: string;
}
事件是资源->事件组的映射:
this.events$ = this.resources$
.pipe(map((resourcesOrGroups) => resourcesOrGroups.map(resOrGroup => this.getResourcesForGroup(resOrGroup))))
.pipe(map(jaggedResources => jaggedResources.reduce((flat, toFlatten) => flat.concat(toFlatten), [])))
.pipe(map(resources => resources.map(res => res.events)))
.pipe(map(jaggedEvents => jaggedEvents.reduce((flat, toFlatten) => flat.concat(toFlatten), [])));
然后你需要将这些事件分成几部分(对于日历模式,时间线模式将按 week/month 每天拆分):
this.eventParts$ = this.events$.pipe(
map((events) => events.map((ev) => this.timelineService.splitInParts(ev)))
);
这是每天将事件分成几个部分的代码:
export class BsTimelineService {
public splitInParts(event: SchedulerEvent | PreviewEvent) {
let startTime = event.start;
const result: SchedulerEventPart[] = [];
const eventOrNull = 'color' in event ? event : null;
while (!this.dateEquals(startTime, event.end)) {
const end = new Date(startTime.getFullYear(), startTime.getMonth(), startTime.getDate() + 1, 0, 0, 0);
result.push({ start: startTime, end: end, event: eventOrNull });
startTime = end;
}
if (startTime != event.end) {
result.push({ start: startTime, end: event.end, event: eventOrNull });
}
return <SchedulerEventWithParts>{ event: event, parts: result };
}
private dateEquals(date1: Date, date2: Date) {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
);
}
}
接口:
export interface SchedulerEventWithParts {
event: SchedulerEvent;
parts: SchedulerEventPart[];
}
最后您可以只筛选出本周的活动部分:
this.eventPartsForThisWeek$ = combineLatest([
this.daysOfWeekWithTimestamps$,
this.eventParts$
.pipe(map(eventParts => eventParts.map(evp => evp.parts)))
.pipe(map(jaggedParts => jaggedParts.reduce((flat, toFlatten) => flat.concat(toFlatten), [])))
])
.pipe(map(([startAndEnd, eventParts]) => {
return eventParts.filter(eventPart => {
return !((eventPart.end.getTime() <= startAndEnd.start) || (eventPart.start.getTime() >= startAndEnd.end));
});
}));
您可以使用 async
管道在您的视图中呈现:
<div *ngFor="let eventPart of (eventPartsForThisWeek$ | async)"></div>
在您的情况下,您对此没有问题,但是当您需要日历模式(就像我开始使用的那个)时,您仍然需要在多列中显示这些事件。因此我创建了另一个可观察对象:
this.timelinedEventPartsForThisWeek$ = this.eventPartsForThisWeek$
.pipe(map(eventParts => {
// We'll only use the events for this week
const events = eventParts.map(ep => ep.event)
.filter((e, i, list) => list.indexOf(e) === i)
.filter((e) => !!e)
.map((e) => <SchedulerEvent>e);
const timeline = this.timelineService.getTimeline(events);
const result = timeline.map(track => track.events.map(ev => ({ event: ev, index: track.index })))
.reduce((flat, toFlatten) => flat.concat(toFlatten), [])
.map((evi) => eventParts.filter(p => p.event === evi.event).map(p => ({ part: p, index: evi.index })))
.reduce((flat, toFlatten) => flat.concat(toFlatten), []);
return {
total: timeline.length,
parts: result
};
}));
以下方法正在为给定事件创建一个时间轴:
public getTimeline(events: SchedulerEvent[]) {
const timestamps = this.getTimestamps(events);
const tracks: TimelineTrack[] = [];
timestamps.forEach((timestamp, tIndex) => {
const starting = events.filter((e) => e.start === timestamp);
// const ending = events.filter((e) => e.end === timestamp);
starting.forEach((startedEvent, eIndex) => {
const freeTracks = tracks.filter(t => this.trackIsFreeAt(t, startedEvent));
if (freeTracks.length === 0) {
tracks.push({ index: tracks.length, events: [startedEvent] });
} else {
freeTracks[0].events.push(startedEvent);
}
});
});
return tracks;
}
private getTimestamps(events: SchedulerEvent[]) {
const allTimestamps = events.map(e => [e.start, e.end])
.reduce((flat, toFlatten) => flat.concat(toFlatten), []);
return allTimestamps
.filter((t, i) => allTimestamps.indexOf(t) === i)
.sort((t1, t2) => <any>t1 - <any>t2);
}
private trackIsFreeAt(track: TimelineTrack, event: SchedulerEvent) {
if (track.events.every((ev) => (ev.end <= event.start) || (event.end <= ev.start))) {
return true;
} else {
return false;
}
}
请注意,对于由 dragging-dropping 创建的事件或移动的事件,我必须省略 TimelineService
。这会导致通过 TimelineService
进行过多的计算,并且网络浏览器无法处理。
我想创建一个类似于此的时间轴网格:https://demo.mobiscroll.com/angular/timeline/month-view#drag-drop=true&themeVariant=light
目标是在 x 轴上有一个时间轴,在 y 轴上有一个可扩展的行数(基本上是一个常规的日历周视图,但天数不固定)。我希望能够沿 x 轴(精确到分钟)在细粒度级别上拖放项目,以及将它们拖放到新行。如何在 Angular 中实现?
我尝试使用 Angular Material's Drag and Drop feature 提出一个解决方案,但无法真正弄清楚我将如何表示细粒度的 x 轴,而不创建一个包含代表每一分钟的项目的长列表当天。这是一种可行的技术,还是有更简单的方法?
我一直在创建 similar component lately, but I haven't implemented the timeline mode yet. A demo can be found here。
我做的是
将资源组保持在 BehaviorSubject
resources$ = new BehaviorSubject<(Resource | ResourceGroup)[]>([]);
接口:
export interface ResourceGroup {
description: string;
children: (ResourceGroup | Resource)[];
}
export interface Resource {
description: string;
events: SchedulerEvent[];
}
export interface SchedulerEvent {
start: Date;
end: Date;
color: string;
description: string;
}
事件是资源->事件组的映射:
this.events$ = this.resources$
.pipe(map((resourcesOrGroups) => resourcesOrGroups.map(resOrGroup => this.getResourcesForGroup(resOrGroup))))
.pipe(map(jaggedResources => jaggedResources.reduce((flat, toFlatten) => flat.concat(toFlatten), [])))
.pipe(map(resources => resources.map(res => res.events)))
.pipe(map(jaggedEvents => jaggedEvents.reduce((flat, toFlatten) => flat.concat(toFlatten), [])));
然后你需要将这些事件分成几部分(对于日历模式,时间线模式将按 week/month 每天拆分):
this.eventParts$ = this.events$.pipe(
map((events) => events.map((ev) => this.timelineService.splitInParts(ev)))
);
这是每天将事件分成几个部分的代码:
export class BsTimelineService {
public splitInParts(event: SchedulerEvent | PreviewEvent) {
let startTime = event.start;
const result: SchedulerEventPart[] = [];
const eventOrNull = 'color' in event ? event : null;
while (!this.dateEquals(startTime, event.end)) {
const end = new Date(startTime.getFullYear(), startTime.getMonth(), startTime.getDate() + 1, 0, 0, 0);
result.push({ start: startTime, end: end, event: eventOrNull });
startTime = end;
}
if (startTime != event.end) {
result.push({ start: startTime, end: event.end, event: eventOrNull });
}
return <SchedulerEventWithParts>{ event: event, parts: result };
}
private dateEquals(date1: Date, date2: Date) {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
);
}
}
接口:
export interface SchedulerEventWithParts {
event: SchedulerEvent;
parts: SchedulerEventPart[];
}
最后您可以只筛选出本周的活动部分:
this.eventPartsForThisWeek$ = combineLatest([
this.daysOfWeekWithTimestamps$,
this.eventParts$
.pipe(map(eventParts => eventParts.map(evp => evp.parts)))
.pipe(map(jaggedParts => jaggedParts.reduce((flat, toFlatten) => flat.concat(toFlatten), [])))
])
.pipe(map(([startAndEnd, eventParts]) => {
return eventParts.filter(eventPart => {
return !((eventPart.end.getTime() <= startAndEnd.start) || (eventPart.start.getTime() >= startAndEnd.end));
});
}));
您可以使用 async
管道在您的视图中呈现:
<div *ngFor="let eventPart of (eventPartsForThisWeek$ | async)"></div>
在您的情况下,您对此没有问题,但是当您需要日历模式(就像我开始使用的那个)时,您仍然需要在多列中显示这些事件。因此我创建了另一个可观察对象:
this.timelinedEventPartsForThisWeek$ = this.eventPartsForThisWeek$
.pipe(map(eventParts => {
// We'll only use the events for this week
const events = eventParts.map(ep => ep.event)
.filter((e, i, list) => list.indexOf(e) === i)
.filter((e) => !!e)
.map((e) => <SchedulerEvent>e);
const timeline = this.timelineService.getTimeline(events);
const result = timeline.map(track => track.events.map(ev => ({ event: ev, index: track.index })))
.reduce((flat, toFlatten) => flat.concat(toFlatten), [])
.map((evi) => eventParts.filter(p => p.event === evi.event).map(p => ({ part: p, index: evi.index })))
.reduce((flat, toFlatten) => flat.concat(toFlatten), []);
return {
total: timeline.length,
parts: result
};
}));
以下方法正在为给定事件创建一个时间轴:
public getTimeline(events: SchedulerEvent[]) {
const timestamps = this.getTimestamps(events);
const tracks: TimelineTrack[] = [];
timestamps.forEach((timestamp, tIndex) => {
const starting = events.filter((e) => e.start === timestamp);
// const ending = events.filter((e) => e.end === timestamp);
starting.forEach((startedEvent, eIndex) => {
const freeTracks = tracks.filter(t => this.trackIsFreeAt(t, startedEvent));
if (freeTracks.length === 0) {
tracks.push({ index: tracks.length, events: [startedEvent] });
} else {
freeTracks[0].events.push(startedEvent);
}
});
});
return tracks;
}
private getTimestamps(events: SchedulerEvent[]) {
const allTimestamps = events.map(e => [e.start, e.end])
.reduce((flat, toFlatten) => flat.concat(toFlatten), []);
return allTimestamps
.filter((t, i) => allTimestamps.indexOf(t) === i)
.sort((t1, t2) => <any>t1 - <any>t2);
}
private trackIsFreeAt(track: TimelineTrack, event: SchedulerEvent) {
if (track.events.every((ev) => (ev.end <= event.start) || (event.end <= ev.start))) {
return true;
} else {
return false;
}
}
请注意,对于由 dragging-dropping 创建的事件或移动的事件,我必须省略 TimelineService
。这会导致通过 TimelineService
进行过多的计算,并且网络浏览器无法处理。