projects/angular-calendar/src/modules/week/calendar-week-view/calendar-week-view.component.ts
Shows all events on a given week. Example usage:
<mwl-calendar-week-view
[viewDate]="viewDate"
[events]="events">
</mwl-calendar-week-view>
OnChanges
OnInit
OnDestroy
AfterViewInit
selector | mwl-calendar-week-view |
template |
|
allDayEventsLabelTemplate | |
Type : TemplateRef<any>
|
|
A custom template to use for the all day events label text |
currentTimeMarkerTemplate | |
Type : TemplateRef<any>
|
|
A custom template to use for the current time marker |
dayEndHour | |
Type : number
|
|
Default value : 23
|
|
The day end hours in 24 hour time. Must be 0-23 |
dayEndMinute | |
Type : number
|
|
Default value : 59
|
|
The day end minutes. Must be 0-59 |
daysInWeek | |
Type : number
|
|
The number of days in a week. Can be used to create a shorter or longer week view.
The first day of the week will always be the |
dayStartHour | |
Type : number
|
|
Default value : 0
|
|
The day start hours in 24 hour time. Must be 0-23 |
dayStartMinute | |
Type : number
|
|
Default value : 0
|
|
The day start minutes. Must be 0-59 |
eventActionsTemplate | |
Type : TemplateRef<any>
|
|
A custom template to use for event actions |
events | |
Type : CalendarEvent[]
|
|
Default value : []
|
|
An array of events to display on view The schema is available here: https://github.com/mattlewis92/calendar-utils/blob/c51689985f59a271940e30bc4e2c4e1fee3fcb5c/src/calendarUtils.ts#L49-L63 |
eventSnapSize | |
Type : number
|
|
The grid size to snap resizing and dragging of hourly events to |
eventTemplate | |
Type : TemplateRef<any>
|
|
A custom template to use for week view events |
eventTitleTemplate | |
Type : TemplateRef<any>
|
|
A custom template to use for event titles |
excludeDays | |
Type : number[]
|
|
Default value : []
|
|
An array of day indexes (0 = sunday, 1 = monday etc) that will be hidden on the view |
headerTemplate | |
Type : TemplateRef<any>
|
|
A custom template to use to replace the header |
hourDuration | |
Type : number
|
|
The duration of each segment group in minutes |
hourSegmentHeight | |
Type : number
|
|
Default value : 30
|
|
The height in pixels of each hour segment |
hourSegments | |
Type : number
|
|
Default value : 2
|
|
The number of segments in an hour. Must divide equally into 60. |
hourSegmentTemplate | |
Type : TemplateRef<any>
|
|
A custom template to use to replace the hour segment |
locale | |
Type : string
|
|
The locale used to format dates |
minimumEventHeight | |
Type : number
|
|
Default value : 30
|
|
The minimum height in pixels of each event |
precision | |
Type : "days" | "minutes"
|
|
Default value : 'days'
|
|
The precision to display events.
|
refresh | |
Type : Subject<any>
|
|
An observable that when emitted on will re-render the current view |
resizeCursors | |
Type : Partial<Pick<ResizeCursors, "leftOrRight" | "topOrBottom">>
|
|
Customise the document cursor when dragging to resize an event |
snapDraggedEvents | |
Type : boolean
|
|
Default value : true
|
|
Whether to snap events to a grid when dragging |
tooltipAppendToBody | |
Type : boolean
|
|
Default value : true
|
|
Whether to append tooltips to the body or next to the trigger element |
tooltipDelay | |
Type : number | null
|
|
Default value : null
|
|
The delay in milliseconds before the tooltip should be displayed. If not provided the tooltip will be displayed immediately. |
tooltipPlacement | |
Type : PlacementArray
|
|
Default value : 'auto'
|
|
The placement of the event tooltip |
tooltipTemplate | |
Type : TemplateRef<any>
|
|
A custom template to use for the event tooltips |
validateEventTimesChanged | |
Type : function
|
|
Allow you to customise where events can be dragged and resized to. Return true to allow dragging and resizing to the new location, or false to prevent it |
viewDate | |
Type : Date
|
|
The current view date |
weekendDays | |
Type : number[]
|
|
An array of day indexes (0 = sunday, 1 = monday etc) that indicate which days are weekends |
weekStartsOn | |
Type : number
|
|
The start number of the week.
This is ignored when the
|
beforeViewRender | |
Type : EventEmitter
|
|
An output that will be called before the view is rendered for the current week.
If you add the |
dayHeaderClicked | |
Type : EventEmitter
|
|
Called when a header week day is clicked. Adding a |
eventClicked | |
Type : EventEmitter
|
|
Called when an event title is clicked |
eventTimesChanged | |
Type : EventEmitter
|
|
Called when an event is resized or dragged and dropped |
hourSegmentClicked | |
Type : EventEmitter
|
|
Called when an hour segment is clicked |
import {
Component,
Input,
Output,
EventEmitter,
ChangeDetectorRef,
OnChanges,
OnInit,
OnDestroy,
LOCALE_ID,
Inject,
TemplateRef,
ElementRef,
AfterViewInit,
} from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import {
WeekDay,
CalendarEvent,
WeekViewAllDayEvent,
WeekView,
ViewPeriod,
WeekViewHourColumn,
WeekViewTimeEvent,
WeekViewHourSegment,
WeekViewHour,
WeekViewAllDayEventRow,
} from 'calendar-utils';
import { ResizeEvent } from 'angular-resizable-element';
import { ResizeCursors } from 'angular-resizable-element';
import { CalendarDragHelper } from '../../common/calendar-drag-helper/calendar-drag-helper.provider';
import { CalendarResizeHelper } from '../../common/calendar-resize-helper/calendar-resize-helper.provider';
import {
CalendarEventTimesChangedEvent,
CalendarEventTimesChangedEventType,
} from '../../common/calendar-event-times-changed-event/calendar-event-times-changed-event.interface';
import { CalendarUtils } from '../../common/calendar-utils/calendar-utils.provider';
import {
validateEvents,
roundToNearest,
trackByWeekDayHeaderDate,
trackByHourSegment,
trackByHour,
getMinutesMoved,
getDefaultEventEnd,
addDaysWithExclusions,
isDraggedWithinPeriod,
shouldFireDroppedEvent,
getWeekViewPeriod,
trackByWeekAllDayEvent,
trackByWeekTimeEvent,
} from '../../common/util/util';
import { DateAdapter } from '../../../date-adapters/date-adapter';
import {
DragEndEvent,
DropEvent,
DragMoveEvent,
ValidateDrag,
} from 'angular-draggable-droppable';
import { PlacementArray } from 'positioning';
export interface WeekViewAllDayEventResize {
originalOffset: number;
originalSpan: number;
edge: string;
}
export interface CalendarWeekViewBeforeRenderEvent extends WeekView {
header: WeekDay[];
}
/**
* Shows all events on a given week. Example usage:
*
* ```typescript
* <mwl-calendar-week-view
* [viewDate]="viewDate"
* [events]="events">
* </mwl-calendar-week-view>
* ```
*/
@Component({
selector: 'mwl-calendar-week-view',
template: `
<div class="cal-week-view" role="grid">
<mwl-calendar-week-view-header
[days]="days"
[locale]="locale"
[customTemplate]="headerTemplate"
(dayHeaderClicked)="dayHeaderClicked.emit($event)"
(eventDropped)="
eventDropped({ dropData: $event }, $event.newStart, true)
"
(dragEnter)="dateDragEnter($event.date)"
>
</mwl-calendar-week-view-header>
<div
class="cal-all-day-events"
#allDayEventsContainer
*ngIf="view.allDayEventRows.length > 0"
mwlDroppable
(dragEnter)="dragEnter('allDay')"
(dragLeave)="dragLeave('allDay')"
>
<div class="cal-day-columns">
<div class="cal-time-label-column">
<ng-container
*ngTemplateOutlet="allDayEventsLabelTemplate"
></ng-container>
</div>
<div
class="cal-day-column"
*ngFor="let day of days; trackBy: trackByWeekDayHeaderDate"
mwlDroppable
dragOverClass="cal-drag-over"
(drop)="eventDropped($event, day.date, true)"
(dragEnter)="dateDragEnter(day.date)"
></div>
</div>
<div
*ngFor="let eventRow of view.allDayEventRows; trackBy: trackById"
#eventRowContainer
class="cal-events-row"
>
<div
*ngFor="
let allDayEvent of eventRow.row;
trackBy: trackByWeekAllDayEvent
"
#event
class="cal-event-container"
[class.cal-draggable]="
allDayEvent.event.draggable && allDayEventResizes.size === 0
"
[class.cal-starts-within-week]="!allDayEvent.startsBeforeWeek"
[class.cal-ends-within-week]="!allDayEvent.endsAfterWeek"
[ngClass]="allDayEvent.event?.cssClass"
[style.width.%]="(100 / days.length) * allDayEvent.span"
[style.marginLeft.%]="
rtl ? null : (100 / days.length) * allDayEvent.offset
"
[style.marginRight.%]="
rtl ? (100 / days.length) * allDayEvent.offset : null
"
mwlResizable
[resizeCursors]="resizeCursors"
[resizeSnapGrid]="{ left: dayColumnWidth, right: dayColumnWidth }"
[validateResize]="validateResize"
(resizeStart)="
allDayEventResizeStarted(eventRowContainer, allDayEvent, $event)
"
(resizing)="
allDayEventResizing(allDayEvent, $event, dayColumnWidth)
"
(resizeEnd)="allDayEventResizeEnded(allDayEvent)"
mwlDraggable
dragActiveClass="cal-drag-active"
[dropData]="{ event: allDayEvent.event, calendarId: calendarId }"
[dragAxis]="{
x: allDayEvent.event.draggable && allDayEventResizes.size === 0,
y:
!snapDraggedEvents &&
allDayEvent.event.draggable &&
allDayEventResizes.size === 0
}"
[dragSnapGrid]="snapDraggedEvents ? { x: dayColumnWidth } : {}"
[validateDrag]="validateDrag"
[touchStartLongPress]="{ delay: 300, delta: 30 }"
(dragStart)="
dragStarted(eventRowContainer, event, allDayEvent, false)
"
(dragging)="allDayEventDragMove()"
(dragEnd)="dragEnded(allDayEvent, $event, dayColumnWidth)"
>
<div
class="cal-resize-handle cal-resize-handle-before-start"
*ngIf="
allDayEvent.event?.resizable?.beforeStart &&
!allDayEvent.startsBeforeWeek
"
mwlResizeHandle
[resizeEdges]="{ left: true }"
></div>
<mwl-calendar-week-view-event
[locale]="locale"
[weekEvent]="allDayEvent"
[tooltipPlacement]="tooltipPlacement"
[tooltipTemplate]="tooltipTemplate"
[tooltipAppendToBody]="tooltipAppendToBody"
[tooltipDelay]="tooltipDelay"
[customTemplate]="eventTemplate"
[eventTitleTemplate]="eventTitleTemplate"
[eventActionsTemplate]="eventActionsTemplate"
[daysInWeek]="daysInWeek"
(eventClicked)="
eventClicked.emit({
event: allDayEvent.event,
sourceEvent: $event.sourceEvent
})
"
>
</mwl-calendar-week-view-event>
<div
class="cal-resize-handle cal-resize-handle-after-end"
*ngIf="
allDayEvent.event?.resizable?.afterEnd &&
!allDayEvent.endsAfterWeek
"
mwlResizeHandle
[resizeEdges]="{ right: true }"
></div>
</div>
</div>
</div>
<div
class="cal-time-events"
mwlDroppable
(dragEnter)="dragEnter('time')"
(dragLeave)="dragLeave('time')"
>
<div
class="cal-time-label-column"
*ngIf="view.hourColumns.length > 0 && daysInWeek !== 1"
>
<div
*ngFor="
let hour of view.hourColumns[0].hours;
trackBy: trackByHour;
let odd = odd
"
class="cal-hour"
[class.cal-hour-odd]="odd"
>
<mwl-calendar-week-view-hour-segment
*ngFor="let segment of hour.segments; trackBy: trackByHourSegment"
[style.height.px]="hourSegmentHeight"
[segment]="segment"
[segmentHeight]="hourSegmentHeight"
[locale]="locale"
[customTemplate]="hourSegmentTemplate"
[isTimeLabel]="true"
[daysInWeek]="daysInWeek"
>
</mwl-calendar-week-view-hour-segment>
</div>
</div>
<div
class="cal-day-columns"
[class.cal-resize-active]="timeEventResizes.size > 0"
#dayColumns
>
<div
class="cal-day-column"
*ngFor="let column of view.hourColumns; trackBy: trackByHourColumn"
>
<mwl-calendar-week-view-current-time-marker
[columnDate]="column.date"
[dayStartHour]="dayStartHour"
[dayStartMinute]="dayStartMinute"
[dayEndHour]="dayEndHour"
[dayEndMinute]="dayEndMinute"
[hourSegments]="hourSegments"
[hourDuration]="hourDuration"
[hourSegmentHeight]="hourSegmentHeight"
[customTemplate]="currentTimeMarkerTemplate"
></mwl-calendar-week-view-current-time-marker>
<div class="cal-events-container">
<div
*ngFor="
let timeEvent of column.events;
trackBy: trackByWeekTimeEvent
"
#event
class="cal-event-container"
[class.cal-draggable]="
timeEvent.event.draggable && timeEventResizes.size === 0
"
[class.cal-starts-within-day]="!timeEvent.startsBeforeDay"
[class.cal-ends-within-day]="!timeEvent.endsAfterDay"
[ngClass]="timeEvent.event.cssClass"
[hidden]="timeEvent.height === 0 && timeEvent.width === 0"
[style.top.px]="timeEvent.top"
[style.height.px]="timeEvent.height"
[style.left.%]="timeEvent.left"
[style.width.%]="timeEvent.width"
mwlResizable
[resizeCursors]="resizeCursors"
[resizeSnapGrid]="{
left: dayColumnWidth,
right: dayColumnWidth,
top: eventSnapSize || hourSegmentHeight,
bottom: eventSnapSize || hourSegmentHeight
}"
[validateResize]="validateResize"
[allowNegativeResizes]="true"
(resizeStart)="
timeEventResizeStarted(dayColumns, timeEvent, $event)
"
(resizing)="timeEventResizing(timeEvent, $event)"
(resizeEnd)="timeEventResizeEnded(timeEvent)"
mwlDraggable
dragActiveClass="cal-drag-active"
[dropData]="{ event: timeEvent.event, calendarId: calendarId }"
[dragAxis]="{
x: timeEvent.event.draggable && timeEventResizes.size === 0,
y: timeEvent.event.draggable && timeEventResizes.size === 0
}"
[dragSnapGrid]="
snapDraggedEvents
? {
x: dayColumnWidth,
y: eventSnapSize || hourSegmentHeight
}
: {}
"
[touchStartLongPress]="{ delay: 300, delta: 30 }"
[ghostDragEnabled]="!snapDraggedEvents"
[ghostElementTemplate]="weekEventTemplate"
[validateDrag]="validateDrag"
(dragStart)="dragStarted(dayColumns, event, timeEvent, true)"
(dragging)="dragMove(timeEvent, $event)"
(dragEnd)="dragEnded(timeEvent, $event, dayColumnWidth, true)"
>
<div
class="cal-resize-handle cal-resize-handle-before-start"
*ngIf="
timeEvent.event?.resizable?.beforeStart &&
!timeEvent.startsBeforeDay
"
mwlResizeHandle
[resizeEdges]="{
left: true,
top: true
}"
></div>
<ng-template
[ngTemplateOutlet]="weekEventTemplate"
></ng-template>
<ng-template #weekEventTemplate>
<mwl-calendar-week-view-event
[locale]="locale"
[weekEvent]="timeEvent"
[tooltipPlacement]="tooltipPlacement"
[tooltipTemplate]="tooltipTemplate"
[tooltipAppendToBody]="tooltipAppendToBody"
[tooltipDisabled]="dragActive || timeEventResizes.size > 0"
[tooltipDelay]="tooltipDelay"
[customTemplate]="eventTemplate"
[eventTitleTemplate]="eventTitleTemplate"
[eventActionsTemplate]="eventActionsTemplate"
[column]="column"
[daysInWeek]="daysInWeek"
(eventClicked)="
eventClicked.emit({
event: timeEvent.event,
sourceEvent: $event.sourceEvent
})
"
>
</mwl-calendar-week-view-event>
</ng-template>
<div
class="cal-resize-handle cal-resize-handle-after-end"
*ngIf="
timeEvent.event?.resizable?.afterEnd &&
!timeEvent.endsAfterDay
"
mwlResizeHandle
[resizeEdges]="{
right: true,
bottom: true
}"
></div>
</div>
</div>
<div
*ngFor="
let hour of column.hours;
trackBy: trackByHour;
let odd = odd
"
class="cal-hour"
[class.cal-hour-odd]="odd"
>
<mwl-calendar-week-view-hour-segment
*ngFor="
let segment of hour.segments;
trackBy: trackByHourSegment
"
[style.height.px]="hourSegmentHeight"
[segment]="segment"
[segmentHeight]="hourSegmentHeight"
[locale]="locale"
[customTemplate]="hourSegmentTemplate"
[daysInWeek]="daysInWeek"
(mwlClick)="
hourSegmentClicked.emit({
date: segment.date,
sourceEvent: $event
})
"
[clickListenerDisabled]="
hourSegmentClicked.observers.length === 0
"
mwlDroppable
[dragOverClass]="
!dragActive || !snapDraggedEvents ? 'cal-drag-over' : null
"
dragActiveClass="cal-drag-active"
(drop)="eventDropped($event, segment.date, false)"
(dragEnter)="dateDragEnter(segment.date)"
[isTimeLabel]="daysInWeek === 1"
>
</mwl-calendar-week-view-hour-segment>
</div>
</div>
</div>
</div>
</div>
`,
})
export class CalendarWeekViewComponent
implements OnChanges, OnInit, OnDestroy, AfterViewInit
{
/**
* The current view date
*/
@Input() viewDate: Date;
/**
* An array of events to display on view
* The schema is available here: https://github.com/mattlewis92/calendar-utils/blob/c51689985f59a271940e30bc4e2c4e1fee3fcb5c/src/calendarUtils.ts#L49-L63
*/
@Input() events: CalendarEvent[] = [];
/**
* An array of day indexes (0 = sunday, 1 = monday etc) that will be hidden on the view
*/
@Input() excludeDays: number[] = [];
/**
* An observable that when emitted on will re-render the current view
*/
@Input() refresh: Subject<any>;
/**
* The locale used to format dates
*/
@Input() locale: string;
/**
* The placement of the event tooltip
*/
@Input() tooltipPlacement: PlacementArray = 'auto';
/**
* A custom template to use for the event tooltips
*/
@Input() tooltipTemplate: TemplateRef<any>;
/**
* Whether to append tooltips to the body or next to the trigger element
*/
@Input() tooltipAppendToBody: boolean = true;
/**
* The delay in milliseconds before the tooltip should be displayed. If not provided the tooltip
* will be displayed immediately.
*/
@Input() tooltipDelay: number | null = null;
/**
* The start number of the week.
* This is ignored when the `daysInWeek` input is also set as the `viewDate` will be used as the start of the week instead.
* Note, you should also pass this to the calendar title pipe so it shows the same days: {{ viewDate | calendarDate:(view + 'ViewTitle'):locale:weekStartsOn }}
* If using the moment date adapter this option won't do anything and you'll need to set it globally like so:
* ```
* moment.updateLocale('en', {
* week: {
* dow: 1, // set start of week to monday instead
* doy: 0,
* },
* });
* ```
*/
@Input() weekStartsOn: number;
/**
* A custom template to use to replace the header
*/
@Input() headerTemplate: TemplateRef<any>;
/**
* A custom template to use for week view events
*/
@Input() eventTemplate: TemplateRef<any>;
/**
* A custom template to use for event titles
*/
@Input() eventTitleTemplate: TemplateRef<any>;
/**
* A custom template to use for event actions
*/
@Input() eventActionsTemplate: TemplateRef<any>;
/**
* The precision to display events.
* `days` will round event start and end dates to the nearest day and `minutes` will not do this rounding
*/
@Input() precision: 'days' | 'minutes' = 'days';
/**
* An array of day indexes (0 = sunday, 1 = monday etc) that indicate which days are weekends
*/
@Input() weekendDays: number[];
/**
* Whether to snap events to a grid when dragging
*/
@Input() snapDraggedEvents: boolean = true;
/**
* The number of segments in an hour. Must divide equally into 60.
*/
@Input() hourSegments: number = 2;
/**
* The duration of each segment group in minutes
*/
@Input() hourDuration: number;
/**
* The height in pixels of each hour segment
*/
@Input() hourSegmentHeight: number = 30;
/**
* The minimum height in pixels of each event
*/
@Input() minimumEventHeight: number = 30;
/**
* The day start hours in 24 hour time. Must be 0-23
*/
@Input() dayStartHour: number = 0;
/**
* The day start minutes. Must be 0-59
*/
@Input() dayStartMinute: number = 0;
/**
* The day end hours in 24 hour time. Must be 0-23
*/
@Input() dayEndHour: number = 23;
/**
* The day end minutes. Must be 0-59
*/
@Input() dayEndMinute: number = 59;
/**
* A custom template to use to replace the hour segment
*/
@Input() hourSegmentTemplate: TemplateRef<any>;
/**
* The grid size to snap resizing and dragging of hourly events to
*/
@Input() eventSnapSize: number;
/**
* A custom template to use for the all day events label text
*/
@Input() allDayEventsLabelTemplate: TemplateRef<any>;
/**
* The number of days in a week. Can be used to create a shorter or longer week view.
* The first day of the week will always be the `viewDate` and `weekStartsOn` if set will be ignored
*/
@Input() daysInWeek: number;
/**
* A custom template to use for the current time marker
*/
@Input() currentTimeMarkerTemplate: TemplateRef<any>;
/**
* Allow you to customise where events can be dragged and resized to.
* Return true to allow dragging and resizing to the new location, or false to prevent it
*/
@Input() validateEventTimesChanged: (
event: CalendarEventTimesChangedEvent
) => boolean;
/**
* Customise the document cursor when dragging to resize an event
*/
@Input() resizeCursors: Partial<
Pick<ResizeCursors, 'leftOrRight' | 'topOrBottom'>
>;
/**
* Called when a header week day is clicked. Adding a `cssClass` property on `$event.day` will add that class to the header element
*/
@Output() dayHeaderClicked = new EventEmitter<{
day: WeekDay;
sourceEvent: MouseEvent;
}>();
/**
* Called when an event title is clicked
*/
@Output() eventClicked = new EventEmitter<{
event: CalendarEvent;
sourceEvent: MouseEvent | KeyboardEvent;
}>();
/**
* Called when an event is resized or dragged and dropped
*/
@Output() eventTimesChanged =
new EventEmitter<CalendarEventTimesChangedEvent>();
/**
* An output that will be called before the view is rendered for the current week.
* If you add the `cssClass` property to a day in the header it will add that class to the cell element in the template
*/
@Output() beforeViewRender =
new EventEmitter<CalendarWeekViewBeforeRenderEvent>();
/**
* Called when an hour segment is clicked
*/
@Output() hourSegmentClicked = new EventEmitter<{
date: Date;
sourceEvent: MouseEvent;
}>();
/**
* @hidden
*/
days: WeekDay[];
/**
* @hidden
*/
view: WeekView;
/**
* @hidden
*/
refreshSubscription: Subscription;
/**
* @hidden
*/
allDayEventResizes: Map<WeekViewAllDayEvent, WeekViewAllDayEventResize> =
new Map();
/**
* @hidden
*/
timeEventResizes: Map<CalendarEvent, ResizeEvent> = new Map();
/**
* @hidden
*/
eventDragEnterByType = {
allDay: 0,
time: 0,
};
/**
* @hidden
*/
dragActive = false;
/**
* @hidden
*/
dragAlreadyMoved = false;
/**
* @hidden
*/
validateDrag: ValidateDrag;
/**
* @hidden
*/
validateResize: (args: any) => boolean;
/**
* @hidden
*/
dayColumnWidth: number;
/**
* @hidden
*/
calendarId = Symbol('angular calendar week view id');
/**
* @hidden
*/
lastDraggedEvent: CalendarEvent;
/**
* @hidden
*/
rtl = false;
/**
* @hidden
*/
trackByWeekDayHeaderDate = trackByWeekDayHeaderDate;
/**
* @hidden
*/
trackByHourSegment = trackByHourSegment;
/**
* @hidden
*/
trackByHour = trackByHour;
/**
* @hidden
*/
trackByWeekAllDayEvent = trackByWeekAllDayEvent;
/**
* @hidden
*/
trackByWeekTimeEvent = trackByWeekTimeEvent;
/**
* @hidden
*/
private lastDragEnterDate: Date;
/**
* @hidden
*/
constructor(
protected cdr: ChangeDetectorRef,
protected utils: CalendarUtils,
@Inject(LOCALE_ID) locale: string,
protected dateAdapter: DateAdapter,
protected element: ElementRef<HTMLElement>
) {
this.locale = locale;
}
/**
* @hidden
*/
trackByHourColumn = (index: number, column: WeekViewHourColumn) =>
column.hours[0] ? column.hours[0].segments[0].date.toISOString() : column;
/**
* @hidden
*/
trackById = (index: number, row: WeekViewAllDayEventRow) => row.id;
/**
* @hidden
*/
ngOnInit(): void {
if (this.refresh) {
this.refreshSubscription = this.refresh.subscribe(() => {
this.refreshAll();
this.cdr.markForCheck();
});
}
}
/**
* @hidden
*/
ngOnChanges(changes: any): void {
const refreshHeader =
changes.viewDate ||
changes.excludeDays ||
changes.weekendDays ||
changes.daysInWeek ||
changes.weekStartsOn;
const refreshBody =
changes.viewDate ||
changes.dayStartHour ||
changes.dayStartMinute ||
changes.dayEndHour ||
changes.dayEndMinute ||
changes.hourSegments ||
changes.hourDuration ||
changes.weekStartsOn ||
changes.weekendDays ||
changes.excludeDays ||
changes.hourSegmentHeight ||
changes.events ||
changes.daysInWeek ||
changes.minimumEventHeight;
if (refreshHeader) {
this.refreshHeader();
}
if (changes.events) {
validateEvents(this.events);
}
if (refreshBody) {
this.refreshBody();
}
if (refreshHeader || refreshBody) {
this.emitBeforeViewRender();
}
}
/**
* @hidden
*/
ngOnDestroy(): void {
if (this.refreshSubscription) {
this.refreshSubscription.unsubscribe();
}
}
/**
* @hidden
*/
ngAfterViewInit() {
this.rtl =
typeof window !== 'undefined' &&
getComputedStyle(this.element.nativeElement).direction === 'rtl';
this.cdr.detectChanges();
}
/**
* @hidden
*/
timeEventResizeStarted(
eventsContainer: HTMLElement,
timeEvent: WeekViewTimeEvent,
resizeEvent: ResizeEvent
): void {
this.timeEventResizes.set(timeEvent.event, resizeEvent);
this.resizeStarted(eventsContainer, timeEvent);
}
/**
* @hidden
*/
timeEventResizing(timeEvent: WeekViewTimeEvent, resizeEvent: ResizeEvent) {
this.timeEventResizes.set(timeEvent.event, resizeEvent);
const adjustedEvents = new Map<CalendarEvent, CalendarEvent>();
const tempEvents = [...this.events];
this.timeEventResizes.forEach((lastResizeEvent, event) => {
const newEventDates = this.getTimeEventResizedDates(
event,
lastResizeEvent
);
const adjustedEvent = { ...event, ...newEventDates };
adjustedEvents.set(adjustedEvent, event);
const eventIndex = tempEvents.indexOf(event);
tempEvents[eventIndex] = adjustedEvent;
});
this.restoreOriginalEvents(tempEvents, adjustedEvents, true);
}
/**
* @hidden
*/
timeEventResizeEnded(timeEvent: WeekViewTimeEvent) {
this.view = this.getWeekView(this.events);
const lastResizeEvent = this.timeEventResizes.get(timeEvent.event);
if (lastResizeEvent) {
this.timeEventResizes.delete(timeEvent.event);
const newEventDates = this.getTimeEventResizedDates(
timeEvent.event,
lastResizeEvent
);
this.eventTimesChanged.emit({
newStart: newEventDates.start,
newEnd: newEventDates.end,
event: timeEvent.event,
type: CalendarEventTimesChangedEventType.Resize,
});
}
}
/**
* @hidden
*/
allDayEventResizeStarted(
allDayEventsContainer: HTMLElement,
allDayEvent: WeekViewAllDayEvent,
resizeEvent: ResizeEvent
): void {
this.allDayEventResizes.set(allDayEvent, {
originalOffset: allDayEvent.offset,
originalSpan: allDayEvent.span,
edge: typeof resizeEvent.edges.left !== 'undefined' ? 'left' : 'right',
});
this.resizeStarted(
allDayEventsContainer,
allDayEvent,
this.getDayColumnWidth(allDayEventsContainer)
);
}
/**
* @hidden
*/
allDayEventResizing(
allDayEvent: WeekViewAllDayEvent,
resizeEvent: ResizeEvent,
dayWidth: number
): void {
const currentResize: WeekViewAllDayEventResize =
this.allDayEventResizes.get(allDayEvent);
const modifier = this.rtl ? -1 : 1;
if (typeof resizeEvent.edges.left !== 'undefined') {
const diff: number =
Math.round(+resizeEvent.edges.left / dayWidth) * modifier;
allDayEvent.offset = currentResize.originalOffset + diff;
allDayEvent.span = currentResize.originalSpan - diff;
} else if (typeof resizeEvent.edges.right !== 'undefined') {
const diff: number =
Math.round(+resizeEvent.edges.right / dayWidth) * modifier;
allDayEvent.span = currentResize.originalSpan + diff;
}
}
/**
* @hidden
*/
allDayEventResizeEnded(allDayEvent: WeekViewAllDayEvent): void {
const currentResize: WeekViewAllDayEventResize =
this.allDayEventResizes.get(allDayEvent);
if (currentResize) {
const allDayEventResizingBeforeStart = currentResize.edge === 'left';
let daysDiff: number;
if (allDayEventResizingBeforeStart) {
daysDiff = allDayEvent.offset - currentResize.originalOffset;
} else {
daysDiff = allDayEvent.span - currentResize.originalSpan;
}
allDayEvent.offset = currentResize.originalOffset;
allDayEvent.span = currentResize.originalSpan;
const newDates = this.getAllDayEventResizedDates(
allDayEvent.event,
daysDiff,
allDayEventResizingBeforeStart
);
this.eventTimesChanged.emit({
newStart: newDates.start,
newEnd: newDates.end,
event: allDayEvent.event,
type: CalendarEventTimesChangedEventType.Resize,
});
this.allDayEventResizes.delete(allDayEvent);
}
}
/**
* @hidden
*/
getDayColumnWidth(eventRowContainer: HTMLElement): number {
return Math.floor(eventRowContainer.offsetWidth / this.days.length);
}
/**
* @hidden
*/
dateDragEnter(date: Date) {
this.lastDragEnterDate = date;
}
/**
* @hidden
*/
eventDropped(
dropEvent: Pick<
DropEvent<{ event?: CalendarEvent; calendarId?: symbol }>,
'dropData'
>,
date: Date,
allDay: boolean
): void {
if (
shouldFireDroppedEvent(dropEvent, date, allDay, this.calendarId) &&
this.lastDragEnterDate.getTime() === date.getTime() &&
(!this.snapDraggedEvents ||
dropEvent.dropData.event !== this.lastDraggedEvent)
) {
this.eventTimesChanged.emit({
type: CalendarEventTimesChangedEventType.Drop,
event: dropEvent.dropData.event,
newStart: date,
allDay,
});
}
this.lastDraggedEvent = null;
}
/**
* @hidden
*/
dragEnter(type: 'allDay' | 'time') {
this.eventDragEnterByType[type]++;
}
/**
* @hidden
*/
dragLeave(type: 'allDay' | 'time') {
this.eventDragEnterByType[type]--;
}
/**
* @hidden
*/
dragStarted(
eventsContainerElement: HTMLElement,
eventElement: HTMLElement,
event: WeekViewTimeEvent | WeekViewAllDayEvent,
useY: boolean
): void {
this.dayColumnWidth = this.getDayColumnWidth(eventsContainerElement);
const dragHelper: CalendarDragHelper = new CalendarDragHelper(
eventsContainerElement,
eventElement
);
this.validateDrag = ({ x, y, transform }) => {
const isAllowed =
this.allDayEventResizes.size === 0 &&
this.timeEventResizes.size === 0 &&
dragHelper.validateDrag({
x,
y,
snapDraggedEvents: this.snapDraggedEvents,
dragAlreadyMoved: this.dragAlreadyMoved,
transform,
});
if (isAllowed && this.validateEventTimesChanged) {
const newEventTimes = this.getDragMovedEventTimes(
event,
{ x, y },
this.dayColumnWidth,
useY
);
return this.validateEventTimesChanged({
type: CalendarEventTimesChangedEventType.Drag,
event: event.event,
newStart: newEventTimes.start,
newEnd: newEventTimes.end,
});
}
return isAllowed;
};
this.dragActive = true;
this.dragAlreadyMoved = false;
this.lastDraggedEvent = null;
this.eventDragEnterByType = {
allDay: 0,
time: 0,
};
if (!this.snapDraggedEvents && useY) {
this.view.hourColumns.forEach((column) => {
const linkedEvent = column.events.find(
(columnEvent) =>
columnEvent.event === event.event && columnEvent !== event
);
// hide any linked events while dragging
if (linkedEvent) {
linkedEvent.width = 0;
linkedEvent.height = 0;
}
});
}
this.cdr.markForCheck();
}
/**
* @hidden
*/
dragMove(dayEvent: WeekViewTimeEvent, dragEvent: DragMoveEvent) {
const newEventTimes = this.getDragMovedEventTimes(
dayEvent,
dragEvent,
this.dayColumnWidth,
true
);
const originalEvent = dayEvent.event;
const adjustedEvent = { ...originalEvent, ...newEventTimes };
const tempEvents = this.events.map((event) => {
if (event === originalEvent) {
return adjustedEvent;
}
return event;
});
this.restoreOriginalEvents(
tempEvents,
new Map([[adjustedEvent, originalEvent]]),
this.snapDraggedEvents
);
this.dragAlreadyMoved = true;
}
/**
* @hidden
*/
allDayEventDragMove() {
this.dragAlreadyMoved = true;
}
/**
* @hidden
*/
dragEnded(
weekEvent: WeekViewAllDayEvent | WeekViewTimeEvent,
dragEndEvent: DragEndEvent,
dayWidth: number,
useY = false
): void {
this.view = this.getWeekView(this.events);
this.dragActive = false;
this.validateDrag = null;
const { start, end } = this.getDragMovedEventTimes(
weekEvent,
dragEndEvent,
dayWidth,
useY
);
if (
(this.snapDraggedEvents ||
this.eventDragEnterByType[useY ? 'time' : 'allDay'] > 0) &&
isDraggedWithinPeriod(start, end, this.view.period)
) {
this.lastDraggedEvent = weekEvent.event;
this.eventTimesChanged.emit({
newStart: start,
newEnd: end,
event: weekEvent.event,
type: CalendarEventTimesChangedEventType.Drag,
allDay: !useY,
});
}
}
protected refreshHeader(): void {
this.days = this.utils.getWeekViewHeader({
viewDate: this.viewDate,
weekStartsOn: this.weekStartsOn,
excluded: this.excludeDays,
weekendDays: this.weekendDays,
...getWeekViewPeriod(
this.dateAdapter,
this.viewDate,
this.weekStartsOn,
this.excludeDays,
this.daysInWeek
),
});
}
protected refreshBody(): void {
this.view = this.getWeekView(this.events);
}
protected refreshAll(): void {
this.refreshHeader();
this.refreshBody();
this.emitBeforeViewRender();
}
protected emitBeforeViewRender(): void {
if (this.days && this.view) {
this.beforeViewRender.emit({
header: this.days,
...this.view,
});
}
}
protected getWeekView(events: CalendarEvent[]) {
return this.utils.getWeekView({
events,
viewDate: this.viewDate,
weekStartsOn: this.weekStartsOn,
excluded: this.excludeDays,
precision: this.precision,
absolutePositionedEvents: true,
hourSegments: this.hourSegments,
hourDuration: this.hourDuration,
dayStart: {
hour: this.dayStartHour,
minute: this.dayStartMinute,
},
dayEnd: {
hour: this.dayEndHour,
minute: this.dayEndMinute,
},
segmentHeight: this.hourSegmentHeight,
weekendDays: this.weekendDays,
minimumEventHeight: this.minimumEventHeight,
...getWeekViewPeriod(
this.dateAdapter,
this.viewDate,
this.weekStartsOn,
this.excludeDays,
this.daysInWeek
),
});
}
protected getDragMovedEventTimes(
weekEvent: WeekViewAllDayEvent | WeekViewTimeEvent,
dragEndEvent: DragEndEvent | DragMoveEvent,
dayWidth: number,
useY: boolean
) {
const daysDragged =
(roundToNearest(dragEndEvent.x, dayWidth) / dayWidth) *
(this.rtl ? -1 : 1);
const minutesMoved = useY
? getMinutesMoved(
dragEndEvent.y,
this.hourSegments,
this.hourSegmentHeight,
this.eventSnapSize,
this.hourDuration
)
: 0;
const start = this.dateAdapter.addMinutes(
addDaysWithExclusions(
this.dateAdapter,
weekEvent.event.start,
daysDragged,
this.excludeDays
),
minutesMoved
);
let end: Date;
if (weekEvent.event.end) {
end = this.dateAdapter.addMinutes(
addDaysWithExclusions(
this.dateAdapter,
weekEvent.event.end,
daysDragged,
this.excludeDays
),
minutesMoved
);
}
return { start, end };
}
protected restoreOriginalEvents(
tempEvents: CalendarEvent[],
adjustedEvents: Map<CalendarEvent, CalendarEvent>,
snapDraggedEvents = true
) {
const previousView = this.view;
if (snapDraggedEvents) {
this.view = this.getWeekView(tempEvents);
}
const adjustedEventsArray = tempEvents.filter((event) =>
adjustedEvents.has(event)
);
this.view.hourColumns.forEach((column, columnIndex) => {
previousView.hourColumns[columnIndex].hours.forEach((hour, hourIndex) => {
hour.segments.forEach((segment, segmentIndex) => {
column.hours[hourIndex].segments[segmentIndex].cssClass =
segment.cssClass;
});
});
adjustedEventsArray.forEach((adjustedEvent) => {
const originalEvent = adjustedEvents.get(adjustedEvent);
const existingColumnEvent = column.events.find(
(columnEvent) =>
columnEvent.event ===
(snapDraggedEvents ? adjustedEvent : originalEvent)
);
if (existingColumnEvent) {
// restore the original event so trackBy kicks in and the dom isn't changed
existingColumnEvent.event = originalEvent;
existingColumnEvent['tempEvent'] = adjustedEvent;
if (!snapDraggedEvents) {
existingColumnEvent.height = 0;
existingColumnEvent.width = 0;
}
} else {
// add a dummy event to the drop so if the event was removed from the original column the drag doesn't end early
const event = {
event: originalEvent,
left: 0,
top: 0,
height: 0,
width: 0,
startsBeforeDay: false,
endsAfterDay: false,
tempEvent: adjustedEvent,
};
column.events.push(event);
}
});
});
adjustedEvents.clear();
}
protected getTimeEventResizedDates(
calendarEvent: CalendarEvent,
resizeEvent: ResizeEvent
) {
const newEventDates = {
start: calendarEvent.start,
end: getDefaultEventEnd(
this.dateAdapter,
calendarEvent,
this.minimumEventHeight
),
};
const { end, ...eventWithoutEnd } = calendarEvent;
const smallestResizes = {
start: this.dateAdapter.addMinutes(
newEventDates.end,
this.minimumEventHeight * -1
),
end: getDefaultEventEnd(
this.dateAdapter,
eventWithoutEnd,
this.minimumEventHeight
),
};
const modifier = this.rtl ? -1 : 1;
if (typeof resizeEvent.edges.left !== 'undefined') {
const daysDiff =
Math.round(+resizeEvent.edges.left / this.dayColumnWidth) * modifier;
const newStart = addDaysWithExclusions(
this.dateAdapter,
newEventDates.start,
daysDiff,
this.excludeDays
);
if (newStart < smallestResizes.start) {
newEventDates.start = newStart;
} else {
newEventDates.start = smallestResizes.start;
}
} else if (typeof resizeEvent.edges.right !== 'undefined') {
const daysDiff =
Math.round(+resizeEvent.edges.right / this.dayColumnWidth) * modifier;
const newEnd = addDaysWithExclusions(
this.dateAdapter,
newEventDates.end,
daysDiff,
this.excludeDays
);
if (newEnd > smallestResizes.end) {
newEventDates.end = newEnd;
} else {
newEventDates.end = smallestResizes.end;
}
}
if (typeof resizeEvent.edges.top !== 'undefined') {
const minutesMoved = getMinutesMoved(
resizeEvent.edges.top as number,
this.hourSegments,
this.hourSegmentHeight,
this.eventSnapSize,
this.hourDuration
);
const newStart = this.dateAdapter.addMinutes(
newEventDates.start,
minutesMoved
);
if (newStart < smallestResizes.start) {
newEventDates.start = newStart;
} else {
newEventDates.start = smallestResizes.start;
}
} else if (typeof resizeEvent.edges.bottom !== 'undefined') {
const minutesMoved = getMinutesMoved(
resizeEvent.edges.bottom as number,
this.hourSegments,
this.hourSegmentHeight,
this.eventSnapSize,
this.hourDuration
);
const newEnd = this.dateAdapter.addMinutes(
newEventDates.end,
minutesMoved
);
if (newEnd > smallestResizes.end) {
newEventDates.end = newEnd;
} else {
newEventDates.end = smallestResizes.end;
}
}
return newEventDates;
}
protected resizeStarted(
eventsContainer: HTMLElement,
event: WeekViewTimeEvent | WeekViewAllDayEvent,
dayWidth?: number
) {
this.dayColumnWidth = this.getDayColumnWidth(eventsContainer);
const resizeHelper = new CalendarResizeHelper(
eventsContainer,
dayWidth,
this.rtl
);
this.validateResize = ({ rectangle, edges }) => {
const isWithinBoundary = resizeHelper.validateResize({
rectangle: { ...rectangle },
edges,
});
if (isWithinBoundary && this.validateEventTimesChanged) {
let newEventDates;
if (!dayWidth) {
newEventDates = this.getTimeEventResizedDates(event.event, {
rectangle,
edges,
});
} else {
const modifier = this.rtl ? -1 : 1;
if (typeof edges.left !== 'undefined') {
const diff = Math.round(+edges.left / dayWidth) * modifier;
newEventDates = this.getAllDayEventResizedDates(
event.event,
diff,
!this.rtl
);
} else {
const diff = Math.round(+edges.right / dayWidth) * modifier;
newEventDates = this.getAllDayEventResizedDates(
event.event,
diff,
this.rtl
);
}
}
return this.validateEventTimesChanged({
type: CalendarEventTimesChangedEventType.Resize,
event: event.event,
newStart: newEventDates.start,
newEnd: newEventDates.end,
});
}
return isWithinBoundary;
};
this.cdr.markForCheck();
}
/**
* @hidden
*/
protected getAllDayEventResizedDates(
event: CalendarEvent,
daysDiff: number,
beforeStart: boolean
) {
let start: Date = event.start;
let end: Date = event.end || event.start;
if (beforeStart) {
start = addDaysWithExclusions(
this.dateAdapter,
start,
daysDiff,
this.excludeDays
);
} else {
end = addDaysWithExclusions(
this.dateAdapter,
end,
daysDiff,
this.excludeDays
);
}
return { start, end };
}
}