import { getDOMInfo , ROOT_NODE } from '@craftjs/utils' ; import findPosition from './findPosition' ; import { EditorStore } from '../editor/store' ; import { DragTarget , DropPosition , Indicator , Node , NodeId , NodeInfo , NodeSelectorWrapper , } from '../interfaces' ; import { getNodesFromSelector } from '../utils/getNodesFromSelector' ; /**
 * Positioner is responsible for computing the drop Indicator during a sequence of drag-n-drop events
 */ export class Positioner { static BORDER_OFFSET = 10 ; private currentDropTargetId : NodeId | null ; private currentDropTargetCanvasAncestorId : NodeId | null ; private currentIndicator : Indicator | null = null ; private currentTargetId : NodeId | null ; private currentTargetChildDimensions : NodeInfo [ ] | null ; private dragError : string | null ; private draggedNodes : NodeSelectorWrapper [ ] ; private onScrollListener : ( e : Event ) => void ; constructor ( readonly store , readonly dragTarget ) { this . currentDropTargetId = null ; this . currentDropTargetCanvasAncestorId = null ; this . currentTargetId = null ; this . currentTargetChildDimensions = null ; this . currentIndicator = null ; this . dragError = null ; this . draggedNodes = this . getDraggedNodes ( ) ; this . validateDraggedNodes ( ) ; this . onScrollListener = this . onScroll . bind ( this ) ; window . addEventListener ( 'scroll' , this . onScrollListener , true ) ; } cleanup ( ) { window . removeEventListener ( 'scroll' , this . onScrollListener , true ) ; } private onScroll ( e ) { const scrollBody = e . target ; const rootNode = this . store . query . node ( ROOT_NODE ) . get ( ) ; const shouldClearChildDimensionsCache = scrollBody instanceof Element && rootNode && rootNode . dom && scrollBody . contains ( rootNode . dom ) ; if ( ! shouldClearChildDimensionsCache ) { return ; } this . currentTargetChildDimensions = null ; } private getDraggedNodes ( ) { if ( this . dragTarget . type === 'new' ) { return getNodesFromSelector ( this . store . query . getNodes ( ) , this . dragTarget . tree . nodes [ this . dragTarget . tree . rootNodeId ] ) ; } return getNodesFromSelector ( this . store . query . getNodes ( ) , this . dragTarget . nodes ) ; } private validateDraggedNodes ( ) { if ( this . dragTarget . type === 'new' ) { return ; } this . draggedNodes . forEach ( ( { node , exists } ) => { if ( ! exists ) { return ; } this . store . query . node ( node . id ) . isDraggable ( ( err ) => { this . dragError = err ; } ) ; } ) ; } private isNearBorders ( domInfo , x , y ) { const { top , bottom , left , right } = domInfo ; if ( top + Positioner . BORDER_OFFSET > y || bottom - Positioner . BORDER_OFFSET < y || left + Positioner . BORDER_OFFSET > x || right - Positioner . BORDER_OFFSET < x ) { return true ; } return false ; } private isDiff ( newPosition ) { if ( this . currentIndicator && this . currentIndicator . placement . parent . id === newPosition . parent . id && this . currentIndicator . placement . index === newPosition . index && this . currentIndicator . placement . where === newPosition . where ) { return false ; } return true ; } /**
     * Get dimensions of every child Node in the specified parent Node
     */ private getChildDimensions ( newParentNode ) { const existingTargetChildDimensions = this . currentTargetChildDimensions ; if ( this . currentTargetId === newParentNode . id && existingTargetChildDimensions ) { return existingTargetChildDimensions ; } return newParentNode . data . nodes . reduce ( ( result , id ) => { const dom = this . store . query . node ( id ) . get ( ) . dom ; if ( dom ) { result . push ( { id , ... getDOMInfo ( dom ) , } ) ; } return result ; } , [ ] as NodeInfo [ ] ) ; } /**
     * Get closest Canvas node relative to the dropTargetId
     * Return dropTargetId if it itself is a Canvas node
     *
     * In most cases it will be the dropTarget itself or its immediate parent.
     * We typically only need to traverse 2 levels or more if the dropTarget is a linked node
     *
     * TODO: We should probably have some special rules to handle linked nodes
     */ private getCanvasAncestor ( dropTargetId ) { if ( dropTargetId === this . currentDropTargetId && this . currentDropTargetCanvasAncestorId ) { const node = this . store . query . node ( this . currentDropTargetCanvasAncestorId ) . get ( ) ; if ( node ) { return node ; } } const getCanvas = ( nodeId ) : Node => { const node = this . store . query . node ( nodeId ) . get ( ) ; if ( node && node . data . isCanvas ) { return node ; } if ( ! node . data . parent ) { return null ; } return getCanvas ( node . data . parent ) ; } ; return getCanvas ( dropTargetId ) ; } /**
     * Compute a new Indicator object based on the dropTarget and x,y coords
     * Returns null if theres no change from the previous Indicator
     */ computeIndicator ( dropTargetId , x , y ) { let newParentNode = this . getCanvasAncestor ( dropTargetId ) ; if ( ! newParentNode ) { return ; } this . currentDropTargetId = dropTargetId ; this . currentDropTargetCanvasAncestorId = newParentNode . id ; if ( newParentNode . data . parent && this . isNearBorders ( getDOMInfo ( newParentNode . dom ) , x , y ) && ! this . store . query . node ( newParentNode . id ) . isLinkedNode ( ) ) { newParentNode = this . store . query . node ( newParentNode . data . parent ) . get ( ) ; } if ( ! newParentNode ) { return ; } this . currentTargetChildDimensions = this . getChildDimensions ( newParentNode ) ; this . currentTargetId = newParentNode . id ; const position = findPosition ( newParentNode , this . currentTargetChildDimensions , x , y ) ; if ( ! this . isDiff ( position ) ) { return ; } let error = this . dragError ; if ( ! error ) { this . store . query . node ( newParentNode . id ) . isDroppable ( this . draggedNodes . map ( ( sourceNode ) => sourceNode . node ) , ( dropError ) => { error = dropError ; } ) ; } const currentNodeId = newParentNode . data . nodes [ position . index ] ; const currentNode = currentNodeId && this . store . query . node ( currentNodeId ) . get ( ) ; this . currentIndicator = { placement : { ... position , currentNode , } , error , } ; return this . currentIndicator ; } getIndicator ( ) { return this . currentIndicator ; } }