@@ -23,6 +23,7 @@ type Props<T> = {
2323 getChildren : ( value : T ) => T [ ]
2424 getIcon ?: ( value : T ) => JSX . Element
2525 getLabel : ( value : T ) => string | JSX . Element
26+ getSelectedItems ?: ( ) => T [ ]
2627 isOpen : ( value : T ) => boolean
2728 isSelected : ( value : T ) => boolean
2829 isHidden : ( value : T ) => boolean
@@ -33,7 +34,7 @@ type Props<T> = {
3334 canDrag ?: ( value : T ) => boolean
3435 canReorder ?: ( source : T , target : T , type : DropType ) => boolean
3536 onSetOpen : ( value : T , isOpen : boolean ) => void
36- onSelect : ( value : T , multiple ?: boolean ) => void
37+ onSelect : ( value : T , clickType ?: 'single' | 'ctrl' | 'shift' ) => void
3738 onDoubleSelect ?: ( value : T ) => void
3839 onDrop : ( source : T , target : T , dropType : DropType ) => void
3940 onRename : ( value : T , label : string ) => void
@@ -42,13 +43,13 @@ type Props<T> = {
4243 onDuplicate : ( value : T ) => void
4344 getDragContext ?: ( ) => unknown
4445 dndType ?: string
45- tree ?: unknown
46+ onLastSelectedChange ?: ( value : T ) => void
4647}
4748
4849type EmptyString = ''
4950
50- const getDefaultLevel = ( ) => 1
51- const getLevelStyles = ( level : number ) => ( { paddingLeft : `${ ( level - 1 ) * 10 } px` } )
51+ const getDefaultLevel = ( ) => 0
52+ const getLevelStyles = ( level : number ) => ( { paddingLeft : `${ level * 10 } px` } )
5253const getExpandStyles = ( active : boolean ) => ( { height : active ? 'auto' : '0' , overflow : 'hidden' , display : 'block' } )
5354const getEditModeStyles = ( active : boolean ) => ( { display : active ? 'none' : '' } )
5455
@@ -64,6 +65,7 @@ export function Tree<T>() {
6465 getId,
6566 getChildren,
6667 getLabel,
68+ getSelectedItems,
6769 isOpen,
6870 isSelected,
6971 onSelect,
@@ -82,7 +84,8 @@ export function Tree<T>() {
8284 onDoubleSelect,
8385 onSetOpen,
8486 getDragContext = ( ) => ( { } ) ,
85- dndType = 'tree'
87+ dndType = 'tree' ,
88+ onLastSelectedChange
8689 } = props
8790 const ref = useRef < HTMLDivElement > ( null )
8891 const id = getId ( value )
@@ -110,33 +113,65 @@ export function Tree<T>() {
110113 [ getId , getChildren ]
111114 )
112115
113- const [ , drag ] = useDrag (
114- ( ) => ( {
115- type : dndType ,
116- canDrag : enableDrag ,
117- item : { value, context : getDragContext ( ) }
118- } ) ,
119- [ value ]
116+ const canDropMultiple = useCallback (
117+ ( target : T , sources : T [ ] ) : boolean => {
118+ if ( sources . some ( ( source ) => getId ( target ) === getId ( source ) ) ) return false
119+ if ( sources . some ( ( source ) => isDescendantOf ( target , source ) ) ) return false
120+ return getChildren ( target ) . every ( ( $ ) => canDropMultiple ( $ , sources ) )
121+ } ,
122+ [ getId , getChildren ]
123+ )
124+
125+ const isDescendantOf = useCallback (
126+ ( ancestor : T , descendant : T ) : boolean => {
127+ const children = getChildren ( ancestor )
128+ if ( children . some ( ( child ) => getId ( child ) === getId ( descendant ) ) ) return true
129+ return children . some ( ( child ) => isDescendantOf ( child , descendant ) )
130+ } ,
131+ [ getId , getChildren ]
120132 )
121133
122134 const [ { isHover } , drop ] = useDrop (
123135 ( ) => ( {
124136 accept : dndType ,
125- drop : ( { value : item } : { value : T } , monitor ) => {
137+ drop : ( item : { items : T [ ] ; context : unknown } , monitor ) => {
126138 const dropTypeValue = dropType || dropTypeRef . current
127- if ( monitor . didDrop ( ) || ! canDrop ( item , value ) || ! dropTypeValue ) return
128- onDrop ( item , value , dropTypeValue )
139+ if ( monitor . didDrop ( ) || ! dropTypeValue ) return
140+
141+ const { items } = item
142+ const isMultipleDrag = items . length > 1
143+
144+ if ( isMultipleDrag ) {
145+ if ( ! canDropMultiple ( value , items ) ) return
146+ items . forEach ( ( sourceItem ) => onDrop ( sourceItem , value , dropTypeValue ) )
147+ } else {
148+ const sourceItem = items [ 0 ]
149+ if ( ! canDrop ( sourceItem , value ) ) return
150+ onDrop ( sourceItem , value , dropTypeValue )
151+ }
129152 } ,
130- hover : ( { value : item } , monitor ) => {
131- if ( ! ref . current || item === value ) {
153+ hover : ( item : { items : T [ ] ; context : unknown } , monitor ) => {
154+ if ( ! ref . current ) {
155+ dropTypeRef . current = ''
156+ return setDropType ( '' )
157+ }
158+
159+ const { items } = item
160+
161+ // check if hovering over one of the dragged items
162+ if ( items . some ( ( sourceItem ) => getId ( sourceItem ) === getId ( value ) ) ) {
132163 dropTypeRef . current = ''
133164 return setDropType ( '' )
134165 }
135166
136167 const coords = monitor . getClientOffset ( ) as XYCoord
137168 const rect = ref . current . getBoundingClientRect ( )
138169 const dropType = calculateDropType ( coords . y , rect )
139- const enableReorder = canReorder ? canReorder ( item , value , dropType ) : true
170+
171+ const enableReorder = canReorder
172+ ? items . every ( ( sourceItem ) => canReorder ( sourceItem , value , dropType ) )
173+ : true
174+
140175 const newDropTypeValue = enableReorder ? dropType : ''
141176
142177 setDropType ( newDropTypeValue )
@@ -146,19 +181,26 @@ export function Tree<T>() {
146181 isHover : monitor . isOver ( { shallow : true } )
147182 } )
148183 } ) ,
149- [ value , dropType , onDrop , canDrop ]
184+ [ value , dropType , onDrop , canDrop , canDropMultiple , canReorder , getId ]
150185 )
151186
152187 const quitEditMode = ( ) => setEditMode ( false )
153188 const quitInsertMode = ( ) => setInsertMode ( false )
154189
155190 const handleSelect = ( event : React . MouseEvent ) => {
156- if ( event . type === ClickType . CONTEXT_MENU && event . ctrlKey ) {
157- onSelect ( value , true )
158- } else if ( event . type === ClickType . CLICK ) {
159- onSelect ( value , event . shiftKey )
160- if ( event . detail > 1 && onDoubleSelect ) onDoubleSelect ( value )
191+ const isMac = / M a c | i P h o n e | i P o d | i P a d / . test ( navigator . userAgent )
192+ const isCtrlClick =
193+ ( isMac ? event . type === ClickType . CONTEXT_MENU : event . type === ClickType . CLICK ) && event . ctrlKey
194+ const isShiftClick = event . type === ClickType . CLICK && event . shiftKey
195+ const isDoubleClick = event . type === ClickType . CLICK && event . detail > 1 && onDoubleSelect
196+ const clickType = isCtrlClick ? 'ctrl' : isShiftClick ? 'shift' : 'single'
197+
198+ if ( clickType === 'single' && onLastSelectedChange ) {
199+ onLastSelectedChange ( value )
161200 }
201+
202+ onSelect ( value , clickType )
203+ if ( isDoubleClick ) onDoubleSelect ( value )
162204 }
163205
164206 const handleOpen = ( _ : React . MouseEvent ) => {
@@ -186,6 +228,29 @@ export function Tree<T>() {
186228 }
187229
188230 const sdk = useSdk ( )
231+
232+ const [ , drag ] = useDrag (
233+ ( ) => ( {
234+ type : dndType ,
235+ canDrag : enableDrag ,
236+ item : ( ) => {
237+ const selectedItems = getSelectedItems ? getSelectedItems ( ) : [ ]
238+ // if this item is selected and there are multiple selections, drag all selected items
239+ if ( selectedItems . length > 1 && selectedItems . some ( ( item ) => getId ( item ) === getId ( value ) ) ) {
240+ return {
241+ items : selectedItems ,
242+ context : getDragContext ( )
243+ }
244+ }
245+ return {
246+ items : [ value ] ,
247+ context : getDragContext ( )
248+ }
249+ }
250+ } ) ,
251+ [ value , getSelectedItems , getId ]
252+ )
253+
189254 const handleRemove = ( ) => {
190255 if ( isEntity && sdk ) {
191256 const selectedEntities = sdk . operations . getSelectedEntities ( )
@@ -252,13 +317,13 @@ export function Tree<T>() {
252317 < div ref = { ref } style = { getEditModeStyles ( editMode ) } className = "item-area" >
253318 < DisclosureWidget enabled = { enableOpen } isOpen = { open } onOpen = { handleOpen } />
254319 < div onClick = { handleSelect } onContextMenu = { handleSelect } className = "selectable-area" >
255- { props . getIcon ? props . getIcon ( value ) : < > </ > }
320+ { props . getIcon && props . getIcon ( value ) }
256321 < div > { label || id } </ div >
257322 { isEntity && < ActionArea entity = { value as Entity } /> }
258323 </ div >
259324 </ div >
260325 { editMode && typeof label === 'string' && (
261- < EditInput value = { label || '' } onCancel = { quitEditMode } onSubmit = { onChangeEditValue } />
326+ < EditInput value = { label } onCancel = { quitEditMode } onSubmit = { onChangeEditValue } />
262327 ) }
263328 </ div >
264329 < TreeChildren { ...props } />
0 commit comments