1- import { Icon , Tag } from '@blueprintjs/core' ;
1+ import type { ElementDropTargetEventBasePayload } from '@atlaskit/pragmatic-drag-and-drop/dist/types/adapter/element-adapter.js' ;
2+ import type { ElementDragPayload } from '@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types.js' ;
3+ import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge' ;
4+ import { Colors , Icon , Tag } from '@blueprintjs/core' ;
25import styled from '@emotion/styled' ;
36import type {
47 CSSProperties ,
@@ -15,8 +18,55 @@ import {
1518 useRef ,
1619 useState ,
1720} from 'react' ;
21+ import { createPortal } from 'react-dom' ;
1822
19- import { draggable , dropTargetForElements } from './pdnd.cjs' ;
23+ import { assert } from '../utility/assert.js' ;
24+
25+ import {
26+ attachClosestEdge ,
27+ combine ,
28+ draggable ,
29+ dropTargetForElements ,
30+ extractClosestEdge ,
31+ getReorderDestinationIndex ,
32+ pointerOutsideOfPreview ,
33+ setCustomNativeDragPreview ,
34+ } from './pdnd.cjs' ;
35+
36+ interface DropIndicatorProps {
37+ edge : Edge ;
38+ }
39+
40+ const DropIndicator = styled . div < DropIndicatorProps > `
41+ position: absolute;
42+ z-index: 3;
43+ background-color: ${ Colors . BLUE3 } ;
44+ height: 2px;
45+ left: 0;
46+ right: 0;
47+ pointer-events: none;
48+ ${ ( props ) => props . edge === 'top' && 'top: -1px;' }
49+ ${ ( props ) => props . edge === 'bottom' && 'bottom: -1px;' }
50+ ` ;
51+
52+ const Preview = styled . div `
53+ padding-block: 4px;
54+ padding-inline: 8px;
55+ border-radius: 4px;
56+ background-color: #f4f5f7;
57+ max-width: 360px;
58+ white-space: nowrap;
59+ overflow: hidden;
60+ text-overflow: ellipsis;
61+ ` ;
62+
63+ type DraggableState =
64+ | { type : 'idle' }
65+ | { type : 'preview' ; container : HTMLElement }
66+ | { type : 'dragging' } ;
67+
68+ const idleState : DraggableState = { type : 'idle' } ;
69+ const draggingState : DraggableState = { type : 'dragging' } ;
2070
2171interface SelectionsContextState {
2272 isOverflow : boolean ;
@@ -167,14 +217,13 @@ interface BaseSectionProps {
167217
168218interface SectionItemProps extends BaseSectionProps {
169219 id ?: string ;
220+ index ?: number ;
170221 onClick ?: ( id , event ?: MouseEvent < HTMLDivElement > ) => void ;
171222 children ?: ReactNode | ( ( options : { isOpen ?: boolean } ) => ReactNode ) ;
172223 isOpen : boolean ;
173224 sticky ?: boolean ;
174- onReorder ?: (
175- sourceId : string | undefined ,
176- targetId : string | undefined ,
177- ) => void ;
225+ onReorder ?: ( sourceId : number , targetId : number ) => void ;
226+ dragLabel ?: string ;
178227}
179228
180229interface SectionProps {
@@ -220,6 +269,7 @@ function SectionBody(props: HTMLAttributes<HTMLDivElement>) {
220269
221270function SectionItem ( props : SectionItemProps ) {
222271 const {
272+ dragLabel = props . title ,
223273 id = props . title ,
224274 title,
225275 onClick,
@@ -232,30 +282,116 @@ function SectionItem(props: SectionItemProps) {
232282 sticky = false ,
233283 arrowProps = { hide : false , style : { } } ,
234284 onReorder,
285+ index,
235286 } = props ;
236287
237288 const { isOverflow, matchContentHeight } = useSections ( ) ;
238289
239290 const wrapperRef = useRef < HTMLDivElement > ( null ) ;
240291 const handleRef = useRef < HTMLDivElement > ( null ) ;
292+ const [ state , setState ] = useState < DraggableState > ( idleState ) ;
293+ const [ closestEdge , setClosestEdge ] = useState < Edge | null > ( null ) ;
294+ const isReorderActive = typeof onReorder === 'function' ;
241295
242296 useEffect ( ( ) => {
243- if ( ! wrapperRef . current || ! handleRef . current ) return ;
244-
245- const cleanDrag = draggable ( {
246- element : wrapperRef . current ,
247- dragHandle : handleRef . current ,
248- getInitialData : ( ) => ( { id } ) ,
249- } ) ;
297+ const dragHandle = handleRef . current ;
298+ const element = wrapperRef . current ;
299+ if ( ! element || ! dragHandle || ! isReorderActive ) return ;
300+
301+ const data = { id, index } ;
302+
303+ function canDrop ( { source } : { source : ElementDragPayload } ) : boolean {
304+ return source . data . id !== id ;
305+ }
306+
307+ function dragHandler ( { source, self } : ElementDropTargetEventBasePayload ) {
308+ const isSource = source . element === dragHandle ;
309+ if ( isSource ) {
310+ // eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates
311+ setClosestEdge ( null ) ;
312+ return ;
313+ }
314+
315+ const closestEdge = extractClosestEdge ( self . data ) ;
316+
317+ const sourceIndex = source . data . index as number ;
318+ assert ( typeof sourceIndex === 'number' , 'index is not defined' ) ;
319+ const isItemBeforeSource = index === sourceIndex - 1 ;
320+ const isItemAfterSource = index === sourceIndex + 1 ;
321+
322+ const isDropIndicatorHidden =
323+ ( isItemBeforeSource && closestEdge === 'bottom' ) ||
324+ ( isItemAfterSource && closestEdge === 'top' ) ;
325+
326+ setClosestEdge ( isDropIndicatorHidden ? null : closestEdge ) ;
327+ }
328+
329+ const cleanDrag = combine (
330+ draggable ( {
331+ element,
332+ dragHandle,
333+ getInitialData : ( ) => data ,
334+ onGenerateDragPreview ( { nativeSetDragImage } ) {
335+ setCustomNativeDragPreview ( {
336+ nativeSetDragImage,
337+ getOffset : pointerOutsideOfPreview ( {
338+ x : '8px' ,
339+ y : '8px' ,
340+ } ) ,
341+ render ( { container } ) {
342+ setState ( { type : 'preview' , container } ) ;
343+
344+ return ( ) => setState ( draggingState ) ;
345+ } ,
346+ } ) ;
347+ } ,
348+ onDragStart ( ) {
349+ setState ( draggingState ) ;
350+ } ,
351+ onDrop ( ) {
352+ setState ( idleState ) ;
353+ } ,
354+ } ) ,
355+ dropTargetForElements ( {
356+ element,
357+ canDrop,
358+ getIsSticky : ( ) => false ,
359+ getData ( { input } ) {
360+ return attachClosestEdge ( data , {
361+ element,
362+ input,
363+ allowedEdges : [ 'top' , 'bottom' ] ,
364+ } ) ;
365+ } ,
366+ onDrag : dragHandler ,
367+
368+ onDragLeave ( ) {
369+ setClosestEdge ( null ) ;
370+ } ,
371+ onDrop ( ) {
372+ setClosestEdge ( null ) ;
373+ } ,
374+ } ) ,
375+ ) ;
250376
251377 return ( ) => {
252378 cleanDrag ( ) ;
253379 } ;
254- } , [ id ] ) ;
255-
380+ } , [ id , index , isReorderActive ] ) ;
256381 return (
257- < DroppableSectionWrapper key = { id } id = { id } onReorder = { onReorder } >
258- < div ref = { wrapperRef } >
382+ < DroppableSectionWrapper
383+ key = { id }
384+ id = { id }
385+ onReorder = { onReorder }
386+ index = { index }
387+ >
388+ < div
389+ style = { {
390+ position : 'relative' ,
391+ opacity : state && state . type === 'dragging' ? 0.3 : 1 ,
392+ } }
393+ ref = { wrapperRef }
394+ >
259395 < SectionWrapper
260396 isOpen = { isOpen }
261397 isOverflow = { isOverflow }
@@ -284,7 +420,10 @@ function SectionItem(props: SectionItemProps) {
284420 arrowProps = { arrowProps }
285421 />
286422 < Wrapper isOpen = { isOpen } > { children } </ Wrapper >
423+ { closestEdge && < DropIndicator edge = { closestEdge } /> }
287424 </ SectionWrapper >
425+ { state . type === 'preview' &&
426+ createPortal ( < Preview > { dragLabel } </ Preview > , state . container ) }
288427 </ div >
289428 </ DroppableSectionWrapper >
290429 ) ;
@@ -376,36 +515,60 @@ function MainSectionHeader(props: MainSectionHeaderProps) {
376515 ) ;
377516}
378517
379- interface DroppableProps extends Pick < SectionItemProps , 'id' | 'onReorder' > {
518+ interface DroppableProps
519+ extends Pick < SectionItemProps , 'id' | 'onReorder' | 'index' > {
380520 children : ReactNode ;
381521}
382522
383- function DroppableSectionWrapper ( { id, onReorder, children } : DroppableProps ) {
523+ function DroppableSectionWrapper ( props : DroppableProps ) {
524+ const { id, onReorder, index : indexOfTarget , children } = props ;
384525 const ref = useRef < HTMLDivElement > ( null ) ;
385- const [ isOver , setIsOver ] = useState ( false ) ;
386526
387527 useEffect ( ( ) => {
388- if ( ! ref . current ) return ;
528+ if ( typeof onReorder !== 'function' ) return ;
529+
530+ const element = ref . current ;
531+
532+ if ( ! element ) return ;
389533
390534 return dropTargetForElements ( {
391- element : ref . current ,
392- canDrop : ( { source } ) => source . data ?. id !== id ,
393- onDrop : ( { source } ) => {
394- const sourceId = source . data ?. id as string ;
395- setIsOver ( false ) ;
396- onReorder ?.( sourceId , id ) ;
535+ element,
536+ onDrop : ( { location, source } ) => {
537+ const { index : startIndex } = source . data ;
538+ const target = location . current . dropTargets [ 0 ] ;
539+ if ( ! target ) {
540+ return ;
541+ }
542+
543+ if (
544+ typeof startIndex !== 'number' ||
545+ typeof indexOfTarget !== 'number'
546+ ) {
547+ return ;
548+ }
549+
550+ const targetData = target . data ;
551+ const closestEdgeOfTarget = extractClosestEdge ( targetData ) ;
552+
553+ const finishIndex = getReorderDestinationIndex ( {
554+ startIndex,
555+ closestEdgeOfTarget,
556+ indexOfTarget,
557+ axis : 'vertical' ,
558+ } ) ;
559+
560+ if ( finishIndex === startIndex ) {
561+ // If there would be no change, we skip the update
562+ return ;
563+ }
564+
565+ onReorder ?.( startIndex , finishIndex ) ;
397566 } ,
398- onDragEnter : ( ) => setIsOver ( true ) ,
399- onDragLeave : ( ) => setIsOver ( false ) ,
400567 } ) ;
401- } , [ id , onReorder ] ) ;
568+ } , [ id , indexOfTarget , onReorder ] ) ;
402569
403570 return (
404- < div
405- ref = { ref }
406- data-drop-id = { id }
407- style = { { ...( isOver && { opacity : 0.1 } ) } }
408- >
571+ < div ref = { ref } data-drop-id = { id } >
409572 { children }
410573 </ div >
411574 ) ;
0 commit comments