import { Directive , Input , OnDestroy , OnInit , Output , ElementRef , HostListener , EventEmitter } from '@angular/core' ; import { DndState , DndListSettings , DndStateConfig , ALL_EFFECTS , MIME_TYPE , EDGE_MIME_TYPE , MSIE_MIME_TYPE , } from '../services' ; import { Subject } from 'rxjs' ; export const dropAccepted = new Subject ( ) ; @ Directive ( { selector : '[dndList]' , } ) export class DndList implements OnInit , OnDestroy { @ Input ( 'dndList' ) public option : DndListSettings = { disabled : false , effectAllowed : 'move' , allowedTypes : undefined , } ; @ Input ( 'dndModel' ) public dndModel : any [ ] ; @ Input ( ) public set dndPlaceholder ( placeholder ) { this . placeholder = placeholder ; placeholder . remove ( ) ; } @ Output ( 'dndDragOver' ) public dndDragOver : EventEmitter < any > = new EventEmitter ( ) ; @ Output ( 'dndDrop' ) public dndDrop : EventEmitter < any > = new EventEmitter ( ) ; @ Output ( 'dndInserted' ) public dndInserted : EventEmitter < any > = new EventEmitter ( ) ; private dragState : DndStateConfig ; private nativeElement : HTMLElement ; private listSettings : { } = { } ; private placeholder : Element ; constructor ( readonly element , private readonly dndState ) { this . dragState = dndState . dragState ; this . nativeElement = element . nativeElement ; this . placeholder = this . getPlaceholderElement ( ) ; } public ngOnInit ( ) { } public ngOnDestroy ( ) { } @ HostListener ( 'dragenter' , [ '$event' ] ) public handleDragEnter ( event ) { event = event [ 'originalEvent' ] || event ; const mimeType = this . getMimeType ( event . dataTransfer . types ) ; if ( ! mimeType || ! this . isDropAllowed ( this . getItemType ( mimeType ) ) ) { return true ; } event . preventDefault ( ) ; return false ; } @ HostListener ( 'dragover' , [ '$event' ] ) public handleDragOver ( event ) { event = event [ 'originalEvent' ] || event ; const mimeType = this . getMimeType ( event . dataTransfer . types ) ; const itemType = this . getItemType ( mimeType ) ; if ( ! mimeType || ! this . isDropAllowed ( itemType ) ) { return true ; } if ( this . placeholder . parentNode !== this . nativeElement ) { this . nativeElement . appendChild ( this . placeholder ) ; } if ( event . target !== this . nativeElement ) { let listItemNode = event . target as Node ; while ( listItemNode . parentNode !== this . nativeElement && listItemNode . parentNode ) { listItemNode = listItemNode . parentNode ; } if ( listItemNode . parentNode === this . nativeElement && listItemNode !== this . placeholder ) { let isFirstHalf ; const rect = ( listItemNode as Element ) . getBoundingClientRect ( ) ; if ( this . option && this . option . horizontal ) { isFirstHalf = event . clientX < rect . left + rect . width / 2 ; } else { isFirstHalf = event . clientY < rect . top + rect . height / 2 ; } this . nativeElement . insertBefore ( this . placeholder , isFirstHalf ? listItemNode : listItemNode . nextSibling ) ; } } let ignoreDataTransfer = mimeType === MSIE_MIME_TYPE ; let dropEffect = this . getDropEffect ( event , ignoreDataTransfer ) ; if ( dropEffect === 'none' ) return this . stopDragOver ( ) ; event . preventDefault ( ) ; if ( ! ignoreDataTransfer ) { event . dataTransfer . dropEffect = dropEffect ; } this . nativeElement . classList . add ( 'dndDragover' ) ; event . stopPropagation ( ) ; return false ; } @ HostListener ( 'drop' , [ '$event' ] ) public handleDrop ( event ) { event = event [ 'originalEvent' ] || event ; let mimeType = this . getMimeType ( event . dataTransfer . types ) ; let itemType = this . getItemType ( mimeType ) ; if ( ! mimeType || ! this . isDropAllowed ( itemType ) ) return true ; event . preventDefault ( ) ; let data = undefined ; try { data = JSON . parse ( event . dataTransfer . getData ( mimeType ) ) ; } catch ( e ) { return this . stopDragOver ( ) ; } if ( mimeType === MSIE_MIME_TYPE || mimeType === EDGE_MIME_TYPE ) { itemType = data . type || undefined ; data = data . item ; if ( ! this . isDropAllowed ( itemType ) ) return this . stopDragOver ( ) ; } let ignoreDataTransfer = mimeType === MSIE_MIME_TYPE ; let dropEffect = this . getDropEffect ( event , ignoreDataTransfer ) ; if ( dropEffect === 'none' ) return this . stopDragOver ( ) ; let index = this . getPlaceholderIndex ( ) ; let offset = this . nativeElement . children . length - 1 - this . dndModel . length ; if ( this . dndDrop ) { this . invokeCallback ( this . dndDrop , event , dropEffect , itemType , index , data ) ; if ( ! data ) return this . stopDragOver ( ) ; } this . dragState . dropEffect = dropEffect ; if ( ! ignoreDataTransfer ) { event . dataTransfer . dropEffect = dropEffect ; } if ( data !== true ) { let insertionPoint = index - offset ; if ( insertionPoint < 0 ) { insertionPoint = 0 ; } this . dndModel . splice ( insertionPoint , 0 , data ) ; } this . invokeCallback ( this . dndInserted , event , dropEffect , itemType , index , data ) ; dropAccepted . next ( { item : data , list : this . dndModel } ) ; this . stopDragOver ( ) ; event . stopPropagation ( ) ; return false ; } @ HostListener ( 'dragleave' , [ '$event' ] ) public handleDragLeave ( event ) { event = event [ 'originalEvent' ] || event ; let newTarget = document . elementFromPoint ( event . clientX , event . clientY ) ; if ( this . nativeElement . contains ( newTarget ) && ! event [ '_dndPhShown' ] ) { event [ '_dndPhShown' ] = true ; } else { this . stopDragOver ( ) ; } } private getPlaceholderElement ( ) { let placeholder = undefined ; if ( this . nativeElement . children ) { for ( let i = 1 ; i < this . nativeElement . children . length ; i ++ ) { let child = this . nativeElement . children . item ( i ) ; if ( child . classList . contains ( 'dndPlaceholder' ) ) { placeholder = child ; } } } let placeholderDefault = document . createElement ( 'li' ) ; placeholderDefault . classList . add ( 'dndPlaceholder' ) ; return placeholder || placeholderDefault ; } /**
     * Given the types array from the DataTransfer object, returns the first valid mime type.
     * A type is valid if it starts with MIME_TYPE, or it equals MSIE_MIME_TYPE or EDGE_MIME_TYPE.
     */ private getMimeType ( types ) { if ( ! types ) return MSIE_MIME_TYPE ; for ( let i = 0 ; i < types . length ; i ++ ) { if ( types [ i ] === MSIE_MIME_TYPE || types [ i ] === EDGE_MIME_TYPE || types [ i ] . substr ( 0 , MIME_TYPE . length ) === MIME_TYPE ) { return types [ i ] ; } } return null ; } /**
     * Determines the type of the item from the dndState, or from the mime type for items from
     * external sources. Returns undefined if no item type was set and null if the item type could
     * not be determined.
     */ private getItemType ( mimeType ) { if ( this . dragState . isDragging ) return this . dragState . itemType || undefined ; if ( mimeType === MSIE_MIME_TYPE || mimeType === EDGE_MIME_TYPE ) return null ; return ( mimeType && mimeType . substr ( MIME_TYPE . length + 1 ) ) || undefined ; } /**
     * Checks various conditions that must be fulfilled for a drop to be allowed, including the
     * dnd-allowed-types attribute. If the item Type is unknown (null), the drop will be allowed.
     */ private isDropAllowed ( itemType ) { if ( this . option ) { if ( this . option . disabled ) return false ; if ( this . option . max && this . dndModel . length === this . option . max ) return false ; if ( ! this . option . externalSources && ! this . dragState . isDragging ) return false ; if ( ! this . option . allowedTypes || itemType === null ) return true ; } return itemType && this . option . allowedTypes . indexOf ( itemType ) !== - 1 ; } /**
     * Determines which drop effect to use for the given event. In Internet Explorer we have to
     * ignore the effectAllowed field on dataTransfer, since we set a fake value in dragstart.
     * In those cases we rely on dndState to filter effects. Read the design doc for more details:
     * https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Data-Transfer-Design
     */ private getDropEffect ( event , ignoreDataTransfer ) { let effects = Object . assign ( [ ] , ALL_EFFECTS ) ; if ( ! ignoreDataTransfer ) { effects = this . dndState . filterEffects ( effects , event . dataTransfer . effectAllowed ) ; } if ( this . dragState . isDragging ) { effects = this . dndState . filterEffects ( effects , this . dragState . effectAllowed ) ; } if ( this . option && this . option . effectAllowed ) { effects = this . dndState . filterEffects ( effects , this . option . effectAllowed ) ; } if ( ! effects . length ) { return 'none' ; } else if ( event . ctrlKey && effects . indexOf ( 'copy' ) !== - 1 ) { return 'copy' ; } else if ( event . altKey && effects . indexOf ( 'link' ) !== - 1 ) { return 'link' ; } else { return effects [ 0 ] ; } } /**
     * Small helper function that cleans up if we aborted a drop.
     */ private stopDragOver ( ) { this . placeholder . remove ( ) ; this . nativeElement . classList . remove ( 'dndDragover' ) ; return true ; } /**
     * Invokes a callback with some interesting parameters and returns the callbacks return value.
     */ private invokeCallback ( eventEmitter , event , dropEffect , itemType , index ? , item ? ) { eventEmitter . emit ( { dropEffect : dropEffect , event : event , external : ! this . dragState . isDragging , index : index !== undefined ? index : this . getPlaceholderIndex ( ) , item : item || undefined , type : itemType , } ) ; return true ; } /**
     * We use the position of the placeholder node to determine at which position of the array the
     * object needs to be inserted
     */ private getPlaceholderIndex ( ) { for ( let i = 0 ; i < this . nativeElement . children . length ; i ++ ) { if ( this . nativeElement . children [ i ] . classList . contains ( 'dndDragging' ) ) { this . nativeElement . children [ i ] . remove ( ) ; break ; } } return Array . prototype . indexOf . call ( this . nativeElement . children , this . placeholder ) ; } }