import { ScrollDispatcher } from '@angular/cdk/scrolling' ; import { Directive , ElementRef , EventEmitter , Input , NgZone , OnDestroy , Output , Renderer2 } from '@angular/core' ; import { fromEvent , Subject } from 'rxjs' ; import { filter , first , takeUntil } from 'rxjs/operators' ; import { DragService , UxDragEvent } from './drag.service' ; export interface DragScrollEvent { offsetX : number ; offsetY : number ; } @ Directive ( { selector : '[uxDrag]' } ) export class DragDirective < T = any > implements OnDestroy { /** Detemine if we should show a clone when dragging */ @ Input ( ) clone : boolean = false ; /** Define the group the drag event belongs to */ @ Input ( ) group : string ; /** Associate some data with the drag event */ @ Input ( ) model : T ; /** Allow the dragging to be enabled/disabled */ @ Input ( ) draggable : boolean = true ; /** Emit an event when dragging starts */ @ Output ( ) onDragStart = new EventEmitter < MouseEvent > ( ) ; /** Emit an event when the mouse moves while dragging */ @ Output ( ) onDrag = new EventEmitter < MouseEvent > ( ) ; /** Emit an event when the document scrolls while dragging */ @ Output ( ) onDragScroll = new EventEmitter < DragScrollEvent > ( ) ; /** Emit an event when the dragging finishes */ @ Output ( ) onDragEnd = new EventEmitter < void > ( ) ; /** Emit when the user drops an item in a drop area */ @ Output ( ) onDrop = new EventEmitter < T > ( ) ; /** Emit when the user drags over a drop area */ @ Output ( ) onDropEnter = new EventEmitter < void > ( ) ; /** Emit when the user drags out of a drop area */ @ Output ( ) onDropLeave = new EventEmitter < void > ( ) ; /** Store the element we have cloned */ private _clone : Element ; /** Store the dragging state */ private _isDragging : boolean = false ; /** Store the mouse offset for the cloned element position */ private _offset : { x : number ; y : number ; } ; /** The nearest scrolling ancestor, populated while dragging */ private _scrollParent : Element ; /** The current scroll position of _scrollParent */ private _scrollPosition : { top : number ; left : number ; } ; /** The scroll event listener handle */ private _scrollListener : ( ) => void ; /** The offset in pixels currently being scrolled while dragging */ private _scrollOffset : { x : number ; y : number ; } = { x : 0 , y : 0 } ; /** The current `setInterval` handle for periodic scrolling */ private _scrollIntervalHandle : number ; /** Create an observable from the mouse down event */ private _mousedown$ = fromEvent < MouseEvent > ( this . _elementRef . nativeElement , 'mousedown' ) ; /** Create an observable from the mouse move event */ private _mousemove$ = fromEvent < MouseEvent > ( document , 'mousemove' ) ; /** Create an observable from the mouse up event */ private _mouseup$ = fromEvent < MouseEvent > ( document , 'mouseup' ) ; /** Use an observable to unsubscribe from all subscriptions */ protected _onDestroy = new Subject < void > ( ) ; constructor ( private _elementRef , private _ngZone , private _renderer , private _scrollDispatcher , private _drag ) { this . _mousedown$ . pipe ( filter ( ( ) => this . draggable ) , takeUntil ( this . _onDestroy ) ) . subscribe ( this . dragStart . bind ( this ) ) ; _drag . onDragStart . pipe ( filter ( ( ) => this . _isDragging ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( dragEvent ) => this . onDragStart . emit ( dragEvent . event ) ) ; _drag . onDrag . pipe ( filter ( ( ) => this . _isDragging ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( dragEvent ) => this . onDrag . emit ( dragEvent . event ) ) ; _drag . onDragEnd . pipe ( filter ( event => this . _isDragging || ( this . model && this . model === event . data ) ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => this . onDragEnd . emit ( ) ) ; _drag . onDrop . pipe ( filter ( ( ) => this . _isDragging ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( event ) => this . onDrop . emit ( event ) ) ; _drag . onDropEnter . pipe ( filter ( ( ) => this . _isDragging ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => this . onDropEnter . emit ( ) ) ; _drag . onDropLeave . pipe ( filter ( ( ) => this . _isDragging ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => this . onDropLeave . emit ( ) ) ; } /** Emit events and create clone when drag starts */ dragStart ( event ) { event . preventDefault ( ) ; this . createScrollEventListener ( ) ; if ( this . clone ) { this . cloneNode ( event ) ; } this . _renderer . addClass ( this . _elementRef . nativeElement , 'ux-drag-dragging' ) ; this . _isDragging = true ; this . _ngZone . run ( ( ) => this . _drag . onDragStart . next ( { event , group : this . group , data : this . model } ) ) ; this . _mousemove$ . pipe ( takeUntil ( this . _mouseup$ ) , takeUntil ( this . _onDestroy ) ) . subscribe ( this . dragMove . bind ( this ) ) ; this . _mouseup$ . pipe ( first ( ) ) . subscribe ( this . dragEnd . bind ( this ) ) ; } /** Emit event and update clone position when dragging moves */ dragMove ( event ) { event . preventDefault ( ) ; this . updateScrolling ( event ) ; if ( this . _clone ) { this . updateNodePosition ( event ) ; } this . _ngZone . run ( ( ) => this . _drag . onDrag . next ( { event , group : this . group , data : this . model } ) ) ; } /** Emit event and destroy clone when dragging ends */ dragEnd ( ) { this . stopScrolling ( ) ; this . removeScrollEventListener ( ) ; if ( this . _clone ) { this . _renderer . removeChild ( document . body , this . _clone ) ; this . _clone = null ; } this . _renderer . removeClass ( this . _elementRef . nativeElement , 'ux-drag-dragging' ) ; this . _ngZone . run ( ( ) => this . _drag . onDragEnd . next ( { group : this . group , data : this . model } ) ) ; this . _isDragging = false ; } /** Emit the onDragScroll event */ scroll ( ) { const offsetX = this . _scrollParent . scrollLeft - this . _scrollPosition . left ; const offsetY = this . _scrollParent . scrollTop - this . _scrollPosition . top ; this . onDragScroll . emit ( { offsetX , offsetY } ) ; this . _scrollPosition = { top : this . _scrollParent . scrollTop , left : this . _scrollParent . scrollLeft } ; } /** Create an exact clone of an element */ cloneNode ( event ) { this . _clone = this . _elementRef . nativeElement . cloneNode ( true ) as Element ; const { top , left , width } = this . _elementRef . nativeElement . getBoundingClientRect ( ) ; this . _offset = { x : event . clientX - left , y : event . clientY - top } ; this . inlineStyles ( this . _elementRef . nativeElement , this . _clone ) ; this . _renderer . setStyle ( this . _clone , 'width' , width + 'px' ) ; this . _renderer . setAttribute ( this . _clone , 'aria-hidden' , 'true' ) ; this . _renderer . setStyle ( this . _clone , 'position' , 'absolute' ) ; this . _renderer . setStyle ( this . _clone , 'z-index' , '99999' ) ; this . _renderer . addClass ( this . _clone , 'ux-drag-dragging-clone' ) ; this . _renderer . appendChild ( document . body , this . _clone ) ; this . updateNodePosition ( event ) ; } /** Position the clone relative to the mouse */ updateNodePosition ( event ) { this . _renderer . setStyle ( this . _clone , 'left' , ( event . pageX - this . _offset . x ) + 'px' ) ; this . _renderer . setStyle ( this . _clone , 'top' , ( event . pageY - this . _offset . y ) + 'px' ) ; } /** Inline all styles to ensure styling is consistent regardless of its position in the dom */ inlineStyles ( source , target ) { const styles = getComputedStyle ( source ) ; for ( let idx = 0 ; idx < styles . length ; idx ++ ) { const style = styles . item ( idx ) ; if ( style !== undefined ) { this . _renderer . setStyle ( target , styles [ idx ] , styles [ style ] ) ; } } this . _renderer . setStyle ( target , 'pointer-events' , 'none' ) ; for ( let idx = 0 ; idx < source . children . length ; idx ++ ) { this . inlineStyles ( source . children [ idx ] , target . children [ idx ] ) ; } } /** Unsubscribe from all subscriptions */ ngOnDestroy ( ) { this . stopScrolling ( ) ; this . removeScrollEventListener ( ) ; this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; } private updateScrolling ( event ) { this . _scrollOffset = this . getScrollOffsets ( this . _scrollParent , event ) ; if ( this . _scrollOffset . x === 0 && this . _scrollOffset . y === 0 ) { this . stopScrolling ( ) ; } else { this . startScrolling ( this . _scrollParent ) ; } } private getScrollParent ( ) { const containers = this . _scrollDispatcher . getAncestorScrollContainers ( this . _elementRef ) ; if ( containers . length > 0 ) { return containers [ containers . length - 1 ] . getElementRef ( ) . nativeElement ; } return document . documentElement ; } private createScrollEventListener ( ) { this . _scrollParent = this . getScrollParent ( ) ; this . _scrollPosition = { top : this . _scrollParent . scrollTop , left : this . _scrollParent . scrollLeft } ; const target = this . _scrollParent === document . documentElement ? 'document' : this . _scrollParent ; this . _scrollListener = this . _renderer . listen ( target , 'scroll' , this . scroll . bind ( this ) ) ; } private removeScrollEventListener ( ) { if ( this . _scrollListener ) { this . _scrollListener ( ) ; this . _scrollListener = null ; } } private getScrollOffsets ( scrollElement , event ) { let scrollX = 0 ; let scrollY = 0 ; const minScroll = 5 ; const isRoot = ( scrollElement === document . documentElement ) ; const bounds = scrollElement . getBoundingClientRect ( ) ; const pointerOffsetX = isRoot ? event . clientX : event . clientX - bounds . x ; const pointerOffsetY = isRoot ? event . clientY : event . clientY - bounds . y ; if ( pointerOffsetX <= 0 && scrollElement . scrollLeft > 0 ) { scrollX = Math . min ( pointerOffsetX , - minScroll ) ; } else if ( pointerOffsetX >= scrollElement . clientWidth && ( scrollElement . scrollLeft + scrollElement . clientWidth ) < scrollElement . scrollWidth ) { scrollX = Math . max ( pointerOffsetX - scrollElement . clientWidth , minScroll ) ; } if ( pointerOffsetY <= 0 && scrollElement . scrollTop > 0 ) { scrollY = Math . min ( pointerOffsetY , - minScroll ) ; } else if ( pointerOffsetY >= scrollElement . clientHeight && ( scrollElement . scrollTop + scrollElement . clientHeight ) < scrollElement . scrollHeight ) { scrollY = Math . max ( pointerOffsetY - scrollElement . clientHeight , minScroll ) ; } return { x : scrollX , y : scrollY } ; } private startScrolling ( scrollElement ) { if ( ! this . _scrollIntervalHandle ) { this . _scrollIntervalHandle = window . setInterval ( ( ) => this . performScroll ( scrollElement ) , 100 ) ; } } private stopScrolling ( ) { if ( this . _scrollIntervalHandle ) { clearInterval ( this . _scrollIntervalHandle ) ; this . _scrollIntervalHandle = 0 ; } } private performScroll ( scrollElement ) { scrollElement . scrollLeft += this . _scrollOffset . x ; scrollElement . scrollTop += this . _scrollOffset . y ; } }