Files
speckle-server/packages/frontend-2/components/viewer/models/VirtualTreeItem.vue
T
2025-08-12 15:54:17 +01:00

228 lines
6.5 KiB
Vue

<template>
<div class="px-1">
<button
type="button"
class="group flex items-center w-full p-1 pr-2 cursor-pointer text-left justify-between"
:class="[getItemBackgroundClass(), getItemOpacityClass()]"
@click="handleItemClick($event)"
@dblclick="handleItemDoubleClick()"
@mouseenter="handleItemMouseEnter()"
@mouseleave="handleItemMouseLeave()"
@focusin="handleItemMouseEnter()"
@focusout="handleItemMouseLeave()"
>
<div class="flex items-center gap-0.5 min-w-0">
<div
:style="{ width: `${(item.indent || 0) * 0.375}rem` }"
class="shrink-0"
></div>
<ViewerExpansionTriangle
v-if="item.hasChildren"
:is-expanded="item.isExpanded"
:class="getItemOpacityClass()"
@click="toggleExpansion()"
/>
<div v-else class="w-4 shrink-0"></div>
<!-- Item content -->
<div
class="flex min-w-0 flex-col"
:class="[getItemTextColorClass(), getItemOpacityClass()]"
>
<div class="truncate text-body-2xs">
{{ getTreeItemHeader() }}
</div>
<div class="truncate text-body-3xs text-foreground-2">
{{ getTreeItemSubheader() }}
</div>
</div>
</div>
<div
class="flex items-center group-hover:w-auto overflow-hidden shrink-0"
:class="isTreeItemHidden || isTreeItemIsolated ? 'w-auto' : 'sm:w-0'"
>
<ViewerVisibilityButton
:is-hidden="isTreeItemHidden"
@click="toggleTreeItemVisibility()"
/>
<ViewerIsolateButton
:is-isolated="isTreeItemIsolated"
@click="toggleTreeItemIsolation()"
/>
</div>
</button>
</div>
</template>
<script setup lang="ts">
import type { ExplorerNode } from '~~/lib/viewer/helpers/sceneExplorer'
import { containsAll } from '~~/lib/common/helpers/utils'
import {
getTargetObjectIds,
getHeaderAndSubheaderForSpeckleObject
} from '~~/lib/object-sidebar/helpers'
import {
useSelectionUtilities,
useFilterUtilities,
useHighlightedObjectsUtilities,
useCameraUtilities
} from '~~/lib/viewer/composables/ui'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
import type { UnifiedVirtualItem } from '~~/lib/viewer/composables/tree'
const props = defineProps<{
item: UnifiedVirtualItem
}>()
const emit = defineEmits<{
'toggle-expansion': [itemId: string]
'item-click': [item: UnifiedVirtualItem, event: MouseEvent | KeyboardEvent]
}>()
const { objects: selectedObjects } = useSelectionUtilities()
const { hideObjects, showObjects, isolateObjects, unIsolateObjects } =
useFilterUtilities()
const { highlightObjects, unhighlightObjects } = useHighlightedObjectsUtilities()
const { zoom } = useCameraUtilities()
const {
viewer: {
metadata: { filteringState }
}
} = useInjectedViewerState()
const hiddenObjects = computed(() => filteringState.value?.hiddenObjects)
const isolatedObjects = computed(() => filteringState.value?.isolatedObjects)
const stateHasIsolatedObjectsInGeneral = computed(() => {
if (!isolatedObjects.value) return false
return isolatedObjects.value.length > 0
})
const rawSpeckleData = computed(() => {
if (props.item.type !== 'tree-item') return null
const node = props.item.data as ExplorerNode
return node.raw || null
})
const isTreeItemHidden = computed((): boolean => {
if (!rawSpeckleData.value || !hiddenObjects.value) return false
const ids = getTargetObjectIds(rawSpeckleData.value)
return containsAll(ids, hiddenObjects.value)
})
const isTreeItemIsolated = computed((): boolean => {
if (!rawSpeckleData.value || !isolatedObjects.value) return false
const ids = getTargetObjectIds(rawSpeckleData.value)
return containsAll(ids, isolatedObjects.value)
})
const toggleTreeItemVisibility = () => {
if (!rawSpeckleData.value) return
const ids = getTargetObjectIds(rawSpeckleData.value)
if (!isTreeItemHidden.value) {
hideObjects(ids)
} else {
showObjects(ids)
}
}
const toggleTreeItemIsolation = () => {
if (!rawSpeckleData.value) return
const ids = getTargetObjectIds(rawSpeckleData.value)
if (!isTreeItemIsolated.value) {
isolateObjects(ids)
} else {
unIsolateObjects(ids)
}
}
const toggleExpansion = () => {
emit('toggle-expansion', props.item.id)
}
const handleItemClick = (event: MouseEvent | KeyboardEvent) => {
emit('item-click', props.item, event)
}
const handleItemDoubleClick = () => {
if (!rawSpeckleData.value?.id) return
zoom([rawSpeckleData.value.id])
}
const handleItemMouseEnter = () => {
if (!rawSpeckleData.value) return
highlightObjects(getTargetObjectIds(rawSpeckleData.value))
}
const handleItemMouseLeave = () => {
if (!rawSpeckleData.value) return
unhighlightObjects(getTargetObjectIds(rawSpeckleData.value))
}
const getItemBackgroundClass = (): string => {
if (props.item.type !== 'tree-item') return ''
const node = props.item.data as ExplorerNode
const speckleData = node.raw
if (!speckleData?.id) return ''
const isSelected = selectedObjects.value.find((o) => o.id === speckleData.id)
const isChildOfSelected = props.item.isDescendantOfSelected
if (isSelected) return 'bg-highlight-3 rounded-sm'
if (isChildOfSelected) return 'bg-foundation-2 hover:bg-highlight-3'
return 'bg-foundation hover:bg-highlight-1 hover:rounded-sm'
}
const getItemOpacityClass = (): string => {
if (!rawSpeckleData.value) return ''
const isHidden = isTreeItemHidden.value
const isIsolated = isTreeItemIsolated.value
if (isHidden || (!isIsolated && stateHasIsolatedObjectsInGeneral.value)) {
return 'opacity-60'
}
return ''
}
const getItemTextColorClass = (): string => {
if (!rawSpeckleData.value) return ''
const isHidden = isTreeItemHidden.value
const isIsolated = isTreeItemIsolated.value
if (isHidden || (!isIsolated && stateHasIsolatedObjectsInGeneral.value)) {
return 'text-foreground-2'
}
return ''
}
const getTreeItemHeader = (): string => {
if (props.item.type !== 'tree-item') return ''
const node = props.item.data as ExplorerNode
const speckleData = node.raw
if (!speckleData) return ''
const { header } = getHeaderAndSubheaderForSpeckleObject(speckleData)
return header
}
const getTreeItemSubheader = (): string => {
if (props.item.type !== 'tree-item') return ''
const node = props.item.data as ExplorerNode
const speckleData = node.raw
if (!speckleData) return ''
const { subheader } = getHeaderAndSubheaderForSpeckleObject(speckleData)
return subheader
}
</script>