Skip to content

Commit cbe67e4

Browse files
feat: add sync scroll directive #TINFR-2346
1 parent 1417a5b commit cbe67e4

15 files changed

+228
-111
lines changed

example/src/app/gantt-advanced/component/flat.component.html

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,19 @@
1010
</div>
1111
</ng-template>
1212
<ng-template #mainTemplate>
13-
<div class="gantt-main-container">
14-
<!-- groups -->
15-
<div class="gantt-main-groups" *ngIf="groups && groups.length > 0" [style.width.px]="view.width">
16-
<ng-container *ngFor="let group of groups; trackBy: trackBy">
17-
<div class="gantt-main-group" [style.height.px]="group.mergedItems?.length * (styles.lineHeight + 10) - 10">
18-
<ng-container *ngFor="let items of group.mergedItems">
19-
<div class="gantt-flat-items" [style.height.px]="styles.lineHeight">
20-
<ng-container *ngFor="let item of items; trackBy: trackBy">
21-
<ngx-gantt-bar [item]="item" [template]="barTemplate" (barClick)="barClick.emit($event)"></ngx-gantt-bar>
22-
</ng-container>
23-
</div>
24-
</ng-container>
25-
</div>
26-
</ng-container>
27-
</div>
13+
<!-- groups -->
14+
<div class="gantt-main-groups" *ngIf="groups && groups.length > 0" [style.width.px]="view.width">
15+
<ng-container *ngFor="let group of groups; trackBy: trackBy">
16+
<div class="gantt-main-group" [style.height.px]="group.mergedItems?.length * (styles.lineHeight + 10) - 10">
17+
<ng-container *ngFor="let items of group.mergedItems">
18+
<div class="gantt-flat-items" [style.height.px]="styles.lineHeight">
19+
<ng-container *ngFor="let item of items; trackBy: trackBy">
20+
<ngx-gantt-bar [item]="item" [template]="barTemplate" (barClick)="barClick.emit($event)"></ngx-gantt-bar>
21+
</ng-container>
22+
</div>
23+
</ng-container>
24+
</div>
25+
</ng-container>
2826
</div>
2927
</ng-template>
3028
</ngx-gantt-root>

packages/gantt/src/components/scrollbar/scrollbar.component.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44
[style.height.px]="ganttRoot?.horizontalScrollbarHeight + 1"
55
[style.right.px]="ganttRoot?.verticalScrollbarWidth"
66
>
7-
<div class="gantt-table-scrollbar" [class.with-scrollbar]="ganttRoot?.horizontalScrollbarHeight" [style.width.px]="tableWidth">
7+
<div
8+
class="gantt-table-scrollbar"
9+
syncScrollX="ganttTableXScroll"
10+
[class.with-scrollbar]="ganttRoot?.horizontalScrollbarHeight"
11+
[style.width.px]="tableWidth"
12+
>
813
<div class="h-100" [style.width.px]="ganttRoot.ganttUpper['ganttTableBody']?.elementRef?.nativeElement?.offsetWidth - 1 || 0"></div>
914
</div>
10-
<div class="gantt-main-scrollbar">
15+
<div class="gantt-main-scrollbar" syncScrollX="ganttMainXScroll">
1116
<div class="h-100" [style.width.px]="ganttRoot['view']?.width"></div>
1217
</div>
1318
</div>

packages/gantt/src/components/scrollbar/scrollbar.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { Component, Inject, Input } from '@angular/core';
22
import { GANTT_UPPER_TOKEN, GanttUpper } from '../../gantt-upper';
33
import { NgClass } from '@angular/common';
44
import { NgxGanttRootComponent } from '../../root.component';
5+
import { GanttSyncScrollXDirective } from '../../directives/sync-scroll.directive';
56

67
@Component({
78
selector: 'gantt-scrollbar',
89
templateUrl: `./scrollbar.component.html`,
9-
imports: [NgClass]
10+
imports: [NgClass, GanttSyncScrollXDirective]
1011
})
1112
export class GanttScrollbarComponent {
1213
@Input() hasFooter: boolean = false;

packages/gantt/src/components/table/body/gantt-table-body.component.ts

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@ import {
1010
HostBinding,
1111
Inject,
1212
Input,
13-
NgZone,
1413
OnDestroy,
1514
OnInit,
1615
Output,
1716
QueryList,
1817
TemplateRef,
1918
ViewChildren
2019
} from '@angular/core';
21-
import { auditTime, filter, fromEvent, merge, startWith, Subject, takeUntil } from 'rxjs';
20+
import { auditTime, filter, startWith, Subject, takeUntil } from 'rxjs';
2221
import {
2322
GanttGroupInternal,
2423
GanttItemInternal,
@@ -33,7 +32,6 @@ import { GANTT_ABSTRACT_TOKEN, GanttAbstractComponent } from '../../../gantt-abs
3332
import { GANTT_UPPER_TOKEN, GanttUpper } from '../../../gantt-upper';
3433
import { IsGanttGroupPipe, IsGanttRangeItemPipe } from '../../../gantt.pipe';
3534
import { NgxGanttTableColumnComponent } from '../../../table/gantt-column.component';
36-
import { passiveListenerOptions } from '../../../utils/passive-listeners';
3735
import { GanttIconComponent } from '../../icon/icon.component';
3836
import { defaultColumnWidth } from '../header/gantt-table-header.component';
3937

@@ -108,21 +106,12 @@ export class GanttTableBodyComponent implements OnInit, OnDestroy, AfterViewInit
108106

109107
private destroy$ = new Subject<void>();
110108

111-
public sideContainer: Element;
112-
113-
public headerContainer: Element;
114-
115-
public footerContainer: Element;
116-
117-
public tableScrollbarContainer: Element;
118-
119109
constructor(
120110
@Inject(GANTT_ABSTRACT_TOKEN) public gantt: GanttAbstractComponent,
121111
@Inject(GANTT_UPPER_TOKEN) public ganttUpper: GanttUpper,
122112
private cdr: ChangeDetectorRef,
123113
@Inject(DOCUMENT) private document: Document,
124-
protected elementRef: ElementRef<HTMLElement>,
125-
private ngZone: NgZone
114+
protected elementRef: ElementRef<HTMLElement>
126115
) {}
127116

128117
ngOnInit() {
@@ -141,7 +130,6 @@ export class GanttTableBodyComponent implements OnInit, OnDestroy, AfterViewInit
141130
}
142131

143132
ngAfterViewInit(): void {
144-
this.initialize();
145133
this.cdkDrags.changes
146134
.pipe(startWith(this.cdkDrags), takeUntil(this.destroy$))
147135
.subscribe((drags: QueryList<CdkDrag<GanttItemInternal>>) => {
@@ -166,33 +154,6 @@ export class GanttTableBodyComponent implements OnInit, OnDestroy, AfterViewInit
166154
});
167155
}
168156

169-
initialize() {
170-
this.sideContainer = this.document.getElementsByClassName('gantt-side-container')[0];
171-
this.headerContainer = this.document.getElementsByClassName('gantt-table-header-container')[0];
172-
this.footerContainer = this.document.getElementsByClassName('gantt-table-footer')[0];
173-
this.tableScrollbarContainer = this.document.getElementsByClassName('gantt-table-scrollbar')[0];
174-
this.monitorScrollChange();
175-
}
176-
177-
private monitorScrollChange() {
178-
const scrollObservers = [
179-
fromEvent(this.sideContainer, 'scroll', passiveListenerOptions),
180-
fromEvent(this.headerContainer, 'scroll', passiveListenerOptions),
181-
fromEvent(this.tableScrollbarContainer, 'scroll', passiveListenerOptions)
182-
];
183-
this.ngZone.runOutsideAngular(() =>
184-
merge(...scrollObservers)
185-
.pipe(takeUntil(this.destroy$))
186-
.subscribe((event) => {
187-
const target = event.currentTarget as HTMLElement;
188-
this.headerContainer && (this.headerContainer.scrollLeft = target.scrollLeft);
189-
this.sideContainer && (this.sideContainer.scrollLeft = target.scrollLeft);
190-
this.tableScrollbarContainer && (this.tableScrollbarContainer.scrollLeft = target.scrollLeft);
191-
this.footerContainer && (this.footerContainer.scrollLeft = target.scrollLeft);
192-
})
193-
);
194-
}
195-
196157
expandGroup(group: GanttGroupInternal) {
197158
this.gantt.expandGroup(group);
198159
}

packages/gantt/src/components/table/header/gantt-table-header.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="gantt-table-header-container" [style.width.px]="tableWidth" cdkScrollable>
1+
<div class="gantt-table-header-container" syncScrollX="ganttTableXScroll" [style.width.px]="tableWidth" cdkScrollable>
22
@for (column of columns; track $index) {
33
<div class="gantt-table-column" [style.width]="column.columnWidth">
44
@if (column.headerTemplateRef) {

packages/gantt/src/components/table/header/gantt-table-header.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Subject, takeUntil } from 'rxjs';
1717
import { GANTT_ABSTRACT_TOKEN, GanttAbstractComponent } from '../../../gantt-abstract';
1818
import { NgxGanttTableColumnComponent } from '../../../table/gantt-column.component';
1919
import { setStyleWithVendorPrefix } from '../../../utils/set-style-with-vendor-prefix';
20+
import { GanttSyncScrollXDirective } from '../../../directives/sync-scroll.directive';
2021
export const defaultColumnWidth = 100;
2122
export const minColumnWidth = 80;
2223
interface DragFixedConfig {
@@ -28,7 +29,7 @@ interface DragFixedConfig {
2829
@Component({
2930
selector: 'gantt-table-header',
3031
templateUrl: './gantt-table-header.component.html',
31-
imports: [NgTemplateOutlet, CdkDrag]
32+
imports: [NgTemplateOutlet, CdkDrag, GanttSyncScrollXDirective]
3233
})
3334
export class GanttTableHeaderComponent implements OnInit, OnDestroy {
3435
public dragStartLeft: number;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Directive, ElementRef, inject, input, OnDestroy, OnInit } from '@angular/core';
2+
import { GanttSyncScrollService } from '../gantt-sync-scroll.service';
3+
@Directive({
4+
selector: '[syncScrollX]',
5+
standalone: true
6+
})
7+
export class GanttSyncScrollXDirective implements OnInit, OnDestroy {
8+
readonly syncScrollX = input<string>();
9+
10+
private elementRef = inject(ElementRef<HTMLElement>);
11+
12+
private syncScrollService = inject(GanttSyncScrollService);
13+
14+
constructor() {}
15+
16+
ngOnInit() {
17+
this.syncScrollService.registerScrollEvent(this.syncScrollX(), this.elementRef.nativeElement, 'x');
18+
}
19+
20+
ngOnDestroy() {
21+
this.syncScrollService.unregisterScrollEvent(this.syncScrollX(), this.elementRef.nativeElement);
22+
}
23+
}
24+
25+
@Directive({
26+
selector: '[syncScrollY]',
27+
standalone: true
28+
})
29+
export class GanttSyncScrollYDirective implements OnInit, OnDestroy {
30+
readonly syncScrollY = input<string>();
31+
32+
private syncScrollService = inject(GanttSyncScrollService);
33+
34+
private elementRef = inject(ElementRef<HTMLElement>);
35+
36+
constructor() {}
37+
38+
ngOnInit() {
39+
this.syncScrollService.registerScrollEvent(this.syncScrollY(), this.elementRef.nativeElement, 'y');
40+
}
41+
42+
ngOnDestroy() {
43+
this.syncScrollService.unregisterScrollEvent(this.syncScrollY(), this.elementRef.nativeElement);
44+
}
45+
}

packages/gantt/src/gantt-dom.service.ts

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { ElementRef, Inject, Injectable, NgZone, OnDestroy, PLATFORM_ID, Writabl
33
import { EMPTY, Observable, Subject, fromEvent, merge } from 'rxjs';
44
import { auditTime, map, pairwise, takeUntil } from 'rxjs/operators';
55
import { isNumber } from './utils/helpers';
6-
import { passiveListenerOptions } from './utils/passive-listeners';
76

87
const scrollThreshold = 50;
98

@@ -53,44 +52,6 @@ export class GanttDomService implements OnDestroy {
5352
@Inject(PLATFORM_ID) private platformId: string
5453
) {}
5554

56-
private monitorScrollChange() {
57-
const scrollObservers = [
58-
fromEvent(this.mainContainer, 'scroll', passiveListenerOptions),
59-
fromEvent(this.sideContainer, 'scroll', passiveListenerOptions)
60-
];
61-
62-
this.mainFooter && scrollObservers.push(fromEvent(this.mainFooter, 'scroll', passiveListenerOptions));
63-
this.mainScrollbar && scrollObservers.push(fromEvent(this.mainScrollbar, 'scroll', passiveListenerOptions));
64-
65-
this.ngZone.runOutsideAngular(() =>
66-
merge(...scrollObservers)
67-
.pipe(takeUntil(this.unsubscribe$))
68-
.subscribe((event) => {
69-
this.syncScroll(event);
70-
})
71-
);
72-
}
73-
74-
private syncScroll(event: Event) {
75-
const target = event.currentTarget as HTMLElement;
76-
const classList = target.classList;
77-
78-
if (!classList.contains('gantt-side-container')) {
79-
this.mainContainer.scrollLeft = target.scrollLeft;
80-
this.calendarHeader.scrollLeft = target.scrollLeft;
81-
this.calendarOverlay.scrollLeft = target.scrollLeft;
82-
this.mainScrollbar && (this.mainScrollbar.scrollLeft = target.scrollLeft);
83-
this.mainFooter && (this.mainFooter.scrollLeft = target.scrollLeft);
84-
if (classList.contains('gantt-main-container')) {
85-
this.sideContainer.scrollTop = target.scrollTop;
86-
this.mainContainer.scrollTop = target.scrollTop;
87-
}
88-
} else {
89-
this.sideContainer.scrollTop = target.scrollTop;
90-
this.mainContainer.scrollTop = target.scrollTop;
91-
}
92-
}
93-
9455
private disableBrowserWheelEvent() {
9556
const container = this.mainContainer as HTMLElement;
9657
this.ngZone.runOutsideAngular(() =>
@@ -126,7 +87,6 @@ export class GanttDomService implements OnDestroy {
12687
this.calendarHeader = this.root.getElementsByClassName('gantt-calendar-header')[0];
12788
this.calendarOverlay = this.root.getElementsByClassName('gantt-calendar-grid')[0];
12889

129-
this.monitorScrollChange();
13090
this.disableBrowserWheelEvent();
13191
}
13292

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { inject, Injectable, NgZone } from '@angular/core';
2+
import { fromEvent, merge, Subject, takeUntil } from 'rxjs';
3+
import { passiveListenerOptions } from './utils/passive-listeners';
4+
5+
@Injectable()
6+
export class GanttSyncScrollService {
7+
private ngZone = inject(NgZone);
8+
9+
private scrollGroupsMap = new Map<string, { elements: HTMLElement[]; direction: 'x' | 'y'; destroy$: Subject<void> }>();
10+
11+
constructor() {}
12+
13+
registerScrollEvent(groupName: string, element: HTMLElement, direction: 'x' | 'y') {
14+
const group = this.scrollGroupsMap.get(groupName) || { elements: [], destroy$: new Subject<void>(), direction };
15+
group.elements.push(element);
16+
this.scrollGroupsMap.set(groupName, group);
17+
this.monitorScrollChange(group);
18+
}
19+
20+
unregisterScrollEvent(groupName: string, element: HTMLElement) {
21+
const group = this.scrollGroupsMap.get(groupName);
22+
if (group) {
23+
group.elements = group.elements.filter((el) => el !== element);
24+
if (!group.elements.length) {
25+
this.scrollGroupsMap.delete(groupName);
26+
} else {
27+
this.scrollGroupsMap.set(groupName, group);
28+
}
29+
this.monitorScrollChange(group);
30+
}
31+
}
32+
33+
private monitorScrollChange(group: { elements: HTMLElement[]; destroy$: Subject<void>; direction: 'x' | 'y' }) {
34+
const { elements, destroy$, direction } = group;
35+
destroy$.next();
36+
destroy$.complete();
37+
if (elements.length) {
38+
const scrollObservers = elements.map((el) => fromEvent(el, 'scroll', passiveListenerOptions));
39+
this.ngZone.runOutsideAngular(() =>
40+
merge(...scrollObservers)
41+
.pipe(takeUntil(destroy$))
42+
.subscribe((event) => {
43+
elements.forEach((el) => {
44+
if (direction === 'x') {
45+
el.scrollLeft = (event.currentTarget as HTMLElement).scrollLeft;
46+
} else {
47+
el.scrollTop = (event.currentTarget as HTMLElement).scrollTop;
48+
}
49+
});
50+
})
51+
);
52+
}
53+
}
54+
}

packages/gantt/src/gantt.component.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
<div class="gantt-header">
33
<gantt-table-header #tableHeader [columns]="columns"></gantt-table-header>
44
<div class="gantt-container-header">
5-
<gantt-calendar-header [style.padding-right.px]="ganttRoot.verticalScrollbarWidth"></gantt-calendar-header>
5+
<gantt-calendar-header
6+
syncScrollX="ganttMainXScroll"
7+
[style.padding-right.px]="ganttRoot.verticalScrollbarWidth"
8+
></gantt-calendar-header>
69
</div>
710
</div>
811
@if (loading) {
@@ -24,7 +27,7 @@
2427
>
2528
<ng-container *cdkVirtualFor="let item of flatItems; trackBy: trackBy"></ng-container>
2629
<div class="gantt-side" [style.width.px]="tableHeader.tableWidth + 1" [style.padding-bottom.px]="ganttRoot.horizontalScrollbarHeight">
27-
<div class="gantt-side-container">
30+
<div class="gantt-side-container" syncScrollX="ganttTableXScroll" syncScrollY="ganttMainYScroll">
2831
<div class="gantt-table">
2932
<gantt-table-body
3033
#ganttTableBody
@@ -48,11 +51,14 @@
4851
</div>
4952
<div class="gantt-container">
5053
<gantt-calendar-grid
54+
syncScrollX="ganttMainXScroll"
5155
[style.padding-right.px]="ganttRoot.verticalScrollbarWidth"
5256
[style.padding-bottom.px]="ganttRoot.horizontalScrollbarHeight"
5357
></gantt-calendar-grid>
5458
<div class="gantt-main">
5559
<gantt-main
60+
syncScrollX="ganttMainXScroll"
61+
syncScrollY="ganttMainYScroll"
5662
[ganttRoot]="ganttRoot"
5763
[flatItems]="flatItems"
5864
[viewportItems]="viewportItems"
@@ -86,12 +92,12 @@
8692
[style.bottom.px]="ganttRoot.horizontalScrollbarHeight"
8793
>
8894
@if (table?.tableFooterTemplate) {
89-
<div class="gantt-table-footer" [style.width.px]="tableHeader.tableWidth + 1">
95+
<div class="gantt-table-footer" syncScrollX="ganttTableXScroll" [style.width.px]="tableHeader.tableWidth + 1">
9096
<ng-template [ngTemplateOutlet]="table?.tableFooterTemplate" [ngTemplateOutletContext]="{ columns: columns }"> </ng-template>
9197
</div>
9298
}
9399
@if (footerTemplate) {
94-
<div class="gantt-container-footer">
100+
<div class="gantt-container-footer" syncScrollX="ganttMainXScroll">
95101
<ng-template [ngTemplateOutlet]="footerTemplate"> </ng-template>
96102
</div>
97103
}

0 commit comments

Comments
 (0)