Skip to content

Commit cd12d3c

Browse files
committed
feat(week-view): allow customising where events can be dragged
Closes #1183
1 parent 40a1d31 commit cd12d3c

File tree

9 files changed

+472
-25
lines changed

9 files changed

+472
-25
lines changed

projects/angular-calendar/src/modules/day/calendar-day-view.component.ts

+9
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export type CalendarDayViewBeforeRenderEvent = CalendarWeekViewBeforeRenderEvent
5353
[snapDraggedEvents]="snapDraggedEvents"
5454
[allDayEventsLabelTemplate]="allDayEventsLabelTemplate"
5555
[currentTimeMarkerTemplate]="currentTimeMarkerTemplate"
56+
[validateEventTimesChanged]="validateEventTimesChanged"
5657
(eventClicked)="eventClicked.emit($event)"
5758
(hourSegmentClicked)="hourSegmentClicked.emit($event)"
5859
(eventTimesChanged)="eventTimesChanged.emit($event)"
@@ -183,6 +184,14 @@ export class CalendarDayViewComponent {
183184
*/
184185
@Input() currentTimeMarkerTemplate: TemplateRef<any>;
185186

187+
/**
188+
* Allow you to customise where events can be dragged and resized to.
189+
* Return true to allow dragging and resizing to the new location, or false to prevent it
190+
*/
191+
@Input() validateEventTimesChanged: (
192+
event: CalendarEventTimesChangedEvent
193+
) => boolean;
194+
186195
/**
187196
* Called when an event title is clicked
188197
*/

projects/angular-calendar/src/modules/week/calendar-week-view.component.ts

+49-20
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ export interface CalendarWeekViewBeforeRenderEvent extends WeekView {
166166
[dragSnapGrid]="snapDraggedEvents ? { x: dayColumnWidth } : {}"
167167
[validateDrag]="validateDrag"
168168
[touchStartLongPress]="{ delay: 300, delta: 30 }"
169-
(dragStart)="dragStarted(eventRowContainer, event)"
169+
(dragStart)="
170+
dragStarted(eventRowContainer, event, allDayEvent, false)
171+
"
170172
(dragging)="allDayEventDragMove()"
171173
(dragEnd)="dragEnded(allDayEvent, $event, dayColumnWidth)"
172174
>
@@ -314,7 +316,7 @@ export interface CalendarWeekViewBeforeRenderEvent extends WeekView {
314316
[ghostDragEnabled]="!snapDraggedEvents"
315317
[ghostElementTemplate]="weekEventTemplate"
316318
[validateDrag]="validateDrag"
317-
(dragStart)="dragStarted(dayColumns, event, timeEvent)"
319+
(dragStart)="dragStarted(dayColumns, event, timeEvent, true)"
318320
(dragging)="dragMove(timeEvent, $event)"
319321
(dragEnd)="dragEnded(timeEvent, $event, dayColumnWidth, true)"
320322
>
@@ -575,6 +577,14 @@ export class CalendarWeekViewComponent
575577
*/
576578
@Input() currentTimeMarkerTemplate: TemplateRef<any>;
577579

580+
/**
581+
* Allow you to customise where events can be dragged and resized to.
582+
* Return true to allow dragging and resizing to the new location, or false to prevent it
583+
*/
584+
@Input() validateEventTimesChanged: (
585+
event: CalendarEventTimesChangedEvent
586+
) => boolean;
587+
578588
/**
579589
* Called when a header week day is clicked. Adding a `cssClass` property on `$event.day` will add that class to the header element
580590
*/
@@ -1023,37 +1033,56 @@ export class CalendarWeekViewComponent
10231033
* @hidden
10241034
*/
10251035
dragStarted(
1026-
eventsContainer: HTMLElement,
1027-
event: HTMLElement,
1028-
dayEvent?: WeekViewTimeEvent
1036+
eventsContainerElement: HTMLElement,
1037+
eventElement: HTMLElement,
1038+
event: WeekViewTimeEvent | WeekViewAllDayEvent,
1039+
useY: boolean
10291040
): void {
1030-
this.dayColumnWidth = this.getDayColumnWidth(eventsContainer);
1041+
this.dayColumnWidth = this.getDayColumnWidth(eventsContainerElement);
10311042
const dragHelper: CalendarDragHelper = new CalendarDragHelper(
1032-
eventsContainer,
1033-
event
1043+
eventsContainerElement,
1044+
eventElement
10341045
);
1035-
this.validateDrag = ({ x, y, transform }) =>
1036-
this.allDayEventResizes.size === 0 &&
1037-
this.timeEventResizes.size === 0 &&
1038-
dragHelper.validateDrag({
1039-
x,
1040-
y,
1041-
snapDraggedEvents: this.snapDraggedEvents,
1042-
dragAlreadyMoved: this.dragAlreadyMoved,
1043-
transform,
1044-
});
1046+
this.validateDrag = ({ x, y, transform }) => {
1047+
const isAllowed =
1048+
this.allDayEventResizes.size === 0 &&
1049+
this.timeEventResizes.size === 0 &&
1050+
dragHelper.validateDrag({
1051+
x,
1052+
y,
1053+
snapDraggedEvents: this.snapDraggedEvents,
1054+
dragAlreadyMoved: this.dragAlreadyMoved,
1055+
transform,
1056+
});
1057+
if (isAllowed && this.validateEventTimesChanged) {
1058+
const newEventTimes = this.getDragMovedEventTimes(
1059+
event,
1060+
{ x, y },
1061+
this.dayColumnWidth,
1062+
useY
1063+
);
1064+
return this.validateEventTimesChanged({
1065+
type: CalendarEventTimesChangedEventType.Drag,
1066+
event: event.event,
1067+
newStart: newEventTimes.start,
1068+
newEnd: newEventTimes.end,
1069+
});
1070+
}
1071+
1072+
return isAllowed;
1073+
};
10451074
this.dragActive = true;
10461075
this.dragAlreadyMoved = false;
10471076
this.lastDraggedEvent = null;
10481077
this.eventDragEnterByType = {
10491078
allDay: 0,
10501079
time: 0,
10511080
};
1052-
if (!this.snapDraggedEvents && dayEvent) {
1081+
if (!this.snapDraggedEvents && useY) {
10531082
this.view.hourColumns.forEach((column) => {
10541083
const linkedEvent = column.events.find(
10551084
(columnEvent) =>
1056-
columnEvent.event === dayEvent.event && columnEvent !== dayEvent
1085+
columnEvent.event === event.event && columnEvent !== event
10571086
);
10581087
// hide any linked events while dragging
10591088
if (linkedEvent) {

projects/angular-calendar/test/calendar-week-view.component.spec.ts

+196-3
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,102 @@ describe('calendarWeekView component', () => {
701701
expect(eventDropped).to.have.been.calledOnce;
702702
});
703703

704+
it('should allow the all day event to be dragged and dropped and control where it can be dragged', () => {
705+
const fixture: ComponentFixture<CalendarWeekViewComponent> = TestBed.createComponent(
706+
CalendarWeekViewComponent
707+
);
708+
fixture.componentInstance.viewDate = new Date('2016-12-08');
709+
fixture.componentInstance.events = [
710+
{
711+
title: 'foo',
712+
start: moment('2016-12-08').add(4, 'hours').toDate(),
713+
end: moment('2016-12-08').add(6, 'hours').toDate(),
714+
draggable: true,
715+
allDay: true,
716+
},
717+
];
718+
fixture.componentInstance.ngOnChanges({ viewDate: {}, events: {} });
719+
fixture.detectChanges();
720+
document.body.appendChild(fixture.nativeElement);
721+
// remove the header as it was causing the test to fail
722+
const header: HTMLElement = fixture.nativeElement.querySelector(
723+
'.cal-day-headers'
724+
);
725+
header.parentNode.removeChild(header);
726+
const event: HTMLElement = fixture.nativeElement.querySelector(
727+
'.cal-event-container'
728+
);
729+
const dayWidth: number = event.parentElement.offsetWidth / 7;
730+
const eventPosition: ClientRect = event.getBoundingClientRect();
731+
const eventDropped = sinon.spy();
732+
const validateEventTimesChanged = sinon
733+
.stub()
734+
.onFirstCall()
735+
.returns(true)
736+
.onSecondCall()
737+
.returns(false);
738+
fixture.componentInstance.validateEventTimesChanged = validateEventTimesChanged;
739+
fixture.componentInstance.eventTimesChanged
740+
.pipe(take(1))
741+
.subscribe(eventDropped);
742+
triggerDomEvent('mousedown', event, {
743+
clientX: eventPosition.left,
744+
clientY: eventPosition.top,
745+
button: 0,
746+
});
747+
fixture.detectChanges();
748+
triggerDomEvent('mousemove', document.body, {
749+
clientX: eventPosition.left,
750+
clientY: eventPosition.top,
751+
});
752+
fixture.detectChanges();
753+
triggerDomEvent('mousemove', document.body, {
754+
clientX: eventPosition.left - dayWidth,
755+
clientY: eventPosition.top,
756+
});
757+
fixture.detectChanges();
758+
const ghostElement = event.nextSibling as HTMLElement;
759+
expect(Math.round(ghostElement.getBoundingClientRect().left)).to.equal(
760+
Math.round(eventPosition.left - dayWidth) + 1
761+
);
762+
triggerDomEvent('mousemove', document.body, {
763+
clientX: eventPosition.left - dayWidth * 2,
764+
clientY: eventPosition.top,
765+
});
766+
fixture.detectChanges();
767+
expect(Math.round(ghostElement.getBoundingClientRect().left)).to.equal(
768+
Math.round(eventPosition.left - dayWidth) + 1
769+
);
770+
triggerDomEvent('mouseup', document.body, {
771+
clientX: eventPosition.left - dayWidth * 2,
772+
clientY: eventPosition.top,
773+
button: 0,
774+
});
775+
fixture.detectChanges();
776+
fixture.destroy();
777+
expect(eventDropped.getCall(0).args[0]).to.deep.equal({
778+
type: 'drag',
779+
event: fixture.componentInstance.events[0],
780+
newStart: moment('2016-12-07').add(4, 'hours').toDate(),
781+
newEnd: moment('2016-12-07').add(6, 'hours').toDate(),
782+
allDay: true,
783+
});
784+
expect(eventDropped).to.have.been.calledOnce;
785+
expect(validateEventTimesChanged).to.have.been.calledTwice;
786+
expect(validateEventTimesChanged.getCall(0).args[0]).to.deep.equal({
787+
type: 'drag',
788+
event: fixture.componentInstance.events[0],
789+
newStart: moment('2016-12-07').add(4, 'hours').toDate(),
790+
newEnd: moment('2016-12-07').add(6, 'hours').toDate(),
791+
});
792+
expect(validateEventTimesChanged.getCall(1).args[0]).to.deep.equal({
793+
type: 'drag',
794+
event: fixture.componentInstance.events[0],
795+
newStart: moment('2016-12-06').add(4, 'hours').toDate(),
796+
newEnd: moment('2016-12-06').add(6, 'hours').toDate(),
797+
});
798+
});
799+
704800
it('should allow all day events to be dragged outside of the calendar', () => {
705801
const fixture: ComponentFixture<CalendarWeekViewComponent> = TestBed.createComponent(
706802
CalendarWeekViewComponent
@@ -709,7 +805,6 @@ describe('calendarWeekView component', () => {
709805
fixture.componentInstance.events = [
710806
{
711807
title: 'foo',
712-
color: { primary: '', secondary: '' },
713808
start: moment('2016-12-08').add(4, 'hours').toDate(),
714809
end: moment('2016-12-08').add(6, 'hours').toDate(),
715810
draggable: true,
@@ -833,7 +928,6 @@ describe('calendarWeekView component', () => {
833928
fixture.componentInstance.events = [
834929
{
835930
title: 'foo',
836-
color: { primary: '', secondary: '' },
837931
start: moment('2016-06-27').add(4, 'hours').toDate(),
838932
end: moment('2016-06-27').add(6, 'hours').toDate(),
839933
resizable: {
@@ -887,7 +981,6 @@ describe('calendarWeekView component', () => {
887981
fixture.componentInstance.events = [
888982
{
889983
title: 'foo',
890-
color: { primary: '', secondary: '' },
891984
start: moment('2016-06-27').add(4, 'hours').toDate(),
892985
end: moment('2016-06-27').add(6, 'hours').toDate(),
893986
resizable: {
@@ -1829,6 +1922,106 @@ describe('calendarWeekView component', () => {
18291922
});
18301923
});
18311924

1925+
it('should control dragging time events', () => {
1926+
const fixture: ComponentFixture<CalendarWeekViewComponent> = TestBed.createComponent(
1927+
CalendarWeekViewComponent
1928+
);
1929+
fixture.componentInstance.viewDate = new Date('2018-07-29');
1930+
fixture.componentInstance.events = [
1931+
{
1932+
start: moment(new Date('2018-07-29'))
1933+
.startOf('day')
1934+
.add(3, 'hours')
1935+
.toDate(),
1936+
end: moment(new Date('2018-07-29'))
1937+
.startOf('day')
1938+
.add(1, 'day')
1939+
.add(18, 'hours')
1940+
.toDate(),
1941+
title: 'foo',
1942+
draggable: true,
1943+
},
1944+
];
1945+
const validateEventTimesChanged = sinon
1946+
.stub()
1947+
.onFirstCall()
1948+
.returns(true)
1949+
.onSecondCall()
1950+
.returns(false);
1951+
fixture.componentInstance.validateEventTimesChanged = validateEventTimesChanged;
1952+
fixture.componentInstance.ngOnChanges({ viewDate: {}, events: {} });
1953+
fixture.detectChanges();
1954+
document.body.appendChild(fixture.nativeElement);
1955+
const event = fixture.nativeElement.querySelector('.cal-event-container');
1956+
const rect: ClientRect = event.getBoundingClientRect();
1957+
let dragEvent: CalendarEventTimesChangedEvent;
1958+
fixture.componentInstance.eventTimesChanged.pipe(take(1)).subscribe((e) => {
1959+
dragEvent = e;
1960+
});
1961+
triggerDomEvent('mousedown', event, {
1962+
clientX: rect.right,
1963+
clientY: rect.bottom,
1964+
button: 0,
1965+
});
1966+
fixture.detectChanges();
1967+
triggerDomEvent('mousemove', event, {
1968+
clientX: rect.right,
1969+
clientY: rect.bottom,
1970+
});
1971+
fixture.detectChanges();
1972+
triggerDomEvent('mousemove', event, {
1973+
clientX: rect.right,
1974+
clientY: rect.bottom + 95,
1975+
});
1976+
fixture.detectChanges();
1977+
expect(event.getBoundingClientRect().top).to.equal(rect.top + 90);
1978+
triggerDomEvent('mousemove', event, {
1979+
clientX: rect.right,
1980+
clientY: rect.bottom + 180,
1981+
});
1982+
fixture.detectChanges();
1983+
expect(event.getBoundingClientRect().top).to.equal(rect.top + 90);
1984+
triggerDomEvent('mouseup', event, {
1985+
clientX: rect.right,
1986+
clientY: rect.bottom + 180,
1987+
button: 0,
1988+
});
1989+
fixture.detectChanges();
1990+
fixture.destroy();
1991+
expect(dragEvent).to.deep.equal({
1992+
type: 'drag',
1993+
allDay: false,
1994+
event: fixture.componentInstance.events[0],
1995+
newStart: moment(fixture.componentInstance.events[0].start)
1996+
.add(90, 'minutes')
1997+
.toDate(),
1998+
newEnd: moment(fixture.componentInstance.events[0].end)
1999+
.add(90, 'minutes')
2000+
.toDate(),
2001+
});
2002+
expect(validateEventTimesChanged).to.have.been.calledTwice;
2003+
expect(validateEventTimesChanged.getCall(0).args[0]).to.deep.equal({
2004+
type: 'drag',
2005+
event: fixture.componentInstance.events[0],
2006+
newStart: moment(fixture.componentInstance.events[0].start)
2007+
.add(90, 'minutes')
2008+
.toDate(),
2009+
newEnd: moment(fixture.componentInstance.events[0].end)
2010+
.add(90, 'minutes')
2011+
.toDate(),
2012+
});
2013+
expect(validateEventTimesChanged.getCall(1).args[0]).to.deep.equal({
2014+
type: 'drag',
2015+
event: fixture.componentInstance.events[0],
2016+
newStart: moment(fixture.componentInstance.events[0].start)
2017+
.add(180, 'minutes')
2018+
.toDate(),
2019+
newEnd: moment(fixture.componentInstance.events[0].end)
2020+
.add(180, 'minutes')
2021+
.toDate(),
2022+
});
2023+
});
2024+
18322025
it('should drag time events back to their original position while snapping to a grid', () => {
18332026
const fixture: ComponentFixture<CalendarWeekViewComponent> = TestBed.createComponent(
18342027
CalendarWeekViewComponent

projects/demos/app/demo-app.module.ts

+10
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,16 @@ import { ClipboardModule } from 'ngx-clipboard';
458458
label: 'RTL',
459459
},
460460
},
461+
{
462+
path: 'validate-drag-and-resize',
463+
loadChildren: () =>
464+
import('./demo-modules/validate-drag-and-resize/module').then(
465+
(m) => m.DemoModule
466+
),
467+
data: {
468+
label: 'Validate dragging and resizing',
469+
},
470+
},
461471
{
462472
path: '**',
463473
redirectTo: 'kitchen-sink',

0 commit comments

Comments
 (0)