Skip to content

Commit dba30e9

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

3 files changed

Lines changed: 73 additions & 11 deletions

File tree

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

Lines changed: 59 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,67 @@ 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(
116+
(fromEntity: Entity, toEntity: Entity) => {
117+
const allEntities = getAllVisibleEntities()
118+
const fromIndex = allEntities.findIndex((e) => getId(e) === getId(fromEntity))
119+
const toIndex = allEntities.findIndex((e) => getId(e) === getId(toEntity))
120+
121+
const startIndex = Math.min(fromIndex, toIndex)
122+
const endIndex = Math.max(fromIndex, toIndex)
123+
124+
allEntities.forEach((entity, index) => {
125+
if (index >= startIndex && index <= endIndex) {
126+
void select(entity, index > startIndex) // first item replaces selection, others add to selection
127+
}
128+
})
129+
},
130+
[getAllVisibleEntities, getId, select]
131+
)
132+
133+
const handleSelect = useCallback(
134+
(entity: Entity, clickType?: 'single' | 'ctrl' | 'shift') => {
135+
if (clickType === 'shift' && lastSelectedItem && lastSelectedItem !== entity) {
136+
handleRangeSelection(lastSelectedItem, entity)
137+
} else {
138+
const isMultipleSelection = clickType === 'ctrl' || clickType === 'shift'
139+
void select(entity, isMultipleSelection)
140+
}
141+
},
142+
[select, lastSelectedItem, handleRangeSelection]
143+
)
144+
145+
const handleLastSelectedChange = useCallback(
146+
(entity: Entity) => {
147+
if (entity !== lastSelectedItem) setLastSelectedItem(entity)
148+
},
149+
[lastSelectedItem]
150+
)
151+
97152
const props = {
98153
getExtraContextMenu: ContextMenu,
99154
onAddChild: addChild,
100155
onDrop: setParent,
101156
onRemove: remove,
102157
onRename: rename,
103-
onSelect: select,
158+
onSelect: handleSelect,
104159
onDoubleSelect: centerViewOnEntity,
105160
onSetOpen: setOpen,
106161
onDuplicate: duplicate,
@@ -115,7 +170,8 @@ const Hierarchy: React.FC = () => {
115170
canRemove: canRemove,
116171
canDuplicate: canDuplicate,
117172
canDrag: canDrag,
118-
canReorder: canReorder
173+
canReorder: canReorder,
174+
onLastSelectedChange: handleLastSelectedChange
119175
}
120176

121177
return (

packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ function ProjectView({ folders, thumbnails }: Props) {
175175
onCancel={() => setSearch('')}
176176
/>
177177
<FilesTree
178-
tree={tree}
179178
className="editor-assets-tree"
180179
value={ROOT}
181180
onAddChild={noop}

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type Props<T> = {
3333
canDrag?: (value: T) => boolean
3434
canReorder?: (source: T, target: T, type: DropType) => boolean
3535
onSetOpen: (value: T, isOpen: boolean) => void
36-
onSelect: (value: T, multiple?: boolean) => void
36+
onSelect: (value: T, clickType?: 'single' | 'ctrl' | 'shift') => void
3737
onDoubleSelect?: (value: T) => void
3838
onDrop: (source: T, target: T, dropType: DropType) => void
3939
onRename: (value: T, label: string) => void
@@ -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+
const clickType = isCtrlClick ? 'ctrl' : isShiftClick ? 'shift' : 'single'
161+
162+
if (clickType === 'single' && onLastSelectedChange) {
163+
onLastSelectedChange(value)
160164
}
165+
166+
onSelect(value, clickType)
167+
if (isDoubleClick) onDoubleSelect(value)
161168
}
162169

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

0 commit comments

Comments
 (0)