import { ref, computed, watch, watchEffect, defineComponent, toRefs, shallowRef } from 'vue'; import type { CSSProperties, ExtractPropTypes } from 'vue'; import type { VueNode } from '../_util/type'; import Trigger, { triggerProps } from '../vc-trigger'; import classNames from '../_util/classNames'; import useMergedState from '../_util/hooks/useMergedState'; import useTarget from './hooks/useTarget'; import type { Gap } from './hooks/useTarget'; import TourStep from './TourStep'; import type { TourStepInfo, TourStepProps } from './interface'; import Mask from './Mask'; import { getPlacements } from './placements'; import type { PlacementType } from './placements'; import { initDefaultProps } from '../_util/props-util'; import { someType, stringType, arrayType, objectType, functionType, booleanType, } from '../_util/type'; import Portal from '../_util/PortalWrapper'; const CENTER_PLACEHOLDER: CSSProperties = { left: '50%', top: '50%', width: '1px', height: '1px', }; export const tourProps = () => { const { builtinPlacements, popupAlign } = triggerProps(); return { builtinPlacements, popupAlign, steps: arrayType(), open: booleanType(), defaultCurrent: { type: Number }, current: { type: Number }, onChange: functionType<(current: number) => void>(), onClose: functionType<(current: number) => void>(), onFinish: functionType<() => void>(), mask: someType([Boolean, Object], true), arrow: someType([Boolean, Object], true), rootClassName: { type: String }, placement: stringType('bottom'), prefixCls: { type: String, default: 'rc-tour' }, renderPanel: functionType<(props: TourStepProps, current: number) => VueNode>(), gap: objectType(), animated: someType([Boolean, Object]), scrollIntoViewOptions: someType([Boolean, Object], true), zIndex: { type: Number, default: 1001 }, }; }; export type TourProps = Partial>>; const Tour = defineComponent({ name: 'Tour', inheritAttrs: false, props: initDefaultProps(tourProps(), {}), setup(props) { const { defaultCurrent, placement, mask, scrollIntoViewOptions, open, gap, arrow } = toRefs(props); const triggerRef = ref(); const [mergedCurrent, setMergedCurrent] = useMergedState(0, { value: computed(() => props.current), defaultValue: defaultCurrent.value, }); const [mergedOpen, setMergedOpen] = useMergedState(undefined, { value: computed(() => props.open), postState: origin => mergedCurrent.value < 0 || mergedCurrent.value >= props.steps.length ? false : origin ?? true, }); const openRef = shallowRef(mergedOpen.value); watchEffect(() => { if (mergedOpen.value && !openRef.value) { setMergedCurrent(0); } openRef.value = mergedOpen.value; }); const curStep = computed(() => (props.steps[mergedCurrent.value] || {}) as TourStepInfo); const mergedPlacement = computed(() => curStep.value.placement ?? placement.value); const mergedMask = computed(() => mergedOpen.value && (curStep.value.mask ?? mask.value)); const mergedScrollIntoViewOptions = computed( () => curStep.value.scrollIntoViewOptions ?? scrollIntoViewOptions.value, ); const [posInfo, targetElement] = useTarget( computed(() => curStep.value.target), open, gap, mergedScrollIntoViewOptions, ); // ========================= arrow ========================= const mergedArrow = computed(() => targetElement.value ? typeof curStep.value.arrow === 'undefined' ? arrow.value : curStep.value.arrow : false, ); const arrowPointAtCenter = computed(() => typeof mergedArrow.value === 'object' ? mergedArrow.value.pointAtCenter : false, ); watch(arrowPointAtCenter, () => { triggerRef.value?.forcePopupAlign(); }); watch(mergedCurrent, () => { triggerRef.value?.forcePopupAlign(); }); // ========================= Change ========================= const onInternalChange = (nextCurrent: number) => { setMergedCurrent(nextCurrent); props.onChange?.(nextCurrent); }; return () => { const { prefixCls, steps, onClose, onFinish, rootClassName, renderPanel, animated, zIndex, ...restProps } = props; // ========================= Render ========================= // Skip if not init yet if (targetElement.value === undefined) { return null; } const handleClose = () => { setMergedOpen(false); onClose?.(mergedCurrent.value); }; const mergedShowMask = typeof mergedMask.value === 'boolean' ? mergedMask.value : !!mergedMask.value; const mergedMaskStyle = typeof mergedMask.value === 'boolean' ? undefined : mergedMask.value; // when targetElement is not exist, use body as triggerDOMNode const getTriggerDOMNode = () => { return targetElement.value || document.body; }; const getPopupElement = () => ( { onInternalChange(mergedCurrent.value - 1); }} onNext={() => { onInternalChange(mergedCurrent.value + 1); }} onClose={handleClose} current={mergedCurrent.value} onFinish={() => { handleClose(); onFinish?.(); }} {...curStep.value} /> ); const posInfoStyle = computed(() => { const info = posInfo.value || CENTER_PLACEHOLDER; // 如果info[key] 是number,添加 px const style: CSSProperties = {}; Object.keys(info).forEach(key => { if (typeof info[key] === 'number') { style[key] = `${info[key]}px`; } else { style[key] = info[key]; } }); return style; }); return mergedOpen.value ? ( <>
) : null; }; }, }); export default Tour;