Merge branch 'feature/initial-viewer-ui-updates' of github.com:specklesystems/speckle-server into feature/initial-viewer-ui-updates
This commit is contained in:
@@ -1,17 +1,16 @@
|
||||
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
||||
<template>
|
||||
<div class="relative border-b border-outline-3">
|
||||
<div
|
||||
@mouseenter="highlightObject"
|
||||
@mouseleave="unhighlightObject"
|
||||
@focusin="highlightObject"
|
||||
@focusout="unhighlightObject"
|
||||
@click="selectObject"
|
||||
@keydown.enter="selectObject"
|
||||
>
|
||||
<div>
|
||||
<!-- Model Header -->
|
||||
<div
|
||||
class="group flex items-center px-1 py-3 select-none cursor-pointer hover:bg-highlight-1"
|
||||
@mouseenter="highlightObject"
|
||||
@mouseleave="unhighlightObject"
|
||||
@focusin="highlightObject"
|
||||
@focusout="unhighlightObject"
|
||||
@click="selectObject"
|
||||
@keydown.enter="selectObject"
|
||||
>
|
||||
<button
|
||||
class="group-hover:opacity-100 hover:bg-highlight-3 rounded-md h-5 w-4 flex items-center justify-center shrink-0"
|
||||
@@ -105,7 +104,7 @@
|
||||
<!-- Scene Explorer Content -->
|
||||
<div
|
||||
v-if="isExpanded && rootNodeChildren.length"
|
||||
class="relative flex flex-col gap-y-2"
|
||||
class="relative flex flex-col gap-y-2 overflow-y-auto"
|
||||
>
|
||||
<div v-for="(childNode, idx) in rootNodeChildren" :key="idx">
|
||||
<ViewerModelsTreeItem
|
||||
@@ -113,6 +112,7 @@
|
||||
:sub-header="'Model content'"
|
||||
:expand-level="expandLevel"
|
||||
:manual-expand-level="manualExpandLevel"
|
||||
:force-expanded-node-ids="forceExpandedNodeIds"
|
||||
:is-descendant-of-selected="false"
|
||||
@expanded="(e: number) => $emit('expanded', e)"
|
||||
/>
|
||||
@@ -164,7 +164,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 +193,9 @@ const { load: loadLatestVersion } = useLoadLatestVersion({
|
||||
const isExpanded = ref(false)
|
||||
const showActionsMenu = ref(false)
|
||||
|
||||
// Track expansion state for auto-expanding selected objects
|
||||
const forceExpandedNodeIds = ref<Set<string>>(new Set())
|
||||
|
||||
const IconEye = resolveComponent('IconEye')
|
||||
const IconEyeClosed = resolveComponent('IconEyeClosed')
|
||||
|
||||
@@ -410,4 +413,68 @@ 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
|
||||
const nodeIdsToExpand = pathsToExpand.flat()
|
||||
forceExpandedNodeIds.value = new Set(nodeIdsToExpand)
|
||||
}
|
||||
}
|
||||
|
||||
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 = new Set()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<div :class="{ 'opacity-60': shouldShowDimmed }">
|
||||
<!-- Header -->
|
||||
<div
|
||||
ref="headerElement"
|
||||
class="group flex items-center justify-between w-full p-2 pr-1 cursor-pointer"
|
||||
:class="getBackgroundClass"
|
||||
@click.stop="(e:MouseEvent) => setSelection(e)"
|
||||
@@ -114,12 +115,13 @@
|
||||
:depth="depth + 1"
|
||||
:expand-level="props.expandLevel"
|
||||
:manual-expand-level="manualExpandLevel"
|
||||
:force-expanded-node-ids="props.forceExpandedNodeIds"
|
||||
:parent="treeItem"
|
||||
:is-descendant-of-selected="isSelected || isChildOfSelected"
|
||||
@expanded="(e) => $emit('expanded', e)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="itemCount <= singleCollectionItems.length" class="mb-2">
|
||||
<div v-if="itemCount < singleCollectionItems.length">
|
||||
<FormButton
|
||||
size="sm"
|
||||
color="outline"
|
||||
@@ -166,6 +168,7 @@ const props = withDefaults(
|
||||
header?: string | null
|
||||
subHeader?: string | null
|
||||
isDescendantOfSelected?: boolean
|
||||
forceExpandedNodeIds?: Set<string>
|
||||
}>(),
|
||||
{ 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<HTMLElement>()
|
||||
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
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user