added multiple selected objects

This commit is contained in:
2025-10-25 11:15:08 +05:30
parent 4a3474cb4e
commit 8c01bd6e62
5 changed files with 90 additions and 34 deletions

View File

@@ -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}
/> />
))} ))}

View File

@@ -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?.("");
}; };

View File

@@ -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,

View File

@@ -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}
/> />
); );

View File

@@ -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;