From c5a03f69cef6186905c9a7a2195a2086dcd59c1c Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Wed, 30 Jul 2025 15:30:57 +0200 Subject: [PATCH 1/3] Scroll to selected item in models panel --- .../components/viewer/models/Card.vue | 73 +++++++- .../components/viewer/models/TreeItem.vue | 176 ++++++++++++------ 2 files changed, 193 insertions(+), 56 deletions(-) diff --git a/packages/frontend-2/components/viewer/models/Card.vue b/packages/frontend-2/components/viewer/models/Card.vue index fb81f02b2..28afe5959 100644 --- a/packages/frontend-2/components/viewer/models/Card.vue +++ b/packages/frontend-2/components/viewer/models/Card.vue @@ -105,7 +105,7 @@
@@ -164,7 +165,7 @@ const { getTooltipProps } = useSmartTooltipDelay() const { highlightObjects, unhighlightObjects } = useHighlightedObjectsUtilities() const { hideObjects, showObjects, isolateObjects, unIsolateObjects } = useFilterUtilities() -const { setSelectionFromObjectIds } = useSelectionUtilities() +const { setSelectionFromObjectIds, objects } = useSelectionUtilities() const { items } = useInjectedViewerRequestedResources() const { viewer: { @@ -193,6 +194,9 @@ const { load: loadLatestVersion } = useLoadLatestVersion({ const isExpanded = ref(false) const showActionsMenu = ref(false) +// Track expansion state for auto-expanding selected objects +const forceExpandedNodeIds = ref>(new Set()) + const IconEye = resolveComponent('IconEye') const IconEyeClosed = resolveComponent('IconEyeClosed') @@ -410,4 +414,69 @@ const onActionChosen = (params: { item: LayoutMenuItem }) => { break } } + +const expandToShowSelectedObjects = (selectedIds: string[]) => { + // Find paths to all selected objects in this model's tree + const pathsToExpand = findPathsToSelectedObjects(selectedIds) + + if (pathsToExpand.length > 0) { + // Expand the model itself + if (!isExpanded.value) { + isExpanded.value = true + } + + // Clear previous force-expanded nodes and set new ones + forceExpandedNodeIds.value.clear() + const nodeIdsToExpand = pathsToExpand.flat() + nodeIdsToExpand.forEach((id) => forceExpandedNodeIds.value.add(id)) + } +} + +const findPathsToSelectedObjects = (selectedIds: string[]): string[][] => { + const paths: string[][] = [] + + for (const rootNode of props.rootNodes) { + const nodePaths = findPathsInNode(rootNode, selectedIds, []) + paths.push(...nodePaths) + } + + return paths +} + +const findPathsInNode = ( + node: ExplorerNode, + selectedIds: string[], + currentPath: string[] +): string[][] => { + const paths: string[][] = [] + const nodeId = node.raw?.id || node.guid || '' + const newPath = nodeId ? [...currentPath, nodeId] : currentPath + + if (nodeId && selectedIds.includes(nodeId)) { + paths.push(newPath) + } + if (node.children) { + for (const child of node.children) { + const childPaths = findPathsInNode(child, selectedIds, newPath) + paths.push(...childPaths) + } + } + + return paths +} + +// Auto-expand tree to show selected objects +watch( + () => objects.value, + (selectedObjects) => { + const selectedIds = selectedObjects.map((obj: { id: string }) => obj.id) + + if (selectedObjects.length > 0) { + expandToShowSelectedObjects(selectedIds) + } else { + forceExpandedNodeIds.value.clear() + } + }, + { deep: true } +) diff --git a/packages/frontend-2/components/viewer/models/TreeItem.vue b/packages/frontend-2/components/viewer/models/TreeItem.vue index dbf6f4e39..5daf460d3 100644 --- a/packages/frontend-2/components/viewer/models/TreeItem.vue +++ b/packages/frontend-2/components/viewer/models/TreeItem.vue @@ -4,6 +4,7 @@
-
+
}>(), { depth: 0, header: null, subHeader: null, isDescendantOfSelected: false } ) @@ -312,33 +315,6 @@ const isAllowedType = (node: ExplorerNode) => { const unfold = ref(false) -// NOTE: not happy with how unfolding and collapsing panned out :( -// it works, but requiring two different props... phew. -watch( - () => props.expandLevel, - (newVal) => { - if (isSingleCollection.value || isMultipleCollection.value) { - unfold.value = newVal >= props.depth - } - // if (newVal > oldVal) unfold.value = true - // else if (newVal <= props.depth) unfold.value = false - } -) - -watch( - () => props.manualExpandLevel, - (newVal, oldVal) => { - if (!(isSingleCollection.value || isMultipleCollection.value)) return - if ( - newVal < oldVal && - unfold.value && - (isSingleCollection.value || isMultipleCollection.value) && - props.depth > newVal - ) - unfold.value = false - } -) - // Note: we need to emit a manual unfold event with the current depth so we can set it upstream // for the collapse/unfold functionality const manualUnfoldToggle = () => { @@ -367,32 +343,6 @@ const getBackgroundClass = computed(() => { return 'bg-foundation hover:bg-highlight-1' }) -const setSelection = (e: MouseEvent) => { - if (isSelected.value && !e.shiftKey) { - // If already selected, try to expand first before deselecting - if ((isSingleCollection.value || isMultipleCollection.value) && !unfold.value) { - unfold.value = true - emit('expanded', props.depth) - return - } - // Only deselect if can't expand or already expanded - clearSelection() - return - } - if (isSelected.value && e.shiftKey) { - removeFromSelection(rawSpeckleData.value) - return - } - if (!e.shiftKey) clearSelection() - addToSelection(rawSpeckleData.value) - - // Auto-expand when selecting if it has children - if ((isSingleCollection.value || isMultipleCollection.value) && !unfold.value) { - unfold.value = true - emit('expanded', props.depth) - } -} - const highlightObject = () => { highlightObjects(getTargetObjectIds(rawSpeckleData.value)) } @@ -444,4 +394,122 @@ const isolateOrUnisolateObject = () => { unIsolateObjects(ids) } + +const setSelection = (e: MouseEvent) => { + disableScrollOnNextSelection.value = true + + if (isSelected.value && !e.shiftKey) { + // If already selected, try to expand if possible, but don't deselect + if ((isSingleCollection.value || isMultipleCollection.value) && !unfold.value) { + unfold.value = true + emit('expanded', props.depth) + } + disableScrollOnNextSelection.value = false + return + } + if (isSelected.value && e.shiftKey) { + removeFromSelection(rawSpeckleData.value) + disableScrollOnNextSelection.value = false + return + } + if (!e.shiftKey) clearSelection() + addToSelection(rawSpeckleData.value) + + // Auto-expand when selecting if it has children + if ((isSingleCollection.value || isMultipleCollection.value) && !unfold.value) { + unfold.value = true + emit('expanded', props.depth) + } +} + +const ensureSelectedItemsVisible = () => { + const selectedIds = objects.value.map((obj: { id: string }) => obj.id) + if (selectedIds.length === 0) return + + // Check if any selected items are in this container but beyond current pagination + const allItems = singleCollectionItems.value + const selectedIndices = allItems + .map((item, index) => ({ item, index })) + .filter(({ item }) => { + const itemId = item.rawNode.raw?.id + return itemId && selectedIds.includes(itemId) + }) + .map(({ index }) => index) + + if (selectedIndices.length > 0) { + const maxSelectedIndex = Math.max(...selectedIndices) + // Ensure itemCount is high enough to show the selected item + if (maxSelectedIndex >= itemCount.value) { + itemCount.value = Math.max(maxSelectedIndex + 1, itemCount.value) + } + } +} + +const checkAndExpand = () => { + const nodeId = rawSpeckleData.value.id + const shouldForceExpand = props.forceExpandedNodeIds?.has(nodeId) + + if ( + (isSingleCollection.value || isMultipleCollection.value) && + (props.expandLevel >= props.depth || shouldForceExpand) + ) { + unfold.value = true + ensureSelectedItemsVisible() + } +} + +const headerElement = ref() +const disableScrollOnNextSelection = ref(false) + +onMounted(() => { + checkAndExpand() + + const nodeId = rawSpeckleData.value.id + const isForceExpanded = props.forceExpandedNodeIds?.has(nodeId) + + if ( + isSelected.value && + isForceExpanded && + headerElement.value && + !disableScrollOnNextSelection.value + ) { + headerElement.value.scrollIntoView({ + behavior: 'instant', + block: 'center' + }) + } + + disableScrollOnNextSelection.value = false +}) + +watch( + () => props.forceExpandedNodeIds, + () => { + checkAndExpand() + }, + { deep: true } +) + +watch( + () => isSelected.value, + (newIsSelected, oldIsSelected) => { + if (newIsSelected && !oldIsSelected) { + const nodeId = rawSpeckleData.value.id + const isForceExpanded = props.forceExpandedNodeIds?.has(nodeId) + + if ( + isForceExpanded && + headerElement.value && + !disableScrollOnNextSelection.value + ) { + headerElement.value.scrollIntoView({ + behavior: 'instant', + block: 'center' + }) + } + + disableScrollOnNextSelection.value = false + } + } +) From 949de347193b01049679aaab60fc89c5bf82595d Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Wed, 30 Jul 2025 15:40:00 +0200 Subject: [PATCH 2/3] Fix reactivity issues with set --- packages/frontend-2/components/viewer/models/Card.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/frontend-2/components/viewer/models/Card.vue b/packages/frontend-2/components/viewer/models/Card.vue index 28afe5959..0b784d2d3 100644 --- a/packages/frontend-2/components/viewer/models/Card.vue +++ b/packages/frontend-2/components/viewer/models/Card.vue @@ -426,9 +426,8 @@ const expandToShowSelectedObjects = (selectedIds: string[]) => { } // Clear previous force-expanded nodes and set new ones - forceExpandedNodeIds.value.clear() const nodeIdsToExpand = pathsToExpand.flat() - nodeIdsToExpand.forEach((id) => forceExpandedNodeIds.value.add(id)) + forceExpandedNodeIds.value = new Set(nodeIdsToExpand) } } @@ -474,7 +473,7 @@ watch( if (selectedObjects.length > 0) { expandToShowSelectedObjects(selectedIds) } else { - forceExpandedNodeIds.value.clear() + forceExpandedNodeIds.value = new Set() } }, { deep: true } From 1884c883d19db41c998c1c8a45d60f8c471c8666 Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Wed, 30 Jul 2025 16:33:49 +0200 Subject: [PATCH 3/3] Fix selection --- .../frontend-2/components/viewer/models/Card.vue | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/frontend-2/components/viewer/models/Card.vue b/packages/frontend-2/components/viewer/models/Card.vue index 0b784d2d3..58b6cc559 100644 --- a/packages/frontend-2/components/viewer/models/Card.vue +++ b/packages/frontend-2/components/viewer/models/Card.vue @@ -1,17 +1,16 @@