frontend: add item tooltip, refactor with itemicon component

This commit is contained in:
2026-03-01 13:42:01 +01:00
parent f6cf2c8a8c
commit 930cbf5a18
7 changed files with 536 additions and 142 deletions

View File

@@ -12,20 +12,16 @@ const emit = defineEmits<{
parentReady: []
}>()
const { data: items, pending: itemsLoading } = useFetch<Array<{ id: number; iconPath: string }>>(
'/api/cdragon/items',
{
lazy: true, // Don't block rendering
server: false // Client-side only
}
)
const { data: items, pending: itemsLoading } = useFetch<Array<Item>>('/api/cdragon/items', {
lazy: true, // Don't block rendering
server: false // Client-side only
})
// Track image loading state
const imagesLoaded = ref(false)
const imageElement: Ref<HTMLImageElement | null> = ref(null)
// Create item map reactively
const itemMap = reactive(new Map<number, { id: number; iconPath: string }>())
const itemMap = reactive(new Map<number, Item>())
watch(
items,
newItems => {
@@ -39,15 +35,13 @@ watch(
{ immediate: true }
)
function getItemIconPath(itemId: number): string {
const item = itemMap.get(itemId)
return item ? CDRAGON_BASE + mapPath(item.iconPath) : ''
}
const start: Ref<Element | null> = useTemplateRef('start')
const startTreeItem = useTemplateRef('start')
const arrows: Array<svgdomarrowsLinePath> = []
const pendingChildMounts: Array<Element> = []
// Get the actual icon element for arrow drawing
const startElement = computed(() => startTreeItem.value?.iconElement ?? null)
// Function to wait for an image to load
function waitForImageLoad(imgElement: HTMLImageElement): Promise<void> {
return new Promise(resolve => {
@@ -91,12 +85,12 @@ onMounted(async () => {
}
})
if (start.value) {
const imgElement = start.value as HTMLImageElement
imageElement.value = imgElement
// Wait for own image to load
await waitForImageLoad(imgElement)
if (startElement.value) {
// Wait for the ItemIcon to load its image
const imgElement = startElement.value.querySelector('img')
if (imgElement) {
await waitForImageLoad(imgElement as HTMLImageElement)
}
// Now that image is loaded and DOM is ready, draw arrows
imagesLoaded.value = true
@@ -108,7 +102,7 @@ onMounted(async () => {
if (pendingChildMounts.length > 0) {
await nextTick()
for (const childEnd of pendingChildMounts) {
drawArrow(start.value!, childEnd)
drawArrow(startElement.value!, childEnd)
}
pendingChildMounts.length = 0
}
@@ -117,7 +111,7 @@ onMounted(async () => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
refreshArrows()
emit('mount', start.value!)
emit('mount', startElement.value!)
})
})
}
@@ -133,12 +127,12 @@ onBeforeUpdate(() => {
onUpdated(async () => {
await nextTick()
if (start.value && imagesLoaded.value) {
if (startElement.value && imagesLoaded.value) {
// Redraw arrows after DOM update
requestAnimationFrame(() => {
requestAnimationFrame(() => {
refreshArrows()
emit('mount', start.value!)
emit('mount', startElement.value!)
})
})
}
@@ -168,7 +162,7 @@ function drawArrow(start: Element, end: Element) {
left: 0
}
},
style: 'stroke:var(--color-on-surface);stroke-width:2;fill:transparent;',
style: 'stroke:var(--color-on-surface);stroke-width:2;fill:transparent;pointer-events:none;',
appendTo: document.body
})
arrows.push(arrow)
@@ -194,12 +188,12 @@ addEventListener('scroll', _ => {
})
function handleSubtreeMount(end: Element) {
if (start.value) {
if (startElement.value) {
if (imagesLoaded.value) {
// Parent is ready, draw arrow immediately
requestAnimationFrame(() => {
requestAnimationFrame(() => {
drawArrow(start.value!, end)
drawArrow(startElement.value!, end)
refreshArrows()
})
})
@@ -213,7 +207,7 @@ function handleSubtreeMount(end: Element) {
function handleParentReady() {
// Parent became ready, redraw all our arrows
if (start.value && imagesLoaded.value) {
if (startElement.value && imagesLoaded.value) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
refreshArrows()
@@ -233,13 +227,15 @@ function handleRefresh() {
<template>
<div class="item-tree-container">
<div v-if="tree.data != undefined && tree.data != null" class="item-tree-node">
<img
<ItemIcon
v-if="itemMap.get(tree.data)"
ref="start"
:item="itemMap.get(tree.data)!"
:show-pickrate="true"
:pickrate="parentCount ? tree.count / parentCount : 0"
:size="48"
class="item-tree-img"
:alt="tree.data.toString()"
:src="getItemIconPath(tree.data)"
/>
<span class="item-tree-pickrate">{{ ((tree.count / parentCount!!) * 100).toFixed(0) }}%</span>
</div>
<div class="item-tree-children">
@@ -268,36 +264,23 @@ function handleRefresh() {
flex-direction: column;
align-items: center;
width: fit-content;
}
.item-tree-img {
width: 48px;
height: 48px;
border-radius: 4px;
border: 1px solid var(--color-on-surface);
}
.item-tree-pickrate {
font-size: 0.65rem;
color: var(--color-on-surface);
opacity: 0.6;
margin-top: 2px;
position: relative;
z-index: 10;
}
.item-tree-children {
margin-left: 32px;
position: relative;
z-index: 1;
}
.item-tree-child {
width: fit-content;
position: relative;
}
/* Mobile responsive */
@media only screen and (max-width: 900px) {
.item-tree-img {
width: 40px;
height: 40px;
}
.item-tree-children {
margin-left: 20px;
}