UNPKG

onborda

Version:

The ultimate product tour library for Next.js

423 lines (422 loc) 18.3 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useEffect, useRef } from "react"; import { useOnborda } from "./OnbordaContext"; import { motion, useInView } from "framer-motion"; import { useRouter } from "next/navigation"; import { Portal } from "@radix-ui/react-portal"; const Onborda = ({ children, interact = false, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, }) => { const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = useOnborda(); const currentTourSteps = steps.find((tour) => tour.tour === currentTour)?.steps; const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); const observeRef = useRef(null); // Ref for the observer element const isInView = useInView(observeRef); const offset = 20; // - - // Route Changes const router = useRouter(); // - - // Initialisze const previousElementRef = useRef(null); useEffect(() => { if (isOnbordaVisible && currentTourSteps) { // Clean up all elements that might have our styles currentTourSteps.forEach(tourStep => { const element = document.querySelector(tourStep.selector); if (element && tourStep !== currentTourSteps[currentStep]) { // Reset styles for non-active elements if interaction is enabled if (interact) { const style = element.style; style.position = ''; style.zIndex = ''; } } }); const step = currentTourSteps[currentStep]; if (step) { const element = document.querySelector(step.selector); if (element) { // Set styles for current element element.style.position = 'relative'; if (interact) { element.style.zIndex = '990'; } setPointerPosition(getElementPosition(element)); currentElementRef.current = element; setElementToScroll(element); const rect = element.getBoundingClientRect(); const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; if (!isInView || !isInViewportWithOffset) { element.scrollIntoView({ behavior: "smooth", block: "center" }); } } } } // Cleanup function for component unmount return () => { if (currentTourSteps) { currentTourSteps.forEach(step => { const element = document.querySelector(step.selector); if (element && interact) { element.style.position = ''; element.style.zIndex = ''; } }); } }; }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible, interact]); // - - // Helper function to get element position const getElementPosition = (element) => { const { top, left, width, height } = element.getBoundingClientRect(); const scrollTop = window.scrollY || document.documentElement.scrollTop; const scrollLeft = window.scrollX || document.documentElement.scrollLeft; return { x: left + scrollLeft, y: top + scrollTop, width, height, }; }; // - - // Update pointerPosition when currentStep changes useEffect(() => { if (isOnbordaVisible && currentTourSteps) { console.log("Onborda: Current Step Changed"); const step = currentTourSteps[currentStep]; if (step) { const element = document.querySelector(step.selector); if (element) { setPointerPosition(getElementPosition(element)); currentElementRef.current = element; setElementToScroll(element); const rect = element.getBoundingClientRect(); const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; if (!isInView || !isInViewportWithOffset) { element.scrollIntoView({ behavior: "smooth", block: "center" }); } } } } }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); useEffect(() => { if (elementToScroll && !isInView && isOnbordaVisible) { console.log("Onborda: Element to Scroll Changed"); const rect = elementToScroll.getBoundingClientRect(); const isAbove = rect.top < 0; elementToScroll.scrollIntoView({ behavior: "smooth", block: isAbove ? "center" : "center", inline: "center", }); } }, [elementToScroll, isInView, isOnbordaVisible]); // - - // Update pointer position on window resize const updatePointerPosition = () => { if (currentTourSteps) { const step = currentTourSteps[currentStep]; if (step) { const element = document.querySelector(step.selector); if (element) { setPointerPosition(getElementPosition(element)); } } } }; // - - // Update pointer position on window resize useEffect(() => { if (isOnbordaVisible) { window.addEventListener("resize", updatePointerPosition); return () => window.removeEventListener("resize", updatePointerPosition); } }, [currentStep, currentTourSteps, isOnbordaVisible]); // - - // Step Controls const nextStep = async () => { if (currentTourSteps && currentStep < currentTourSteps.length - 1) { try { const nextStepIndex = currentStep + 1; const route = currentTourSteps[currentStep].nextRoute; if (route) { await router.push(route); const targetSelector = currentTourSteps[nextStepIndex].selector; // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { const element = document.querySelector(targetSelector); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(nextStepIndex); scrollToElement(nextStepIndex); // Stop observing after the element is found observer.disconnect(); } }); // Start observing the document body for changes observer.observe(document.body, { childList: true, subtree: true, }); } else { setCurrentStep(nextStepIndex); scrollToElement(nextStepIndex); } } catch (error) { console.error("Error navigating to next route", error); } } }; const prevStep = async () => { if (currentTourSteps && currentStep > 0) { try { const prevStepIndex = currentStep - 1; const route = currentTourSteps[currentStep].prevRoute; if (route) { await router.push(route); const targetSelector = currentTourSteps[prevStepIndex].selector; // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { const element = document.querySelector(targetSelector); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(prevStepIndex); scrollToElement(prevStepIndex); // Stop observing after the element is found observer.disconnect(); } }); // Start observing the document body for changes observer.observe(document.body, { childList: true, subtree: true, }); } else { setCurrentStep(prevStepIndex); scrollToElement(prevStepIndex); } } catch (error) { console.error("Error navigating to previous route", error); } } }; // - - // Scroll to the correct element when the step changes const scrollToElement = (stepIndex) => { if (currentTourSteps) { const element = document.querySelector(currentTourSteps[stepIndex].selector); if (element) { const { top } = element.getBoundingClientRect(); const isInViewport = top >= -offset && top <= window.innerHeight + offset; if (!isInViewport) { element.scrollIntoView({ behavior: "smooth", block: "center" }); } // Update pointer position after scrolling setPointerPosition(getElementPosition(element)); } } }; // - - // Card Side const getCardStyle = (side) => { switch (side) { case "top": return { transform: `translate(-50%, 0)`, left: "50%", bottom: "100%", marginBottom: "25px", }; case "bottom": return { transform: `translate(-50%, 0)`, left: "50%", top: "100%", marginTop: "25px", }; case "left": return { transform: `translate(0, -50%)`, right: "100%", top: "50%", marginRight: "25px", }; case "right": return { transform: `translate(0, -50%)`, left: "100%", top: "50%", marginLeft: "25px", }; case "top-left": return { bottom: "100%", marginBottom: "25px", }; case "top-right": return { right: 0, bottom: "100%", marginBottom: "25px", }; case "bottom-left": return { top: "100%", marginTop: "25px", }; case "bottom-right": return { right: 0, top: "100%", marginTop: "25px", }; case "right-bottom": return { left: "100%", bottom: 0, marginLeft: "25px", }; case "right-top": return { left: "100%", top: 0, marginLeft: "25px", }; case "left-bottom": return { right: "100%", bottom: 0, marginRight: "25px", }; case "left-top": return { right: "100%", top: 0, marginRight: "25px", }; default: return {}; // Default case if no side is specified } }; // - - // Arrow position based on card side const getArrowStyle = (side) => { switch (side) { case "bottom": return { transform: `translate(-50%, 0) rotate(270deg)`, left: "50%", top: "-23px", }; case "top": return { transform: `translate(-50%, 0) rotate(90deg)`, left: "50%", bottom: "-23px", }; case "right": return { transform: `translate(0, -50%) rotate(180deg)`, top: "50%", left: "-23px", }; case "left": return { transform: `translate(0, -50%) rotate(0deg)`, top: "50%", right: "-23px", }; case "top-left": return { transform: `rotate(90deg)`, left: "10px", bottom: "-23px", }; case "top-right": return { transform: `rotate(90deg)`, right: "10px", bottom: "-23px", }; case "bottom-left": return { transform: `rotate(270deg)`, left: "10px", top: "-23px", }; case "bottom-right": return { transform: `rotate(270deg)`, right: "10px", top: "-23px", }; case "right-bottom": return { transform: `rotate(180deg)`, left: "-23px", bottom: "10px", }; case "right-top": return { transform: `rotate(180deg)`, left: "-23px", top: "10px", }; case "left-bottom": return { transform: `rotate(0deg)`, right: "-23px", bottom: "10px", }; case "left-top": return { transform: `rotate(0deg)`, right: "-23px", top: "10px", }; default: return {}; // Default case if no side is specified } }; // - - // Card Arrow const CardArrow = () => { return (_jsx("svg", { viewBox: "0 0 54 54", "data-name": "onborda-arrow", className: "absolute w-6 h-6 origin-center", style: getArrowStyle(currentTourSteps?.[currentStep]?.side), children: _jsx("path", { id: "triangle", d: "M27 27L0 0V54L27 27Z", fill: "currentColor" }) })); }; // - - // Overlay Variants const variants = { visible: { opacity: 1 }, hidden: { opacity: 0 }, }; // - - // Pointer Options const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; const pointerPadOffset = pointerPadding / 2; const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; return (_jsxs("div", { "data-name": "onborda-wrapper", className: "relative w-full", "data-onborda": "dev", children: [_jsx("div", { "data-name": "onborda-site", className: "block w-full", children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsxs(Portal, { children: [!interact && (_jsx("div", { className: "fixed inset-0 z-[900]" })), _jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 ", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[900]", style: { boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, }, initial: pointerPosition ? { x: pointerPosition.x - pointerPadOffset, y: pointerPosition.y - pointerPadOffset, width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } : {}, animate: pointerPosition ? { x: pointerPosition.x - pointerPadOffset, y: pointerPosition.y - pointerPadOffset, width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[950]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, {}) }) }) }) })] }))] })); }; export default Onborda;