diff --git a/example/src/app/gantt-virtual-scroll/gantt.component.html b/example/src/app/gantt-virtual-scroll/gantt.component.html index 97d28420..73f1b3c5 100644 --- a/example/src/app/gantt-virtual-scroll/gantt.component.html +++ b/example/src/app/gantt-virtual-scroll/gantt.component.html @@ -12,6 +12,7 @@ [virtualScrollEnabled]="true" (virtualScrolledIndexChange)="virtualScrolledIndexChange($event)" [loading]="loading" + [quickTimeFocus]="true" > `; const dragIcon = ``; +const arrowLeftIcon = ``; +const arrowRightIcon = ``; export const icons = { 'angle-right': angleRight, 'angle-down': angleDown, @@ -85,5 +87,7 @@ export const icons = { 'minus-square': minusSquare, loading: loadingIcon, empty: emptyIcon, - drag: dragIcon + drag: dragIcon, + 'arrow-left': arrowLeftIcon, + 'arrow-right': arrowRightIcon }; diff --git a/packages/gantt/src/components/main/gantt-main.component.html b/packages/gantt/src/components/main/gantt-main.component.html index a9fc0eb5..2b1412aa 100644 --- a/packages/gantt/src/components/main/gantt-main.component.html +++ b/packages/gantt/src/components/main/gantt-main.component.html @@ -35,3 +35,29 @@ + +@if (quickTimeFocus) { +
+
+ +
+ + @if ((data.refs.x < dom.visibleRangeX().min ) && data.refs.width ) { + + + + } + + + + @if((data.refs.x + data.refs.width > dom.visibleRangeX().max) && data.refs.width) { + + + + } + +
+
+
+
+} diff --git a/packages/gantt/src/components/main/gantt-main.component.ts b/packages/gantt/src/components/main/gantt-main.component.ts index 6ec1587b..21dfee99 100644 --- a/packages/gantt/src/components/main/gantt-main.component.ts +++ b/packages/gantt/src/components/main/gantt-main.component.ts @@ -1,4 +1,4 @@ -import { Component, HostBinding, Inject, Input, TemplateRef, Output, EventEmitter } from '@angular/core'; +import { Component, HostBinding, Inject, Input, TemplateRef, Output, EventEmitter, OnInit, AfterViewInit, NgZone } from '@angular/core'; import { GanttGroupInternal, GanttItemInternal, GanttBarClickEvent, GanttLineClickEvent } from '../../class'; import { GANTT_UPPER_TOKEN, GanttUpper } from '../../gantt-upper'; import { IsGanttRangeItemPipe, IsGanttBarItemPipe, IsGanttCustomItemPipe } from '../../gantt.pipe'; @@ -7,6 +7,10 @@ import { NgxGanttBarComponent } from '../bar/bar.component'; import { NgxGanttRangeComponent } from '../range/range.component'; import { NgFor, NgIf, NgClass, NgTemplateOutlet } from '@angular/common'; import { GanttLinksComponent } from '../links/links.component'; +import { NgxGanttRootComponent } from 'ngx-gantt'; +import { GanttIconComponent } from '../icon/icon.component'; +import { GanttDomService } from '../../gantt-dom.service'; +import { combineLatest, from, Subject, take, takeUntil } from 'rxjs'; @Component({ selector: 'gantt-main', @@ -23,10 +27,11 @@ import { GanttLinksComponent } from '../links/links.component'; NgxGanttBaselineComponent, IsGanttRangeItemPipe, IsGanttBarItemPipe, - IsGanttCustomItemPipe + IsGanttCustomItemPipe, + GanttIconComponent ] }) -export class GanttMainComponent { +export class GanttMainComponent implements OnInit { @Input() viewportItems: (GanttGroupInternal | GanttItemInternal)[]; @Input() flatItems: (GanttGroupInternal | GanttItemInternal)[]; @@ -41,15 +46,43 @@ export class GanttMainComponent { @Input() baselineTemplate: TemplateRef; + @Input() ganttRoot: NgxGanttRootComponent; + + @Input() quickTimeFocus: boolean; + @Output() barClick = new EventEmitter(); @Output() lineClick = new EventEmitter(); @HostBinding('class.gantt-main-container') ganttMainClass = true; - constructor(@Inject(GANTT_UPPER_TOKEN) public ganttUpper: GanttUpper) {} + private unsubscribe$ = new Subject(); + + constructor(@Inject(GANTT_UPPER_TOKEN) public ganttUpper: GanttUpper, public dom: GanttDomService, protected ngZone: NgZone) {} + + ngOnInit(): void { + const onStable$ = this.ngZone.isStable ? from(Promise.resolve()) : this.ngZone.onStable.pipe(take(1)); + this.ngZone.runOutsideAngular(() => { + onStable$.pipe(takeUntil(this.unsubscribe$)).subscribe(() => { + this.setupResize(); + }); + }); + } trackBy(index: number, item: GanttGroupInternal | GanttItemInternal) { return item.id || index; } + + private setupResize() { + combineLatest([this.dom.getResize(), this.dom.getResizeByElement(this.dom.mainContainer)]) + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(() => { + this.dom.setVisibleRangeX(); + }); + } + + quickTime(item: GanttItemInternal, type: 'left' | 'right') { + const date = type === 'left' ? item.start || item.end : item.end || item.start; + this.ganttRoot.scrollToDate(date); + } } diff --git a/packages/gantt/src/gantt-dom.service.ts b/packages/gantt/src/gantt-dom.service.ts index 6e1b26bc..e6a69b58 100644 --- a/packages/gantt/src/gantt-dom.service.ts +++ b/packages/gantt/src/gantt-dom.service.ts @@ -1,5 +1,5 @@ import { isPlatformServer } from '@angular/common'; -import { Injectable, ElementRef, OnDestroy, Inject, PLATFORM_ID, NgZone } from '@angular/core'; +import { Injectable, ElementRef, OnDestroy, Inject, PLATFORM_ID, NgZone, WritableSignal, signal } from '@angular/core'; import { fromEvent, Subject, merge, EMPTY, Observable } from 'rxjs'; import { pairwise, map, auditTime, takeUntil } from 'rxjs/operators'; import { isNumber } from './utils/helpers'; @@ -40,6 +40,8 @@ export class GanttDomService implements OnDestroy { public linksOverlay: Element; + public visibleRangeX: WritableSignal<{ min: number; max: number }> = signal({ min: 0, max: 0 }); + private mainFooter: Element; private mainScrollbar: Element; @@ -141,6 +143,7 @@ export class GanttDomService implements OnDestroy { map(() => this.mainContainer.scrollLeft), pairwise(), map(([previous, current]) => { + this.setVisibleRangeX(); const event: ScrollEvent = { target: this.mainContainer, direction: ScrollDirection.NONE @@ -170,6 +173,15 @@ export class GanttDomService implements OnDestroy { return isPlatformServer(this.platformId) ? EMPTY : fromEvent(window, 'resize').pipe(auditTime(150)); } + getResizeByElement(element: Element) { + return new Observable((observer) => { + const resizeObserver = new ResizeObserver(() => { + observer.next(); + }); + resizeObserver.observe(element); + }); + } + scrollMainContainer(left: number) { if (isNumber(left)) { const scrollLeft = left - this.mainContainer.clientWidth / 2; @@ -181,6 +193,13 @@ export class GanttDomService implements OnDestroy { } } + setVisibleRangeX() { + this.visibleRangeX.set({ + min: this.mainContainer.scrollLeft, + max: this.mainContainer.scrollLeft + this.mainContainer.clientWidth + }); + } + ngOnDestroy() { this.unsubscribe$.next(); this.unsubscribe$.complete(); diff --git a/packages/gantt/src/gantt-upper.ts b/packages/gantt/src/gantt-upper.ts index 19d0db47..678064e5 100644 --- a/packages/gantt/src/gantt-upper.ts +++ b/packages/gantt/src/gantt-upper.ts @@ -109,6 +109,8 @@ export abstract class GanttUpper implements OnChanges, OnInit, OnDestroy { return this._multiple; } + @Input() quickTimeFocus = false; + @Output() loadOnScroll = new EventEmitter(); @Output() dragStarted = new EventEmitter(); diff --git a/packages/gantt/src/gantt.component.html b/packages/gantt/src/gantt.component.html index ea3d7866..6113ac10 100644 --- a/packages/gantt/src/gantt.component.html +++ b/packages/gantt/src/gantt.component.html @@ -50,6 +50,7 @@ >
diff --git a/packages/gantt/src/gantt.component.scss b/packages/gantt/src/gantt.component.scss index 363e4218..f0b4a987 100644 --- a/packages/gantt/src/gantt.component.scss +++ b/packages/gantt/src/gantt.component.scss @@ -110,6 +110,59 @@ background-color: rgba($color: variables.$gantt-table-header-drag-line-color, $alpha: 0.1); } } + + .gantt-quick-time-focus-container { + position: absolute; + left: 0; + top: 0; + .gantt-quick-time-focus { + position: sticky; + left: 0; + width: 0px; + z-index: 3; + pointer-events: none; + + &-item { + display: flex; + justify-content: space-between; + align-items: center; + span { + width: 24px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 2px; + pointer-events: all; + &:hover { + .gantt-quick-time-focus-item-arrow { + border: 1px solid rgba(variables.$gantt-primary-color, 1); + } + } + } + + &-arrow { + width: 20px; + height: 20px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + background-color: variables.$gantt-bar-bg; + border: 1px solid variables.$gantt-border-color; + border-radius: 4px; + box-shadow: 0 4px 7px 1px rgba(0, 0, 0, 0.05); + .gantt-icon { + display: inline-block; + svg { + width: 14px; + height: 14px; + } + } + } + } + } + } } .gantt-normal-viewport { diff --git a/packages/gantt/src/gantt.component.ts b/packages/gantt/src/gantt.component.ts index 26377c10..3521dc62 100644 --- a/packages/gantt/src/gantt.component.ts +++ b/packages/gantt/src/gantt.component.ts @@ -1,5 +1,5 @@ import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport, ViewportRuler } from '@angular/cdk/scrolling'; -import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common'; +import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common'; import { AfterViewChecked, AfterViewInit, @@ -82,7 +82,8 @@ import { GanttScrollbarComponent } from './components/scrollbar/scrollbar.compon GanttMainComponent, GanttDragBackdropComponent, GanttScrollbarComponent, - NgTemplateOutlet + NgTemplateOutlet, + NgFor ] }) export class NgxGanttComponent extends GanttUpper implements OnInit, OnChanges, AfterViewInit, AfterViewChecked { diff --git a/packages/gantt/src/root.component.ts b/packages/gantt/src/root.component.ts index a492c728..487876b1 100644 --- a/packages/gantt/src/root.component.ts +++ b/packages/gantt/src/root.component.ts @@ -127,7 +127,7 @@ export class NgxGanttRootComponent implements OnInit, OnDestroy { } private setupViewScroll() { - if (this.ganttUpper.disabledLoadOnScroll) { + if (this.ganttUpper.disabledLoadOnScroll && !this.ganttUpper.quickTimeFocus) { return; } this.dom