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.
102 lines
2.8 KiB
102 lines
2.8 KiB
1 year ago
|
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];
|
||
|
}
|