import { AfterViewInit , ContentChild , Directive , ElementRef , EventEmitter , HostListener , Input , NgZone , OnDestroy , Output , Renderer2 } from "@angular/core" ; import { DndEvent , DragDropData , getDirectChildElement , getDropData , shouldPositionPlaceholderBeforeElement } from "./dnd-utils" ; import { getDndType , getDropEffect , isExternalDrag , setDropEffect } from "./dnd-state" ; import { DropEffect , EffectAllowed } from "./dnd-types" ; export interface DndDropEvent { event : DragEvent ; dropEffect : DropEffect ; isExternal : boolean ; data ? : any ; index ? : number ; type ? : any ; } @ Directive ( { selector : "[dndPlaceholderRef]" } ) export class DndPlaceholderRefDirective { constructor ( public readonly elementRef ) { } } @ Directive ( { selector : "[dndDropzone]" } ) export class DndDropzoneDirective implements AfterViewInit , OnDestroy { @ Input ( ) dndDropzone ? : string [ ] | "" = "" ; @ Input ( ) dndEffectAllowed : EffectAllowed = "uninitialized" ; @ Input ( ) dndAllowExternal : boolean = false ; @ Input ( ) dndHorizontal : boolean = false ; @ Input ( ) dndDragoverClass : string = "dndDragover" ; @ Input ( ) dndDropzoneDisabledClass = "dndDropzoneDisabled" ; @ Output ( ) readonly dndDragover : EventEmitter < DragEvent > = new EventEmitter < DragEvent > ( ) ; @ Output ( ) readonly dndDrop : EventEmitter < DndDropEvent > = new EventEmitter < DndDropEvent > ( ) ; @ ContentChild ( DndPlaceholderRefDirective ) private readonly dndPlaceholderRef ? : DndPlaceholderRefDirective ; private placeholder : Element | null = null ; private disabled : boolean = false ; private readonly dragEnterEventHandler : ( event : DragEvent ) => void = ( event ) => this . onDragEnter ( event ) ; private readonly dragOverEventHandler : ( event : DragEvent ) => void = ( event ) => this . onDragOver ( event ) ; private readonly dragLeaveEventHandler : ( event : DragEvent ) => void = ( event ) => this . onDragLeave ( event ) ; @ Input ( ) set dndDisableIf ( value ) { this . disabled = ! ! value ; if ( this . disabled ) { this . renderer . addClass ( this . elementRef . nativeElement , this . dndDropzoneDisabledClass ) ; } else { this . renderer . removeClass ( this . elementRef . nativeElement , this . dndDropzoneDisabledClass ) ; } } @ Input ( ) set dndDisableDropIf ( value ) { this . dndDisableIf = value ; } constructor ( private ngZone , private elementRef , private renderer ) { } ngAfterViewInit ( ) { this . placeholder = this . tryGetPlaceholder ( ) ; this . removePlaceholderFromDOM ( ) ; this . ngZone . runOutsideAngular ( ( ) => { this . elementRef . nativeElement . addEventListener ( "dragenter" , this . dragEnterEventHandler ) ; this . elementRef . nativeElement . addEventListener ( "dragover" , this . dragOverEventHandler ) ; this . elementRef . nativeElement . addEventListener ( "dragleave" , this . dragLeaveEventHandler ) ; } ) ; } ngOnDestroy ( ) { this . elementRef . nativeElement . removeEventListener ( "dragenter" , this . dragEnterEventHandler ) ; this . elementRef . nativeElement . removeEventListener ( "dragover" , this . dragOverEventHandler ) ; this . elementRef . nativeElement . removeEventListener ( "dragleave" , this . dragLeaveEventHandler ) ; } onDragEnter ( event ) { if ( event . _dndDropzoneActive === true ) { this . cleanupDragoverState ( ) ; return ; } if ( typeof event . _dndDropzoneActive === "undefined" ) { const newTarget = document . elementFromPoint ( event . clientX , event . clientY ) ; if ( this . elementRef . nativeElement . contains ( newTarget ) ) { event . _dndDropzoneActive = true ; } } const type = getDndType ( event ) ; if ( this . isDropAllowed ( type ) === false ) { return ; } event . preventDefault ( ) ; } onDragOver ( event ) { if ( event . defaultPrevented ) { return ; } const type = getDndType ( event ) ; if ( this . isDropAllowed ( type ) === false ) { return ; } this . checkAndUpdatePlaceholderPosition ( event ) ; const dropEffect = getDropEffect ( event , this . dndEffectAllowed ) ; if ( dropEffect === "none" ) { this . cleanupDragoverState ( ) ; return ; } event . preventDefault ( ) ; setDropEffect ( event , dropEffect ) ; this . dndDragover . emit ( event ) ; this . renderer . addClass ( this . elementRef . nativeElement , this . dndDragoverClass ) ; } @ HostListener ( "drop" , [ "$event" ] ) onDrop ( event ) { try { const type = getDndType ( event ) ; if ( this . isDropAllowed ( type ) === false ) { return ; } const data = getDropData ( event , isExternalDrag ( ) ) ; if ( this . isDropAllowed ( data . type ) === false ) { return ; } event . preventDefault ( ) ; const dropEffect = getDropEffect ( event ) ; setDropEffect ( event , dropEffect ) ; if ( dropEffect === "none" ) { return ; } const dropIndex = this . getPlaceholderIndex ( ) ; if ( dropIndex === - 1 ) { return ; } this . dndDrop . emit ( { event : event , dropEffect : dropEffect , isExternal : isExternalDrag ( ) , data : data . data , index : dropIndex , type : type , } ) ; event . stopPropagation ( ) ; } finally { this . cleanupDragoverState ( ) ; } } onDragLeave ( event ) { if ( typeof event . _dndDropzoneActive === "undefined" ) { const newTarget = document . elementFromPoint ( event . clientX , event . clientY ) ; if ( this . elementRef . nativeElement . contains ( newTarget ) ) { event . _dndDropzoneActive = true ; return ; } } this . cleanupDragoverState ( ) ; setDropEffect ( event , "none" ) ; } private isDropAllowed ( type ? ) { if ( this . disabled === true ) { return false ; } if ( isExternalDrag ( ) === true && this . dndAllowExternal === false ) { return false ; } if ( ! this . dndDropzone ) { return true ; } if ( ! type ) { return true ; } if ( Array . isArray ( this . dndDropzone ) === false ) { throw new Error ( "dndDropzone: bound value to [dndDropzone] must be an array!" ) ; } return this . dndDropzone . indexOf ( type ) !== - 1 ; } private tryGetPlaceholder ( ) { if ( typeof this . dndPlaceholderRef !== "undefined" ) { return this . dndPlaceholderRef . elementRef . nativeElement as Element ; } return this . elementRef . nativeElement . querySelector ( "[dndPlaceholderRef]" ) ; } private removePlaceholderFromDOM ( ) { if ( this . placeholder !== null && this . placeholder . parentNode !== null ) { this . placeholder . parentNode . removeChild ( this . placeholder ) ; } } private checkAndUpdatePlaceholderPosition ( event ) { if ( this . placeholder === null ) { return ; } if ( this . placeholder . parentNode !== this . elementRef . nativeElement ) { this . renderer . appendChild ( this . elementRef . nativeElement , this . placeholder ) ; } const directChild = getDirectChildElement ( this . elementRef . nativeElement , event . target as Element ) ; if ( directChild === null || directChild === this . placeholder ) { return ; } const positionPlaceholderBeforeDirectChild = shouldPositionPlaceholderBeforeElement ( event , directChild , this . dndHorizontal ) ; if ( positionPlaceholderBeforeDirectChild ) { if ( directChild . previousSibling !== this . placeholder ) { this . renderer . insertBefore ( this . elementRef . nativeElement , this . placeholder , directChild ) ; } } else { if ( directChild . nextSibling !== this . placeholder ) { this . renderer . insertBefore ( this . elementRef . nativeElement , this . placeholder , directChild . nextSibling ) ; } } } private getPlaceholderIndex ( ) { if ( this . placeholder === null ) { return undefined ; } const element = this . elementRef . nativeElement as HTMLElement ; return Array . prototype . indexOf . call ( element . children , this . placeholder ) ; } private cleanupDragoverState ( ) { this . renderer . removeClass ( this . elementRef . nativeElement , this . dndDragoverClass ) ; this . removePlaceholderFromDOM ( ) ; } }