diff --git a/.circleci/config.yml b/.circleci/config.yml index 6fc0b1db..dd9a2c7e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,6 @@ jobs: paths: - 'node_modules' - run: npm run lint -- --quiet - - run: npm run test -- --no-watch --no-progress --browsers=ChromeHeadlessCI + - run: npm run test -- --no-watch --no-progress --browsers=ChromeHeadlessCI --source-map=false - run: npm run report-coverage - run: npm run build diff --git a/.eslintrc.json b/.eslintrc.json index fc29e703..2088b56a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,7 +24,8 @@ "rules": { "rxjs/no-unsafe-takeuntil": "error", "rxjs/no-subject-unsubscribe": "error", - "rxjs/no-unsafe-subject-next": "error" + "rxjs/no-unsafe-subject-next": "error", + "@angular-eslint/no-host-metadata-property": "off" } }, { diff --git a/packages/gantt/src/components/bar/bar-drag.ts b/packages/gantt/src/components/bar/bar-drag.ts index 543c3722..9508f032 100644 --- a/packages/gantt/src/components/bar/bar-drag.ts +++ b/packages/gantt/src/components/bar/bar-drag.ts @@ -1,4 +1,4 @@ -import { Injectable, ElementRef, OnDestroy } from '@angular/core'; +import { Injectable, ElementRef, OnDestroy, SkipSelf } from '@angular/core'; import { DragRef, DragDrop } from '@angular/cdk/drag-drop'; import { GanttDomService } from '../../gantt-dom.service'; import { GanttDragContainer, InBarPosition } from '../../gantt-drag-container'; @@ -8,6 +8,8 @@ import { fromEvent, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { GanttUpper } from '../../gantt-upper'; import { GanttLinkType } from '../../class/link'; +import { NgxGanttRootComponent } from '../../root.component'; +import { passiveListenerOptions } from '../../utils/passive-listeners'; const dragMinWidth = 10; const activeClass = 'gantt-bar-active'; @@ -44,7 +46,12 @@ export class GanttBarDrag implements OnDestroy { private destroy$ = new Subject(); - constructor(private dragDrop: DragDrop, private dom: GanttDomService, private dragContainer: GanttDragContainer) {} + constructor( + private dragDrop: DragDrop, + private dom: GanttDomService, + private dragContainer: GanttDragContainer, + @SkipSelf() private root: NgxGanttRootComponent + ) {} private createMouseEvents() { const dropClass = @@ -53,9 +60,9 @@ export class GanttBarDrag implements OnDestroy { ? singleDropActiveClass : dropActiveClass; - fromEvent(this.barElement, 'mouseenter') + fromEvent(this.barElement, 'mouseenter', passiveListenerOptions) .pipe(takeUntil(this.destroy$)) - .subscribe((event: MouseEvent) => { + .subscribe(() => { if (this.dragContainer.linkDraggingId && this.dragContainer.linkDraggingId !== this.item.id) { if (this.item.linkable) { this.barElement.classList.add(dropClass); @@ -69,9 +76,9 @@ export class GanttBarDrag implements OnDestroy { } }); - fromEvent(this.barElement, 'mouseleave') + fromEvent(this.barElement, 'mouseleave', passiveListenerOptions) .pipe(takeUntil(this.destroy$)) - .subscribe((event: MouseEvent) => { + .subscribe(() => { if (!this.dragContainer.linkDraggingId) { this.barElement.classList.remove(activeClass); } else { @@ -244,23 +251,25 @@ export class GanttBarDrag implements OnDestroy { } private openDragBackdrop(dragElement: HTMLElement, start: GanttDate, end: GanttDate) { - const dragMaskElement = this.dom.root.querySelector('.gantt-drag-mask') as HTMLElement; - const dragBackdropElement = this.dom.root.querySelector('.gantt-drag-backdrop') as HTMLElement; + const dragBackdropElement = this.root.backdrop.nativeElement; + const dragMaskElement = dragBackdropElement.querySelector('.gantt-drag-mask') as HTMLElement; const rootRect = this.dom.root.getBoundingClientRect(); const dragRect = dragElement.getBoundingClientRect(); const left = dragRect.left - rootRect.left - this.dom.side.clientWidth; const width = dragRect.right - dragRect.left; + // Note: updating styles will cause re-layout so we have to place them consistently one by one. dragMaskElement.style.left = left + 'px'; dragMaskElement.style.width = width + 'px'; - dragMaskElement.querySelector('.start').innerHTML = start.format('MM-dd'); - dragMaskElement.querySelector('.end').innerHTML = end.format('MM-dd'); dragMaskElement.style.display = 'block'; dragBackdropElement.style.display = 'block'; + // This will invalidate the layout, but we won't need re-layout, because we set styles previously. + dragMaskElement.querySelector('.start').innerHTML = start.format('MM-dd'); + dragMaskElement.querySelector('.end').innerHTML = end.format('MM-dd'); } private closeDragBackdrop() { - const dragMaskElement = this.dom.root.querySelector('.gantt-drag-mask') as HTMLElement; - const dragBackdropElement = this.dom.root.querySelector('.gantt-drag-backdrop') as HTMLElement; + const dragBackdropElement = this.root.backdrop.nativeElement; + const dragMaskElement = dragBackdropElement.querySelector('.gantt-drag-mask') as HTMLElement; dragMaskElement.style.display = 'none'; dragBackdropElement.style.display = 'none'; } diff --git a/packages/gantt/src/components/drag-backdrop/drag-backdrop.component.ts b/packages/gantt/src/components/drag-backdrop/drag-backdrop.component.ts index 133f6ed7..7394be5b 100644 --- a/packages/gantt/src/components/drag-backdrop/drag-backdrop.component.ts +++ b/packages/gantt/src/components/drag-backdrop/drag-backdrop.component.ts @@ -1,9 +1,10 @@ -import { Component, HostBinding } from '@angular/core'; +import { Component } from '@angular/core'; @Component({ selector: 'gantt-drag-backdrop', - templateUrl: `./drag-backdrop.component.html` + templateUrl: `./drag-backdrop.component.html`, + host: { + class: 'gantt-drag-backdrop' + } }) -export class GanttDragBackdropComponent { - @HostBinding('class.gantt-drag-backdrop') backdropClass = true; -} +export class GanttDragBackdropComponent {} diff --git a/packages/gantt/src/gantt-dom.service.ts b/packages/gantt/src/gantt-dom.service.ts index a71d3864..1247408b 100644 --- a/packages/gantt/src/gantt-dom.service.ts +++ b/packages/gantt/src/gantt-dom.service.ts @@ -3,6 +3,7 @@ import { Injectable, ElementRef, OnDestroy, Inject, PLATFORM_ID, NgZone } from ' import { fromEvent, Subject, merge, EMPTY, Observable } from 'rxjs'; import { pairwise, map, auditTime, takeUntil } from 'rxjs/operators'; import { isNumber } from './utils/helpers'; +import { passiveListenerOptions } from './utils/passive-listeners'; const scrollThreshold = 50; @@ -39,7 +40,10 @@ export class GanttDomService implements OnDestroy { private monitorScrollChange() { this.ngZone.runOutsideAngular(() => - merge(fromEvent(this.mainContainer, 'scroll', { passive: true }), fromEvent(this.sideContainer, 'scroll', { passive: true })) + merge( + fromEvent(this.mainContainer, 'scroll', passiveListenerOptions), + fromEvent(this.sideContainer, 'scroll', passiveListenerOptions) + ) .pipe(takeUntil(this.unsubscribe$)) .subscribe((event) => { this.syncScroll(event); diff --git a/packages/gantt/src/root.component.ts b/packages/gantt/src/root.component.ts index 1688fdb9..2b59e24d 100644 --- a/packages/gantt/src/root.component.ts +++ b/packages/gantt/src/root.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit, - HostBinding, NgZone, ElementRef, Inject, @@ -9,7 +8,8 @@ import { TemplateRef, Input, Optional, - OnDestroy + OnDestroy, + ViewChild } from '@angular/core'; import { GanttDomService, ScrollDirection } from './gantt-dom.service'; import { GanttDragContainer } from './gantt-drag-container'; @@ -17,21 +17,27 @@ import { take, takeUntil, startWith } from 'rxjs/operators'; import { from, Subject } from 'rxjs'; import { GanttUpper, GANTT_UPPER_TOKEN } from './gantt-upper'; import { GanttPrintService } from './gantt-print.service'; +import { passiveListenerOptions } from './utils/passive-listeners'; +import { GanttDragBackdropComponent } from './components/drag-backdrop/drag-backdrop.component'; @Component({ selector: 'ngx-gantt-root', templateUrl: './root.component.html', - providers: [GanttDomService, GanttDragContainer] + providers: [GanttDomService, GanttDragContainer], + host: { + class: 'gantt' + } }) export class NgxGanttRootComponent implements OnInit, OnDestroy { @Input() sideWidth: number; - @HostBinding('class.gantt') ganttClass = true; - @ContentChild('sideTemplate', { static: true }) sideTemplate: TemplateRef; @ContentChild('mainTemplate', { static: true }) mainTemplate: TemplateRef; + /** The native `` element. */ + @ViewChild(GanttDragBackdropComponent, { static: true, read: ElementRef }) backdrop: ElementRef; + private unsubscribe$ = new Subject(); private get view() { @@ -82,7 +88,7 @@ export class NgxGanttRootComponent implements OnInit, OnDestroy { return; } this.dom - .getViewerScroll({ passive: true }) + .getViewerScroll(passiveListenerOptions) .pipe(takeUntil(this.unsubscribe$)) .subscribe((event) => { if (event.direction === ScrollDirection.LEFT) { diff --git a/packages/gantt/src/utils/passive-listeners.ts b/packages/gantt/src/utils/passive-listeners.ts new file mode 100644 index 00000000..5b07f288 --- /dev/null +++ b/packages/gantt/src/utils/passive-listeners.ts @@ -0,0 +1,36 @@ +/** Cached result of whether the user's browser supports passive event listeners. */ +let supportsPassiveEvents: boolean; + +/** + * Checks whether the user's browser supports passive event listeners. + * See: https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md + */ +export function supportsPassiveEventListeners(): boolean { + if (supportsPassiveEvents == null && typeof window !== 'undefined') { + try { + window.addEventListener( + 'test', + null!, + Object.defineProperty({}, 'passive', { + get: () => (supportsPassiveEvents = true) + }) + ); + } finally { + supportsPassiveEvents = supportsPassiveEvents || false; + } + } + + return supportsPassiveEvents; +} + +/** + * Normalizes an `AddEventListener` object to something that can be passed + * to `addEventListener` on any browser, no matter whether it supports the + * `options` parameter. + */ +export function normalizePassiveListenerOptions(options: AddEventListenerOptions): AddEventListenerOptions | boolean { + return supportsPassiveEventListeners() ? options : !!options.capture; +} + +/** Options used to bind passive event listeners. */ +export const passiveListenerOptions = normalizePassiveListenerOptions({ passive: true });