Skip to content

Commit 331e48f

Browse files
committed
add range select for Tree component
1 parent fd46ad4 commit 331e48f

2 files changed

Lines changed: 63 additions & 9 deletions

File tree

packages/@dcl/inspector/src/components/Hierarchy/Hierarchy.tsx

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo } from 'react'
1+
import React, { useCallback, useMemo, useState } from 'react'
22
import { Entity } from '@dcl/ecs'
33

44
import { CAMERA, PLAYER, ROOT } from '../../lib/sdk/tree'
@@ -86,6 +86,7 @@ const Hierarchy: React.FC = () => {
8686
centerViewOnEntity
8787
} = useTree()
8888
const selectedEntities = useEntitiesWith((components) => components.Selection)
89+
const [lastSelectedItem, setLastSelectedItem] = useState<Entity | undefined>(undefined)
8990

9091
const isSelected = useCallback(
9192
(entity: Entity) => {
@@ -94,13 +95,58 @@ const Hierarchy: React.FC = () => {
9495
[selectedEntities]
9596
)
9697

98+
const getAllVisibleEntities = useCallback(() => {
99+
const entities: Entity[] = []
100+
101+
const traverse = (entity: Entity) => {
102+
if (!isHidden(entity)) {
103+
entities.push(entity)
104+
if (isOpen(entity)) {
105+
getChildren(entity).forEach(child => traverse(child))
106+
}
107+
}
108+
}
109+
110+
traverse(ROOT)
111+
112+
return entities
113+
}, [getChildren, isOpen, isHidden])
114+
115+
const handleRangeSelection = useCallback((fromEntity: Entity, toEntity: Entity) => {
116+
const allEntities = getAllVisibleEntities()
117+
const fromIndex = allEntities.findIndex(e => getId(e) === getId(fromEntity))
118+
const toIndex = allEntities.findIndex(e => getId(e) === getId(toEntity))
119+
120+
const startIndex = Math.min(fromIndex, toIndex)
121+
const endIndex = Math.max(fromIndex, toIndex)
122+
123+
allEntities.forEach((entity, index) => {
124+
if (index >= startIndex && index <= endIndex) {
125+
select(entity, index > startIndex) // first item replaces selection, others add to selection
126+
}
127+
})
128+
}, [getAllVisibleEntities, getId, select])
129+
130+
const handleSelect = useCallback((entity: Entity, multiple?: boolean) => {
131+
// if this is a shift-click and we have a last selected item, do range selection
132+
if (multiple && lastSelectedItem && lastSelectedItem !== entity) {
133+
handleRangeSelection(lastSelectedItem, entity)
134+
} else {
135+
select(entity, multiple)
136+
}
137+
}, [select, lastSelectedItem, handleRangeSelection])
138+
139+
const handleLastSelectedChange = useCallback((entity: Entity) => {
140+
if (entity !== lastSelectedItem) setLastSelectedItem(entity)
141+
}, [lastSelectedItem])
142+
97143
const props = {
98144
getExtraContextMenu: ContextMenu,
99145
onAddChild: addChild,
100146
onDrop: setParent,
101147
onRemove: remove,
102148
onRename: rename,
103-
onSelect: select,
149+
onSelect: handleSelect,
104150
onDoubleSelect: centerViewOnEntity,
105151
onSetOpen: setOpen,
106152
onDuplicate: duplicate,
@@ -115,7 +161,8 @@ const Hierarchy: React.FC = () => {
115161
canRemove: canRemove,
116162
canDuplicate: canDuplicate,
117163
canDrag: canDrag,
118-
canReorder: canReorder
164+
canReorder: canReorder,
165+
onLastSelectedChange: handleLastSelectedChange
119166
}
120167

121168
return (

packages/@dcl/inspector/src/components/Tree/Tree.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Props<T> = {
4242
onDuplicate: (value: T) => void
4343
getDragContext?: () => unknown
4444
dndType?: string
45+
onLastSelectedChange?: (value: T) => void
4546
}
4647

4748
type EmptyString = ''
@@ -81,7 +82,8 @@ export function Tree<T>() {
8182
onDoubleSelect,
8283
onSetOpen,
8384
getDragContext = () => ({}),
84-
dndType = 'tree'
85+
dndType = 'tree',
86+
onLastSelectedChange
8587
} = props
8688
const ref = useRef<HTMLDivElement>(null)
8789
const id = getId(value)
@@ -152,12 +154,17 @@ export function Tree<T>() {
152154
const quitInsertMode = () => setInsertMode(false)
153155

154156
const handleSelect = (event: React.MouseEvent) => {
155-
if (event.type === ClickType.CONTEXT_MENU && event.ctrlKey) {
156-
onSelect(value, true)
157-
} else if (event.type === ClickType.CLICK) {
158-
onSelect(value, event.shiftKey)
159-
if (event.detail > 1 && onDoubleSelect) onDoubleSelect(value)
157+
const isCtrlClick = event.type === ClickType.CONTEXT_MENU && event.ctrlKey
158+
const isShiftClick = event.type === ClickType.CLICK && event.shiftKey
159+
const isDoubleClick = event.type === ClickType.CLICK && event.detail > 1 && onDoubleSelect
160+
161+
// update last selected item for non-multiple selections
162+
if (!isCtrlClick && !isShiftClick && onLastSelectedChange) {
163+
onLastSelectedChange(value)
160164
}
165+
166+
onSelect(value, isCtrlClick || isShiftClick)
167+
if (isDoubleClick) onDoubleSelect(value)
161168
}
162169

163170
const handleOpen = (_: React.MouseEvent) => {

0 commit comments

Comments
 (0)