You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ant-design-vue/components/vc-tour/hooks/useTarget.ts

102 lines
2.8 KiB

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<TourStepInfo['target']>,
open: Ref<boolean>,
gap?: Ref<Gap>,
scrollIntoViewOptions?: Ref<boolean | ScrollIntoViewOptions>,
): [Ref<PosInfo>, Ref<HTMLElement>] {
// ========================= Target =========================
// We trade `undefined` as not get target by function yet.
// `null` as empty target.
const [targetElement, setTargetElement] = useState<null | HTMLElement | undefined>(undefined);
watchEffect(
() => {
const nextElement =
typeof target.value === 'function' ? (target.value as any)() : target.value;
setTargetElement(nextElement || null);
},
{ flush: 'post' },
);
// ========================= Align ==========================
const [posInfo, setPosInfo] = useState<PosInfo>(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];
}