added multiple selected objects
This commit is contained in:
@@ -31,25 +31,11 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
search,
|
search,
|
||||||
data,
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
||||||
const [draggedNode, setDraggedNode] = useState<AssetGroupChild | null>(null);
|
const [draggedNode, setDraggedNode] = useState<AssetGroupChild | null>(null);
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
const [hierarchy, setHierarchy] = useState<AssetGroupChild[]>(data);
|
const [hierarchy, setHierarchy] = useState<AssetGroupChild[]>(data);
|
||||||
|
const [selectedObject, setSelectedObject] = useState<AssetGroupChild | null>(null);
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent): void => {
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
if (!target.closest(".outline-card")) {
|
|
||||||
setSelectedId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragStart = (item: AssetGroupChild) => {
|
const handleDragStart = (item: AssetGroupChild) => {
|
||||||
setDraggedNode(item);
|
setDraggedNode(item);
|
||||||
@@ -75,7 +61,7 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
const y = event.clientY - rect.top;
|
const y = event.clientY - rect.top;
|
||||||
const dropZone = getDropZone(y);
|
const dropZone = getDropZone(y);
|
||||||
|
|
||||||
switch (dropZone || selectedId === null) {
|
switch (dropZone || selectedObject === null) {
|
||||||
case "above":
|
case "above":
|
||||||
hoveredDiv.style.borderTop = "2px solid purple";
|
hoveredDiv.style.borderTop = "2px solid purple";
|
||||||
break;
|
break;
|
||||||
@@ -264,7 +250,7 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
<div
|
<div
|
||||||
className="outline-overlay"
|
className="outline-overlay"
|
||||||
style={{
|
style={{
|
||||||
justifyContent: panelSide === "left" ? "flex-start" : "flex-end",
|
justifyContent: panelSide || DEFAULT_PRAMS.panelSide,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -313,8 +299,8 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
draggingItem={draggedNode}
|
draggingItem={draggedNode}
|
||||||
selectedObject={selectedId}
|
setSelectedObject={setSelectedObject}
|
||||||
setSelectedObject={setSelectedId}
|
selectedObject={selectedObject}
|
||||||
onRename={handleRename}
|
onRename={handleRename}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
}, [inputValue, debounced, onChange]);
|
}, [inputValue, debounced, onChange]);
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
console.warn("Search field cleared.");
|
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
onChange?.("");
|
onChange?.("");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { TouchEventHandler, useEffect, useState } from "react";
|
||||||
import { DEFAULT_PRAMS } from "./OutlineList ";
|
import { DEFAULT_PRAMS } from "./OutlineList ";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
@@ -22,9 +22,9 @@ interface TreeNodeProps {
|
|||||||
onRename: (id: string, item: string) => void;
|
onRename: (id: string, item: string) => void;
|
||||||
onDrop: (item: AssetGroupChild, parent: AssetGroupChild | null, e: any) => void;
|
onDrop: (item: AssetGroupChild, parent: AssetGroupChild | null, e: any) => void;
|
||||||
draggingItem: AssetGroupChild | null;
|
draggingItem: AssetGroupChild | null;
|
||||||
selectedObject: string | null;
|
selectedObject: AssetGroupChild | null;
|
||||||
onDragOver: (item: AssetGroupChild, parent: AssetGroupChild | null, e: React.DragEvent) => void;
|
onDragOver: (item: AssetGroupChild, parent: AssetGroupChild | null, e: React.DragEvent) => void;
|
||||||
setSelectedObject: React.Dispatch<React.SetStateAction<string | null>>;
|
setSelectedObject: React.Dispatch<React.SetStateAction<AssetGroupChild | null>>;
|
||||||
}
|
}
|
||||||
const TreeNode: React.FC<TreeNodeProps> = ({
|
const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
item,
|
item,
|
||||||
@@ -47,6 +47,22 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||||||
const [isLocked, setIsLocked] = useState(item.isLocked ?? false);
|
const [isLocked, setIsLocked] = useState(item.isLocked ?? false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [name, setName] = useState(item.groupName || item.modelName);
|
const [name, setName] = useState(item.groupName || item.modelName);
|
||||||
|
const [selectedObjects, setSelectedObjects] = useState<AssetGroupChild[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent): void => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (!target.closest(".outline-card")) {
|
||||||
|
setSelectedObjects([]);
|
||||||
|
setSelectedObject(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDragStart = (e: React.DragEvent) => {
|
const handleDragStart = (e: React.DragEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -82,12 +98,50 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle selection (used by mouse click, keyboard and touch)
|
const toggleSelect = (event?: React.MouseEvent | React.KeyboardEvent) => {
|
||||||
const toggleSelect = () => {
|
event?.stopPropagation?.();
|
||||||
const currentId = item.modelUuid || item.groupUuid || null;
|
|
||||||
setSelectedObject((prev) => (prev === currentId ? null : currentId));
|
const currentId = item.modelUuid || item.groupUuid;
|
||||||
|
const isMultiSelect =
|
||||||
|
!!event &&
|
||||||
|
("ctrlKey" in event || "metaKey" in event) &&
|
||||||
|
(event.ctrlKey || event.metaKey);
|
||||||
|
|
||||||
|
if (isMultiSelect) {
|
||||||
|
setSelectedObjects((prevSelected) => {
|
||||||
|
const isAlreadySelected = prevSelected.some(
|
||||||
|
(obj) => obj.modelUuid === currentId || obj.groupUuid === currentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAlreadySelected) {
|
||||||
|
return prevSelected.filter(
|
||||||
|
(obj) => obj.modelUuid !== currentId && obj.groupUuid !== currentId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return [...prevSelected, item];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedObject(item);
|
||||||
|
} else {
|
||||||
|
setSelectedObject(item);
|
||||||
|
setSelectedObjects([]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMultiSelected = (() => {
|
||||||
|
const totalSelectedItems = selectedObjects.length;
|
||||||
|
if (totalSelectedItems <= 1) return false;
|
||||||
|
|
||||||
|
if (isGroupNode) {
|
||||||
|
// Group selection
|
||||||
|
return selectedObjects.some((obj) => obj.groupUuid === item.groupUuid);
|
||||||
|
} else {
|
||||||
|
// Asset selection
|
||||||
|
return selectedObjects.some((obj) => obj.modelUuid === item.modelUuid);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const handleDivKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
const handleDivKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -97,7 +151,9 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchStart = () => {
|
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
toggleSelect();
|
toggleSelect();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,7 +165,11 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||||||
className={clsx("tree-node-content", {
|
className={clsx("tree-node-content", {
|
||||||
"group-node": isGroupNode,
|
"group-node": isGroupNode,
|
||||||
"asset-node": !isGroupNode,
|
"asset-node": !isGroupNode,
|
||||||
selected: selectedObject === item.modelUuid,
|
"multi-selected": selectedObjects.some(
|
||||||
|
(obj) =>
|
||||||
|
obj.modelUuid === item.modelUuid || obj.groupUuid === item.groupUuid
|
||||||
|
),
|
||||||
|
selected: selectedObject?.modelUuid === item.modelUuid,
|
||||||
locked: isLocked,
|
locked: isLocked,
|
||||||
hidden: !isVisible,
|
hidden: !isVisible,
|
||||||
dragging: isBeingDragged,
|
dragging: isBeingDragged,
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { OutlineListData } from "../data/OutlineListData";
|
|||||||
const SceneView = () => {
|
const SceneView = () => {
|
||||||
return (
|
return (
|
||||||
<OutlinePanel
|
<OutlinePanel
|
||||||
// height="1000px"
|
// height="500px"
|
||||||
// width="120px"
|
// width="420px"
|
||||||
// panelSide="right"
|
panelSide="right"
|
||||||
// textColor=""
|
// textColor=""
|
||||||
// addIconColor=""
|
// addIconColor=""
|
||||||
// eyeIconColor=""
|
// eyeIconColor=""
|
||||||
// backgroundColor=""
|
// backgroundColor=""
|
||||||
search={true}
|
// search={true}
|
||||||
data={OutlineListData}
|
data={OutlineListData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ $icon-size: 16px;
|
|||||||
$node-height: 28px;
|
$node-height: 28px;
|
||||||
$spacing-unit: 8px;
|
$spacing-unit: 8px;
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
// Mixins
|
// Mixins
|
||||||
@mixin flex-center {
|
@mixin flex-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -31,6 +38,7 @@ $spacing-unit: 8px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.outline-overlay {
|
.outline-overlay {
|
||||||
|
width: 100%;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
font-family: "Segoe UI", sans-serif;
|
font-family: "Segoe UI", sans-serif;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -227,7 +235,10 @@ $spacing-unit: 8px;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
&.multi-selected {
|
||||||
|
background: #6f42c1;
|
||||||
|
border-radius: 50px;
|
||||||
|
}
|
||||||
&.locked {
|
&.locked {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
Reference in New Issue
Block a user