From 8c01bd6e62fd8cfcf828d3eedb4e1c0cef58c772 Mon Sep 17 00:00:00 2001 From: Poovizhi99 Date: Sat, 25 Oct 2025 11:15:08 +0530 Subject: [PATCH] added multiple selected objects --- src/components/ui/OutlineList .tsx | 24 ++------ src/components/ui/Search.tsx | 1 - src/components/ui/TreeNode.tsx | 78 ++++++++++++++++++++++--- src/pages/SceneView.tsx | 8 +-- src/styles/components/outlinePanel.scss | 13 ++++- 5 files changed, 90 insertions(+), 34 deletions(-) diff --git a/src/components/ui/OutlineList .tsx b/src/components/ui/OutlineList .tsx index a770e98..82660fb 100644 --- a/src/components/ui/OutlineList .tsx +++ b/src/components/ui/OutlineList .tsx @@ -31,25 +31,11 @@ export const OutlineList: React.FC = ({ search, data, }) => { - const [selectedId, setSelectedId] = useState(null); const [isPanelOpen, setIsPanelOpen] = useState(true); const [draggedNode, setDraggedNode] = useState(null); const [searchValue, setSearchValue] = useState(""); const [hierarchy, setHierarchy] = useState(data); - - 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 [selectedObject, setSelectedObject] = useState(null); const handleDragStart = (item: AssetGroupChild) => { setDraggedNode(item); @@ -75,7 +61,7 @@ export const OutlineList: React.FC = ({ const y = event.clientY - rect.top; const dropZone = getDropZone(y); - switch (dropZone || selectedId === null) { + switch (dropZone || selectedObject === null) { case "above": hoveredDiv.style.borderTop = "2px solid purple"; break; @@ -264,7 +250,7 @@ export const OutlineList: React.FC = ({
= ({ onDrop={handleDrop} onDragOver={handleDragOver} draggingItem={draggedNode} - selectedObject={selectedId} - setSelectedObject={setSelectedId} + setSelectedObject={setSelectedObject} + selectedObject={selectedObject} onRename={handleRename} /> ))} diff --git a/src/components/ui/Search.tsx b/src/components/ui/Search.tsx index 2e673b3..554d40a 100644 --- a/src/components/ui/Search.tsx +++ b/src/components/ui/Search.tsx @@ -44,7 +44,6 @@ const Search: React.FC = ({ }, [inputValue, debounced, onChange]); const handleClear = () => { - console.warn("Search field cleared."); setInputValue(""); onChange?.(""); }; diff --git a/src/components/ui/TreeNode.tsx b/src/components/ui/TreeNode.tsx index 3945c29..2062c18 100644 --- a/src/components/ui/TreeNode.tsx +++ b/src/components/ui/TreeNode.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { TouchEventHandler, useEffect, useState } from "react"; import { DEFAULT_PRAMS } from "./OutlineList "; import clsx from "clsx"; import { @@ -22,9 +22,9 @@ interface TreeNodeProps { onRename: (id: string, item: string) => void; onDrop: (item: AssetGroupChild, parent: AssetGroupChild | null, e: any) => void; draggingItem: AssetGroupChild | null; - selectedObject: string | null; + selectedObject: AssetGroupChild | null; onDragOver: (item: AssetGroupChild, parent: AssetGroupChild | null, e: React.DragEvent) => void; - setSelectedObject: React.Dispatch>; + setSelectedObject: React.Dispatch>; } const TreeNode: React.FC = ({ item, @@ -47,6 +47,22 @@ const TreeNode: React.FC = ({ const [isLocked, setIsLocked] = useState(item.isLocked ?? false); const [isEditing, setIsEditing] = useState(false); const [name, setName] = useState(item.groupName || item.modelName); + const [selectedObjects, setSelectedObjects] = useState([]); + + 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) => { e.stopPropagation(); @@ -82,12 +98,50 @@ const TreeNode: React.FC = ({ } }; - // Toggle selection (used by mouse click, keyboard and touch) - const toggleSelect = () => { - const currentId = item.modelUuid || item.groupUuid || null; - setSelectedObject((prev) => (prev === currentId ? null : currentId)); + const toggleSelect = (event?: React.MouseEvent | React.KeyboardEvent) => { + event?.stopPropagation?.(); + + 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) => { if (e.key === "Enter") { e.preventDefault(); @@ -97,7 +151,9 @@ const TreeNode: React.FC = ({ } }; - const handleTouchStart = () => { + const handleTouchStart = (e: React.TouchEvent) => { + e.stopPropagation(); + e.preventDefault(); toggleSelect(); }; @@ -109,7 +165,11 @@ const TreeNode: React.FC = ({ className={clsx("tree-node-content", { "group-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, hidden: !isVisible, dragging: isBeingDragged, diff --git a/src/pages/SceneView.tsx b/src/pages/SceneView.tsx index 48700b7..077df40 100644 --- a/src/pages/SceneView.tsx +++ b/src/pages/SceneView.tsx @@ -4,14 +4,14 @@ import { OutlineListData } from "../data/OutlineListData"; const SceneView = () => { return ( ); diff --git a/src/styles/components/outlinePanel.scss b/src/styles/components/outlinePanel.scss index 3b03f27..0994ab8 100644 --- a/src/styles/components/outlinePanel.scss +++ b/src/styles/components/outlinePanel.scss @@ -10,6 +10,13 @@ $icon-size: 16px; $node-height: 28px; $spacing-unit: 8px; +html, +body, +#root { + width: 100%; + height: 100%; +} + // Mixins @mixin flex-center { display: flex; @@ -31,6 +38,7 @@ $spacing-unit: 8px; } .outline-overlay { + width: 100%; padding: 0 4px; font-family: "Segoe UI", sans-serif; display: flex; @@ -227,7 +235,10 @@ $spacing-unit: 8px; align-items: center; flex: 1; } - + &.multi-selected { + background: #6f42c1; + border-radius: 50px; + } &.locked { opacity: 0.6; cursor: not-allowed;