Features
Uses Angular Scheduler component from DayPilot Pro for JavaScript library (trial version).
One or more events can be selected, copied to a local "clipboard" using context menu and pasted at a specified location.
The “Cut” action allows moving events to new location using context menu.
Optional "auto copy" feature copies selected events to the clipboard automatically on click.
This tutorial doesn't cover Scheduler component setup in an Angular application. For a step-by-step tutorial, please see the following article:
License
Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript.
Copy & Paste for a Single Scheduler Event
We will implement the copy feature using event context menu. The context menu will have a single item ("Copy") which saves the selected event (available as args.source
in the onClick
event handler) to this.clipboard
array.
config: DayPilot.SchedulerConfig = {
contextMenu: new DayPilot.Menu({
items: [
{
text: "Copy",
onClick: args => {
this.clipboard = [ args.source ];
}
}
]
}),
// ...
};
In order to better visualize the workflow we will display the content of the clipboard array in a special section below the Scheduler:
import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot} from "daypilot-pro-angular";
import {DataService} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `
<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>
<div class="clipboard">
<div class="clipboard-header">Clipboard</div>
<div *ngFor="let item of clipboard" class="clipboard-item">
{{item.text()}}
</div>
<div *ngIf="clipboard.length === 0" class="clipboard-empty">
No items.
</div>
</div>
`,
styles: [`
.options {
margin-top: 10px;
}
.clipboard {
margin-top: 20px;
border: 1px solid #ccc;
width: 200px;
}
.clipboard-header {
background-color: #efefef;
border-bottom: 1px solid #ccc;
padding: 5px;
}
.clipboard-item {
padding: 5px;
}
.clipboard-empty {
padding: 5px;
}
`]
})
export class SchedulerComponent implements AfterViewInit {
// ...
clipboard: DayPilot.Event[] = [];
config: DayPilot.SchedulerConfig = {
// ...
};
// ...
}
The "Paste" action will also require a context menu. This time it's the context menu for time range selection. The time range context menu is activated automatically when you right click a grid cell. The onClick
event handler copies the event properties from the clipboard and creates a new event at the target location using createEvent()
method of the DataService
class.
config: DayPilot.SchedulerConfig = {
// ...
contextMenuSelection: new DayPilot.Menu({
items: [
{
text: "Paste",
onClick: args => {
const e = this.clipboard[0];
if (!e) {
return;
}
const duration = e.duration();
const start = args.source.start;
const end = start.addTime(duration);
const targetResource = args.source.resource;
const params = {
start: start,
end: end,
resource: targetResource,
text: e.text()
};
this.ds.createEvent(params).subscribe(result => this.scheduler.control.events.add(result));
}
}
]
})
};
Complete Scheduler Component Class
scheduler.component.ts
import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `
<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>
<div class="options" *ngIf="false">
<label for="autocopy"><input id="autocopy" type="checkbox" [(ngModel)]="autoCopy"> Copy selected events to clipboard automatically</label>
</div>
<div class="clipboard">
<div class="clipboard-header">Clipboard</div>
<div *ngFor="let item of clipboard" class="clipboard-item">
{{item.text()}}
</div>
<div *ngIf="clipboard.length === 0" class="clipboard-empty">
No items.
</div>
</div>
`,
styles: [`
.options {
margin-top: 10px;
}
.clipboard {
margin-top: 20px;
border: 1px solid #ccc;
width: 200px;
}
.clipboard-header {
background-color: #efefef;
border-bottom: 1px solid #ccc;
padding: 5px;
}
.clipboard-item {
padding: 5px;
}
.clipboard-empty {
padding: 5px;
}
`]
})
export class SchedulerComponent implements AfterViewInit {
@ViewChild("scheduler")
scheduler!: DayPilotSchedulerComponent;
clipboard: DayPilot.Event[] = [];
autoCopy: boolean;
config: DayPilot.SchedulerConfig = {
timeHeaders : [
{groupBy: "Month", format: "MMMM yyyy"},
{groupBy: "Day", format: "d"}
],
scale: "Day",
days: 31,
startDate: "2023-01-01",
contextMenu: new DayPilot.Menu({
items: [
{
text: "Copy",
onClick: args => {
this.clipboard = [ args.source ];
}
}
]
}),
contextMenuSelection: new DayPilot.Menu({
items: [
{
text: "Paste",
onClick: args => {
const e = this.clipboard[0];
if (!e) {
return;
}
const duration = e.duration();
const start = args.source.start;
const end = start.addTime(duration);
const targetResource = args.source.resource;
const params = {
start: start,
end: end,
resource: targetResource,
text: e.text()
};
this.ds.createEvent(params).subscribe(result => this.scheduler.control.events.add(result));
}
}
]
})
};
constructor(private ds: DataService, private cdr: ChangeDetectorRef) {}
ngAfterViewInit(): void {
this.ds.getResources().subscribe(result => this.config.resources = result);
const from = this.scheduler.control.visibleStart();
const to = this.scheduler.control.visibleEnd();
this.ds.getEvents(from, to).subscribe(result => {
this.scheduler.control.update({events: result});
});
}
}
Copying Multiple Scheduler Events
As the first step, we will enable selecting multiple events using Ctrl+click:
config: DayPilot.SchedulerConfig = {
// ...
allowMultiSelect: true,
eventClickHandling: "Select",
// ...
};
Selecting an arbitrary collection of events would make calculating the target location when pasting more complicated so we will limit the selection to events from the same row for now. The onEventSelect event handler is fired after click but before the actual change to the selection is made. We can use args.preventDefault()
to cancel the selection if it doesn't meet our rules:
config: DayPilot.SchedulerConfig = {
// ..
onEventSelect: args => {
const selected = this.scheduler.control.multiselect.events();
const onlyThis = !args.selected && !args.ctrl && !args.meta;
if (selected.length > 0 && selected[0].resource() !== args.e.resource() && !onlyThis) {
this.scheduler.control.message("You can only select events from the same row.");
args.preventDefault();
}
},
// ...
};
Now we can modify the event context menu to allow copying multiple events. We will also sort the selected events by start date before saving them in the clipboard. Note that the selection is updated before displaying the context menu using onShow event - if you click an event that isn't selected it will clear the selection and select the current event.
config: DayPilot.SchedulerConfig = {
// ...
contextMenu: new DayPilot.Menu({
onShow: args => {
if (!this.scheduler.control.multiselect.isSelected(args.source)) {
this.scheduler.control.multiselect.clear();
this.scheduler.control.multiselect.add(args.source);
}
},
items: [
{
text: "Copy",
onClick: args => {
const selected = this.scheduler.control.multiselect.events();
this.clipboard = selected.sort((e1, e2) => e1.start().getTime() - e2.start().getTime());
}
}
]
}),
// ...
};
Pasting Multiple Events
Our modified time range selection menu creates new events at the specified offset (depending on the distance from the first event) at the target location. In this example, the backend event creating method is called for every event separately but you may want to replace it with a batch method that creates all events in a single transaction.
config: DayPilot.SchedulerConfig = {
// ...
contextMenuSelection: new DayPilot.Menu({
onShow: args => {
let noItemsInClipboard = this.clipboard.length === 0;
args.menu.items[0].disabled = noItemsInClipboard;
},
items: [
{
text: "Paste",
onClick: args => {
if (this.clipboard.length === 0) {
return;
}
const targetStart = args.source.start;
const targetResource = args.source.resource;
const firstStart = this.clipboard[0].start();
this.clipboard.forEach(e => {
const offset = new DayPilot.Duration(firstStart, e.start());
const duration = e.duration();
const start = targetStart.addTime(offset);
const end = start.addTime(duration);
const params = {
start: start,
end: end,
resource: targetResource,
text: e.text()
};
this.ds.createEvent(params).subscribe(result => this.scheduler.control.events.add(result));
});
}
}
]
})
};
Automatically Copy Selected Events to Clipboard
It may be more convenient to copy the selected events to the clipboard automatically, immediately after selection. We will add a checkbox for this option and implement the "auto copy" feature using onEventSelected event handler of the Scheduler. Unlike onEventSelect
, this event handler is called after the selection is updated so we can get the updated list of selected events there.
import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `
<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>
<div class="options" *ngIf="false">
<label for="autocopy"><input id="autocopy" type="checkbox" [(ngModel)]="autoCopy"> Copy to clipboard automatically</label>
</div>
...
`,
styles: [`...`]
})
export class SchedulerComponent implements AfterViewInit {
@ViewChild("scheduler")
scheduler!: DayPilotSchedulerComponent;
clipboard: DayPilot.Event[] = [];
autoCopy: boolean = false;
config: DayPilot.SchedulerConfig = {
// ...
onEventSelected: args => {
if (this.autoCopy) {
this.clipboard = this.scheduler.control.multiselect.events();
}
},
// ...
};
// ...
}
Cut & Paste
In this last step, we will extend out copy and paste example with a “Cut” option.
First, we will add “Cut” item to the Scheduler event context menu:
contextMenu: new DayPilot.Menu({
items: [
{
text: "Copy",
onClick: args => {
let selected = this.scheduler.control.multiselect.events();
this.clipboard = selected.sort((e1, e2) => e1.start().getTime() - e2.start().getTime());
this.cut = false;
}
},
{
text: "Cut",
onClick: args => {
let selected = this.scheduler.control.multiselect.events();
this.clipboard = selected.sort((e1, e2) => e1.start().getTime() - e2.start().getTime());
this.cut = true;
}
},
]
})
As you can see the implementation is the same. The only difference is that we set the cut
flag to true.
export class SchedulerComponent implements AfterViewInit {
// ...
cut: boolean = false;
// ...
}
This flag is implemented as a property of our SchedulerComponent
class. The default value is set to false - but we need to set the value explicitly when “Copy” or “Cut” menu items are clicked.
The “Paste” item of the time range selection context menu will have to include more complex logic. It needs to check whether the cut
flag is set and update the events instead of making a copy.
contextMenuSelection: new DayPilot.Menu({
items: [
{
text: "Paste",
onClick: args => {
if (this.clipboard.length === 0) {
return;
}
let targetStart = args.source.start;
let targetResource = args.source.resource;
let firstStart = this.clipboard[0].start();
this.scheduler.control.clearSelection();
this.clipboard.forEach(e => {
const offset = new DayPilot.Duration(firstStart, e.start());
const duration = e.duration();
const start = targetStart.addTime(offset);
const end = start.addTime(duration);
if (this.cut) {
this.ds.updateEvent(e, start, end, targetResource).subscribe(result => {
this.scheduler.control.events.update(result);
});
} else {
const params = {
start: start,
end: end,
resource: targetResource,
text: e.text()
};
this.ds.createEvent(params).subscribe(result => {
this.scheduler.control.events.add(result);
});
}
});
}
}
]
})
The updateEvent()
method of DataService
updates the event data object:
updateEvent(e: DayPilot.Event, start: DayPilot.Date, end: DayPilot.Date, resource: string): Observable<EventData> {
e.data.start = start;
e.data.end = end;
e.data.resource = resource;
// simulating an HTTP request
return new Observable(observer => {
setTimeout(() => {
observer.next(e.data);
observer.complete();
}, 200);
});
}
Depending on the desired behavior, you can also clear the clipboard items in the “Paste” action.