diff --git a/frontend/components/item/Tree.vue b/frontend/components/item/Tree.vue index 81227bb..8fea6d5 100644 --- a/frontend/components/item/Tree.vue +++ b/frontend/components/item/Tree.vue @@ -9,13 +9,18 @@ defineProps<{ const emit = defineEmits<{ mount: [end: Element] refresh: [] + parentReady: [] }>() -const { data: items } = useFetch>('/api/cdragon/items', { +const { data: items, pending: itemsLoading } = useFetch>('/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 = ref(null) + // Create item map reactively const itemMap = reactive(new Map()) watch( @@ -38,12 +43,72 @@ function getItemIconPath(itemId: number): string { const start: Ref = useTemplateRef('start') const arrows: Array = [] +const pendingChildMounts: Array = [] -onMounted(() => { - // Only refresh arrows and emit if start element is available +// Function to wait for an image to load +function waitForImageLoad(imgElement: HTMLImageElement): Promise { + return new Promise((resolve) => { + if (imgElement.complete) { + requestAnimationFrame(() => resolve()) + return + } + + imgElement.addEventListener('load', () => { + requestAnimationFrame(() => resolve()) + }, { once: true }) + imgElement.addEventListener('error', () => { + requestAnimationFrame(() => resolve()) + }, { once: true }) + }) +} + +onMounted(async () => { + // Wait for next tick to ensure DOM is ready + await nextTick() + + // Wait for items to be loaded + await new Promise((resolve) => { + if (!itemsLoading.value) { + resolve() + } else { + const unwatch = watch(itemsLoading, (loading) => { + if (!loading) { + unwatch() + resolve() + } + }) + } + }) + if (start.value) { - refreshArrows() - emit('mount', start.value) + const imgElement = start.value as HTMLImageElement + imageElement.value = imgElement + + // Wait for own image to load + await waitForImageLoad(imgElement) + + // Now that image is loaded and DOM is ready, draw arrows + imagesLoaded.value = true + + // Notify children that parent is ready + emit('parentReady') + + // Draw any pending arrows from children that mounted before we were ready + if (pendingChildMounts.length > 0) { + await nextTick() + for (const childEnd of pendingChildMounts) { + drawArrow(start.value!, childEnd) + } + pendingChildMounts.length = 0 + } + + // Use multiple requestAnimationFrame to ensure rendering is complete + requestAnimationFrame(() => { + requestAnimationFrame(() => { + refreshArrows() + emit('mount', start.value!) + }) + }) } }) @@ -54,10 +119,17 @@ onBeforeUpdate(() => { arrows.splice(0, arrows.length) }) -onUpdated(() => { - if (start.value) { - refreshArrows() - emit('mount', start.value) +onUpdated(async () => { + await nextTick() + + if (start.value && imagesLoaded.value) { + // Redraw arrows after DOM update + requestAnimationFrame(() => { + requestAnimationFrame(() => { + refreshArrows() + emit('mount', start.value!) + }) + }) } }) @@ -89,16 +161,17 @@ function drawArrow(start: Element, end: Element) { appendTo: document.body }) arrows.push(arrow) - // Redraw immediately after creation to ensure correct position - requestAnimationFrame(() => { - arrow.redraw() - }) } function refreshArrows() { - for (const arrow of arrows) { - arrow.redraw() - } + // Double requestAnimationFrame to ensure layout is complete + requestAnimationFrame(() => { + requestAnimationFrame(() => { + for (const arrow of arrows) { + arrow.redraw() + } + }) + }) } // Redraw arrows on window resize @@ -111,11 +184,35 @@ addEventListener('scroll', _ => { function handleSubtreeMount(end: Element) { if (start.value) { - drawArrow(start.value, end) - refreshArrows() + if (imagesLoaded.value) { + // Parent is ready, draw arrow immediately + requestAnimationFrame(() => { + requestAnimationFrame(() => { + drawArrow(start.value!, end) + refreshArrows() + }) + }) + } else { + // Parent not ready yet, store for later + pendingChildMounts.push(end) + } } emit('refresh') } + +function handleParentReady() { + // Parent became ready, redraw all our arrows + if (start.value && imagesLoaded.value) { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + refreshArrows() + }) + }) + } + // Propagate to children + emit('parentReady') +} + function handleRefresh() { refreshArrows() emit('refresh') @@ -141,6 +238,7 @@ function handleRefresh() { :parent-count="tree.count" @refresh="handleRefresh" @mount="end => handleSubtreeMount(end)" + @parent-ready="handleParentReady" /> @@ -193,4 +291,4 @@ function handleRefresh() { margin-left: 20px; } } - + \ No newline at end of file