import { computed, watchEffect, onMounted, watch, onBeforeUnmount } from 'vue'; import type { Ref } from 'vue'; import { isInViewPort } from '../util'; import type { TourStepInfo } from '..'; import useState from '../../_util/hooks/useState'; export interface Gap { offset?: number; radius?: number; } export interface PosInfo { left: number; top: number; height: number; width: number; radius: number; } export default function useTarget( target: Ref, open: Ref, gap?: Ref, scrollIntoViewOptions?: Ref, ): [Ref, Ref] { // ========================= Target ========================= // We trade `undefined` as not get target by function yet. // `null` as empty target. const [targetElement, setTargetElement] = useState(undefined); watchEffect( () => { const nextElement = typeof target.value === 'function' ? (target.value as any)() : target.value; setTargetElement(nextElement || null); }, { flush: 'post' }, ); // ========================= Align ========================== const [posInfo, setPosInfo] = useState(null); const updatePos = () => { if (!open.value) { setPosInfo(null); return; } if (targetElement.value) { // Exist target element. We should scroll and get target position if (!isInViewPort(targetElement.value) && open.value) { targetElement.value.scrollIntoView(scrollIntoViewOptions.value); } const { left, top, width, height } = targetElement.value.getBoundingClientRect(); const nextPosInfo: PosInfo = { left, top, width, height, radius: 0 }; if (JSON.stringify(posInfo.value) !== JSON.stringify(nextPosInfo)) { setPosInfo(nextPosInfo); } } else { // Not exist target which means we just show in center setPosInfo(null); } }; onMounted(() => { watch( [open, targetElement], () => { updatePos(); }, { flush: 'post', immediate: true }, ); // update when window resize window.addEventListener('resize', updatePos); }); onBeforeUnmount(() => { window.removeEventListener('resize', updatePos); }); // ======================== PosInfo ========================= const mergedPosInfo = computed(() => { if (!posInfo.value) { return posInfo.value; } const gapOffset = gap.value?.offset || 6; const gapRadius = gap.value?.radius || 2; return { left: posInfo.value.left - gapOffset, top: posInfo.value.top - gapOffset, width: posInfo.value.width + gapOffset * 2, height: posInfo.value.height + gapOffset * 2, radius: gapRadius, }; }); return [mergedPosInfo, targetElement]; }