· 6 years ago · Apr 17, 2020, 12:20 PM
1import {
2 OnInit,
3 Directive,
4 ViewContainerRef,
5 Renderer2,
6 ElementRef,
7 HostListener,
8 ChangeDetectorRef,
9} from '@angular/core';
10import { takeUntil, tap, skipWhile, take, switchMap, debounceTime } from 'rxjs/operators';
11import * as moment from 'moment';
12import { BsModalRef } from 'ngx-bootstrap';
13import { Point } from '@angular/cdk/drag-drop/typings/drag-ref';
14
15import {
16 W6GanttBaseObjectComponent
17} from '@pages/w6.click-schedule.schedule/w6.click-schedule.containers/w6.click-gantt/w6.click-gantt-objects/w6-gantt-base-object.component';
18import {
19 W6GanttObjectsFacade,
20 W6GanttRowFacade
21} from '@pages/w6.click-schedule.schedule/w6.click-schedule.facades';
22import {
23 ObjectDragData,
24 ObjectDragAssignedEngineer,
25 W6GanttObjectType
26} from '@pages/w6.click-schedule.schedule/w6.click-schedule.containers/w6.click-gantt/w6.click-gantt.models/w6.click-gantt-objects/w6-base-gantt-date-object';
27import { W6FormFacade } from '../../w6.click-schedule.core/w6.click-schedule.facades/w6.click-schedule.form-facade/w6-form.facade';
28import { W6MetadataService, W6DragAndDropService } from '@services';
29import { W6ResourceGanttRowSyncService } from '@pages/w6.click-schedule.schedule/w6.click-schedule.containers/w6.click-gantt/w6.click-services/w6-resource-gantt-row-sync.service';
30import { DragAndDropObject, SnapType } from 'src/w6.click-schedule.app/w6.click-schedule.core/w6.click-schedule.models/w6.click-schedule.drag.and.drop';
31import { GanttScrollDirections } from '@services/w6-drag-and-drop.service';
32import { Subject, interval } from 'rxjs';
33import { W6ResourceGanttTaskSharedFacade } from '@pages/w6.click-schedule.schedule/w6.click-schedule.facades/w6.click-schedual.shared.facade/w6-resource-gantt-task-shared.facade';
34
35const DRAGGABLE_OBJECT_CLASS = 'draggable-object';
36const DATE_FORMATS = {
37 DATE_ONLY: 'DD/MM/YYYY',
38 DATE_WITH_TIME: 'DD/MM/YYYY HH:mm',
39 SERVER_FORMAT: 'YYYY-MM-DD[T]HH:mm:ss'
40};
41
42
43@Directive({
44 selector: '[clickDraggableObject]'
45})
46export class W6GanttDraggableDirective implements OnInit {
47
48 private readonly GANTT_SCROLL_INTERVAL_MS = 500;
49 private readonly GANTT_SCROLL_DISTANCE_X_PX = 100;
50 private readonly GANTT_SCROLL_DISTANCE_Y_PX = 40;
51
52 private initialEngineerIndex = -1;
53 private currentScrollDirection: GanttScrollDirections | null;
54 private scrollGanttHandler: Subject<GanttScrollDirections | null> = new Subject();
55
56 constructor(
57 private readonly viewContainerRef: ViewContainerRef,
58 private readonly ganttObjectsFacade: W6GanttObjectsFacade,
59 private readonly ganttRowFacade: W6GanttRowFacade,
60 private readonly renderer: Renderer2,
61 private readonly hostElementRef: ElementRef<HTMLDivElement>,
62 private readonly formFacade: W6FormFacade,
63 private readonly metadataService: W6MetadataService,
64 private readonly ganttResourceRowSyncService: W6ResourceGanttRowSyncService,
65 private readonly dragAndDropSerivce: W6DragAndDropService,
66 private readonly sharedFacade: W6ResourceGanttTaskSharedFacade,
67 private readonly changeDetectorRef: ChangeDetectorRef
68 ) { }
69
70 private get host(): W6GanttBaseObjectComponent {
71 return this.viewContainerRef['_data'].componentView.component;
72 }
73
74 public ngOnInit(): void {
75 this.host.initDraggable$
76 .asObservable()
77 .pipe(
78 takeUntil(this.host.destroy$),
79 tap(() => {
80 this.initDraggable();
81 this.dragAndDropSerivce.pointerOffsetY = 0;
82 this.dragAndDropSerivce.createGanttReflection();
83 })
84 )
85 .subscribe();
86 this.handleGanttScrollWhenOutOfBounds();
87 }
88
89 private handleGanttScrollWhenOutOfBounds(): void {
90 this.scrollGanttHandler
91 .pipe(
92 skipWhile(direction => !this.host.dragRef.isDragging() || this.currentScrollDirection === direction),
93 tap((direction) => {
94 this.currentScrollDirection = direction;
95 }),
96 debounceTime(400),
97 switchMap(() =>
98 interval(this.GANTT_SCROLL_INTERVAL_MS)
99 .pipe(
100 skipWhile(() => !this.host.dragRef.isDragging()),
101 takeUntil(this.host.destroy$),
102 )
103 ),
104 takeUntil(this.host.destroy$),
105 ).subscribe(() => {
106 switch (this.currentScrollDirection) {
107 case GanttScrollDirections.RIGHT: {
108 this.sharedFacade.updateGanttScrollerPositionDeltaX.next(this.GANTT_SCROLL_DISTANCE_X_PX);
109 break;
110 }
111 case GanttScrollDirections.LEFT: {
112 this.sharedFacade.updateGanttScrollerPositionDeltaX.next(this.GANTT_SCROLL_DISTANCE_X_PX * -1);
113 break;
114 }
115 case GanttScrollDirections.TOP: {
116 let positionY = this.dragAndDropSerivce.restrictiveContainer.scrollTop - this.GANTT_SCROLL_DISTANCE_Y_PX;
117 if (positionY < 0) { positionY = 0; }
118 this.ganttObjectsFacade.perfectScrollbarDirective.scrollToY(positionY);
119 this.ganttObjectsFacade.perfectScrollbarDirective.update();
120 break;
121 }
122 case GanttScrollDirections.BOTTOM: {
123 const positionY = this.dragAndDropSerivce.restrictiveContainer.scrollTop + this.GANTT_SCROLL_DISTANCE_Y_PX;
124 this.ganttObjectsFacade.perfectScrollbarDirective.scrollToY(positionY);
125 this.ganttObjectsFacade.perfectScrollbarDirective.update();
126 break;
127 }
128 }
129 this.host.changeDetectorRef.detectChanges();
130 });
131 }
132
133 private initDraggable(): void {
134 /* Initialize drag listeners on object. */
135 if (!this.host.dragRef) { return; }
136 this.setDragConstrains();
137 this.initBeforeDragStart();
138 this.initDragStarted(this.renderer, this.hostElementRef);
139 this.initDragMove();
140 this.initDragEnded();
141 }
142
143 private setDragOverlapConstrains(
144 { x, y }: Point,
145 engineerIndex: number,
146 scrollTop: number
147 ): Point | null {
148 /* Handles Y Axis constrains when hovering upon an expanded overlap object. */
149 const { GANTT_ROW_HEIGHT } = this.ganttRowFacade;
150 const { height, offsetYStart } = this.dragAndDropSerivce.getRowReflection(engineerIndex);
151 const numberOfOverlapRows = height / GANTT_ROW_HEIGHT;
152 const rowIndex = Math.floor((y - (offsetYStart - scrollTop)) / GANTT_ROW_HEIGHT);
153 if (rowIndex < 0 || rowIndex > (numberOfOverlapRows - 1)) {
154 return null;
155 }
156 const deltaY = offsetYStart - scrollTop + (rowIndex * GANTT_ROW_HEIGHT) + (this.dragAndDropSerivce.pointerOffsetY / 2);
157 return { x, y: deltaY + 11 };
158 }
159
160 private setDragConstrains(): void {
161 /* Handles Y Axis constrains when activating drag and drop functionality. */
162 this.host.dragRef.constrainPosition = ({ x: clientX, y: clientY }: Point): Point => {
163 const { scrollTop, scrollLeft } = this.dragAndDropSerivce;
164 const [firstVisibleRow, lastVisibleRow] = this.visibleRows;
165 const engineerIndex = this.dragAndDropSerivce.getEngineerIndex(clientY)
166 const isExpanded = this.ganttResourceRowSyncService.isExpanded(engineerIndex);
167 if (isExpanded) {
168 const point = this.setDragOverlapConstrains({ x: clientX, y: clientY }, engineerIndex, scrollTop);
169 // incase the object was dragged outside of overlap area, continue as usual.
170 if (point) { return point; }
171 }
172 if (engineerIndex >= firstVisibleRow && engineerIndex <= lastVisibleRow) {
173 const { offsetYStart, height: rowHeight } = this.dragAndDropSerivce.getRowReflection(engineerIndex)
174 const x = clientX - scrollLeft;
175 const y = offsetYStart - scrollTop + (this.dragAndDropSerivce.pointerOffsetY - (rowHeight / 2)) + rowHeight - 10;
176 return { x, y };
177 }
178 return { x: clientX - scrollLeft, y: clientY - scrollTop };
179
180 }
181 }
182
183 private initBeforeDragStart(): void {
184 /*
185 Handles 'BeforeDragStart' event.
186 The whole purpose of this function is to create an updated reflection of the current gantt state
187 for easier manipulation and calculations.
188 */
189 this.host.dragRef.beforeStarted
190 .asObservable()
191 .pipe(
192 tap(() => {
193 const { allEngineers } = this.ganttObjectsFacade.ganttData;
194 this.initialEngineerIndex = allEngineers.findIndex(engineer => engineer['Key'] === this.host.object.assignedEngineerKey);
195 }),
196 takeUntil(this.host.destroy$),
197 ).subscribe();
198 }
199
200 private initDragStarted(renderer: Renderer2, elementRef: ElementRef<HTMLDivElement>): void {
201 /*
202 Handles 'DragStart' event.
203 Initializing dragRef.data with the current assigned engineer, start time, end time
204 and adding 'draggable-object' class for visuals.
205 */
206 this.host.dragRef.started
207 .asObservable()
208 .pipe(
209 tap(() => {
210 this.dragAndDropSerivce.dragRefKey = this.host.object.key;
211 this.dragAndDropSerivce.saveScrollOffset();
212 renderer.addClass(elementRef.nativeElement, DRAGGABLE_OBJECT_CLASS);
213 this.host.changeDetectorRef.detectChanges();
214 const { allEngineers } = this.ganttObjectsFacade.ganttData;
215 const initialEngineer = allEngineers[this.initialEngineerIndex];
216 this.host.dragRef.data = {
217 initialEngineerIndex: this.initialEngineerIndex,
218 outOfBounds: false,
219 initial: {
220 assignedEngineer: {
221 key: initialEngineer['Key'],
222 ID: initialEngineer['ID'],
223 displayString: initialEngineer['Name']
224 },
225 startTime: this.host.object.start.clone(),
226 finishTime: this.host.object.finish.clone(),
227 }
228 } as ObjectDragData;
229 }),
230 takeUntil(this.host.destroy$),
231 ).subscribe();
232 }
233
234 private initDragMove(): void {
235 /*
236 Handles 'DragMove' event.
237 With client's cursor coordinates, determine which new engineer the object is going to be assigned to,
238 and in which timelap the object will be placed and update dragRef.data.
239 */
240 this.host.dragRef.moved
241 .pipe(
242 tap(({ distance, event }) => {
243 this.dragAndDropSerivce.dragRefBoundingRect = this.hostElementRef.nativeElement.getBoundingClientRect();
244 this.dragAndDropSerivce.createGanttReflection();
245 const { clientX, clientY } = event as MouseEvent;
246 const isOutOfBounds = this.dragAndDropSerivce.isOutOfBounds({ x: clientX, y: clientY });
247 this.host.dragRef.data.outOfBounds = isOutOfBounds;
248 if (isOutOfBounds) {
249 return this.scrollGantt(clientX, clientY);
250 }
251 this.currentScrollDirection = null;
252 const { x } = distance;
253 const newDateRage = this.getNewObjectDates(this.host.dragRef.data, x);
254
255 const newAssignedEngineer = this.getNewAssignedEngineer(clientY);
256 if (!newDateRage || !newAssignedEngineer) { return; }
257 const [startTime, finishTime] = newDateRage;
258 this.host.dragRef.data.updated = {
259 startTime,
260 finishTime,
261 assignedEngineer: newAssignedEngineer
262 };
263 }),
264 takeUntil(this.host.destroy$)
265 )
266 .subscribe();
267 }
268
269 private initDragEnded(): void {
270 /*
271 Handles 'DragEnd' event.
272 When object is 'dropped' submit changes.
273 */
274 this.host.dragRef.ended
275 .asObservable()
276 .pipe(
277 tap(() => {
278 this.host.changeDetectorRef.detectChanges();
279 }),
280 skipWhile(() => !this.host.dragRef.data.updated),
281 tap(() => this.updateObject(this.host.dragRef.data)),
282 takeUntil(this.host.destroy$),
283 ).subscribe();
284 }
285
286 private getNewObjectDates({ initial }: ObjectDragData, deltaX: number): [moment.Moment, moment.Moment] | void {
287 /* Caluculate new object time (start + finish) based on how much the object traveled on the x axis. */
288 const { ganttDatesRepresentation } = this.ganttObjectsFacade.ganttGridFacade;
289 const { headerWidth, selectedDates } = ganttDatesRepresentation;
290 const { startTime, finishTime } = initial;
291 const { scrollLeft } = this.dragAndDropSerivce.restrictiveContainer;
292
293 const duration = finishTime.diff(startTime, 'seconds');
294 const startDay = selectedDates.find(date =>
295 date.currentDay.format(DATE_FORMATS.DATE_ONLY) === startTime.format(DATE_FORMATS.DATE_ONLY));
296 const { hoursRange } = startDay.daySettings;
297
298 // Time Configurations
299 const dayWidth = headerWidth / selectedDates.length;
300 const hourWidth = dayWidth / (hoursRange + 1);
301 const minutesWidth = hourWidth / 60;
302 const secondsWidth = minutesWidth / 60;
303
304 let leftPosition = +this.host.object.leftIndentPx.replace('px', '');
305 if (this.host.overlapObject) {
306 // If object is part of overlap (expanded) add parent's left indent (positions are relative)
307 leftPosition += +this.host.overlapObject.leftIndentPx.replace('px', '');
308 }
309 leftPosition += (deltaX + scrollLeft);
310 const newDayIndex = Math.floor(leftPosition / dayWidth);
311 let newDate = selectedDates[newDayIndex];
312
313 if (!newDate) {
314 this.host.dragRef.data.outOfBounds = true;
315 newDate = { ...startDay };
316 }
317
318 const updatedStartTime = moment(
319 newDate.daySettings.startHour
320 ).add(
321 Math.ceil(leftPosition - (dayWidth * newDayIndex)) / secondsWidth,
322 'seconds'
323 );
324
325 const updatedFinishTime = updatedStartTime.clone().add(duration, 'seconds');
326 return [updatedStartTime, updatedFinishTime];
327 }
328
329 private getNewAssignedEngineer(clientY: number): ObjectDragAssignedEngineer | void {
330 /* Caluculate new object assignee based on client Y cursor position. */
331 const { allEngineers } = this.ganttObjectsFacade.ganttData;
332 const index = this.dragAndDropSerivce.getEngineerIndex(clientY);
333 const [firstVisibleRow, lastVisibleRow] = this.visibleRows;
334 if (index > lastVisibleRow || index < firstVisibleRow) {
335 this.host.dragRef.data.outOfBounds = true;
336 return this.host.dragRef.data.initial.assignedEngineer;
337 }
338 const engineerObject = allEngineers[index];
339 return { key: engineerObject['Key'], ID: engineerObject['ID'], displayString: engineerObject['Name'] };
340 }
341
342 private updateObject({ updated }: ObjectDragData): void {
343 /* Submit updated object's data */
344 const { outOfBounds } = this.host.dragRef.data;
345 if (outOfBounds) {
346 return this.resetDraggable();
347 }
348 const { finishTime, assignedEngineer, startTime } = updated;
349 const { key } = this.host.object;
350
351 const objectType = this.host.object.type === W6GanttObjectType.Assignment ?
352 this.metadataService.taskAssignmentId :
353 this.metadataService.assignmentMetadata.ID;
354
355 const dragAndDropObject: DragAndDropObject = {
356 key: this.host.object.type === W6GanttObjectType.Assignment ? '-1' : key,
357 start: startTime,
358 finish: finishTime,
359 snap: { type: SnapType.NONE },
360 assignedEnginners: [],
361 revision: -1,
362 taskKey: this.host.object.type === W6GanttObjectType.Assignment ? key : '-1'
363 };
364
365 dragAndDropObject.assignedEnginners = [{
366 key: assignedEngineer.key,
367 displayString: assignedEngineer.displayString
368 }];
369
370 const onConfirm = (modalRef: BsModalRef): void => {
371 // Callback for confirm rule violations.
372 modalRef.hide();
373 this.submitDragChanges(dragAndDropObject);
374 };
375
376 // Callback for dismiss rule violations.
377 const onCancel = (): void => this.resetDraggable();
378
379 this.formFacade.getObjectData(objectType, key)
380 .pipe(
381 take(1),
382 switchMap((data) => {
383 dragAndDropObject.revision = data['Revision'];
384 if (data['Assignment_Key']) {
385 dragAndDropObject.key = data['Assignment_Key'];
386 }
387 // Check objects rules violations based on new dates and new engineer(s).
388 return this.dragAndDropSerivce.checkRulesViolation(dragAndDropObject);
389 }),
390 )
391 .subscribe((response) => {
392 if (response.hasError) {
393 this.formFacade.showErrorModal(response.error);
394 this.resetDraggable();
395 } else if (response.violations.length !== 0) {
396 this.formFacade.showRuleViolationsModal(response.violations, onCancel.bind(this), onConfirm.bind(this));
397 } else if (response.violations.length === 0) {
398 this.submitDragChanges(dragAndDropObject);
399 }
400 });
401 }
402
403 private submitDragChanges(object: DragAndDropObject): void {
404 /* Handles API call to submit the changes. */
405 const { assignedEngineer, startTime, finishTime } = this.host.dragRef.data.initial;
406 const initialFormData = {};
407 initialFormData['Start'] = startTime.format(DATE_FORMATS.SERVER_FORMAT);
408 initialFormData['Finish'] = finishTime.format(DATE_FORMATS.SERVER_FORMAT);
409 initialFormData['AssignedEngineerKey'] = assignedEngineer.key;
410 if (this.host.object.type === W6GanttObjectType.Na) {
411 initialFormData['Engineers'] = this.host.object['Engineers'];
412 }
413 this.dragAndDropSerivce.updateObject(object, initialFormData)
414 .pipe(
415 take(1),
416 tap((response) => {
417 if (response.hasError) {
418 // Callback for dismiss error modal
419 const confirmCallback = (): void => this.resetDraggable();
420 this.formFacade.showErrorModal(response.error, confirmCallback);
421 }
422 })
423 )
424 .subscribe(() => this.resetDraggable());
425 }
426
427 private get visibleRows(): [number, number] {
428 /* Get the current visible rows in gantt's viewport. */
429 const { GANTT_ROW_HEIGHT } = this.ganttRowFacade;
430 const { height } = this.dragAndDropSerivce.headerContainer.getBoundingClientRect();
431 const { offsetHeight, scrollTop } = this.dragAndDropSerivce.container;
432 const firstVisibleRow = Math.floor(scrollTop / GANTT_ROW_HEIGHT);
433 const lastVisibleRow = firstVisibleRow + Math.floor((offsetHeight - height) / GANTT_ROW_HEIGHT);
434 return [firstVisibleRow, lastVisibleRow];
435 }
436
437 private scrollGantt(clientX: number, clientY: number): void {
438 const direction = this.dragAndDropSerivce.getScrollDirection(clientX, clientY);
439 this.scrollGanttHandler.next(direction);
440 }
441
442 @HostListener('pointerdown', ['$event'])
443 private onPointerDown($event: PointerEvent): void {
444 this.dragAndDropSerivce.pointerOffsetY = $event.offsetY;
445 if (($event.target as HTMLElement).classList.contains('ellipsis')) {
446 this.dragAndDropSerivce.pointerOffsetY += 5;
447 }
448 }
449
450 private resetDraggable(): void {
451 /* Reset drag element to it's default position. */
452 this.host.dragRef.reset();
453 this.dragAndDropSerivce.dragRefBoundingRect = undefined;
454 this.dragAndDropSerivce.dragRefKey = undefined;
455 this.dragAndDropSerivce.snapTo = { type: SnapType.NONE };
456 this.dragAndDropSerivce.pointerOffsetY = 0;
457 this.dragAndDropSerivce.clearScrollOffset();
458 this.renderer.removeClass(this.hostElementRef.nativeElement, DRAGGABLE_OBJECT_CLASS);
459 this.host.changeDetectorRef.detectChanges();
460 this.changeDetectorRef.detectChanges();
461
462 }
463}