import type { ZoomTransform } from "d3-zoom"; import { inject, onScopeDispose, type Ref, ref, watch } from "vue"; /** * Observe a section of the dom tree using a resize observer * @param child child node to start observation from * @param root root node to observe * @param observer resize observer to preform the observation */ function observeDomTree(child: Element, root: Element, observer: ResizeObserver) { let current: Element | null = child; while (current && current !== root) { observer.observe(current); current = current.parentElement; } observer.observe(root); } /** * Gets the relative position between a child element and a root element, * with transforms (e.g scale) applied * @param child element to get position of * @param root element to determine position relative to * @returns exact relative position in client space */ export function getRelativePosition(child: HTMLElement, root: HTMLElement) { const childBoundingBox = child.getBoundingClientRect(); const rootBoundingBox = root.getBoundingClientRect(); return { offsetLeft: childBoundingBox.left - rootBoundingBox.left, offsetTop: childBoundingBox.top - rootBoundingBox.top, }; } export function useRelativePosition(child: Ref, root: Ref) { const position = ref({ offsetLeft: 0, offsetTop: 0 }); const transform = inject("transform") as Ref; // not supported by two mobile device browsers // eslint-disable-next-line compat/compat const observer = new ResizeObserver(() => { if (child.value && root.value) { const clientPosition = getRelativePosition(child.value, root.value); position.value = { offsetLeft: clientPosition.offsetLeft / transform.value.k, offsetTop: clientPosition.offsetTop / transform.value.k, }; } }); watch( [child, root], () => { observer.disconnect(); if (child.value && root.value) { observeDomTree(child.value, root.value, observer); } }, { immediate: true, }, ); onScopeDispose(() => { observer.disconnect(); }); return position; }