-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy pathOnborda.js
More file actions
353 lines (353 loc) · 20.7 KB
/
Copy pathOnborda.js
File metadata and controls
353 lines (353 loc) · 20.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useEffect, useMemo, useRef, useState } from "react";
import { useOnborda } from "./OnbordaContext";
import { motion } from "framer-motion";
import { usePathname, useRouter } from "next/navigation";
import { Portal } from "@radix-ui/react-portal";
import { getCardStyle, getArrowStyle } from "./OnbordaStyles";
/**
* Onborda Component
* @param {OnbordaProps} props
* @constructor
*/
const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, debug = false, observerTimeout = 5000, }) => {
const { currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps, completedSteps, setCompletedSteps, tours, closeOnborda } = useOnborda();
const [elementToScroll, setElementToScroll] = useState(null);
const [pointerPosition, setPointerPosition] = useState(null);
const currentElementRef = useRef(null);
const [currentRoute, setCurrentRoute] = useState(null);
const [pendingRouteChange, setPendingRouteChange] = useState(false);
const hasSelector = (step) => {
return !!step?.selector || !!step?.customQuerySelector;
};
const getStepSelectorElement = (step) => {
return step?.selector ? document.querySelector(step.selector) : step?.customQuerySelector ? step.customQuerySelector() : null;
};
// Get the current tour object
const currentTourObject = useMemo(() => {
return tours.find((tour) => tour.tour === currentTour);
}, [currentTour, isOnbordaVisible]);
// - -
// Route Changes
const router = useRouter();
const path = usePathname();
// Update the current route on route changes
useEffect(() => {
setCurrentRoute(path);
}, [path]);
// - -
// Initialisze
useEffect(() => {
let cleanup = [];
if (isOnbordaVisible && currentTourSteps) {
debug && console.log("Onborda: Current Step Changed", currentStep);
const step = currentTourSteps[currentStep];
if (step) {
let elementFound = false;
// Check if the step has a selector
if (hasSelector(step)) {
// This step has a selector. Lets find the element
const element = getStepSelectorElement(step);
// Check if the element is found
if (element) {
// Once the element is found, update the step and scroll to the element
setPointerPosition(getElementPosition(element));
setElementToScroll(element);
currentElementRef.current = element;
// Function to mark the step as completed if the conditions are met
const handleInteraction = () => {
const isComplete = step?.isCompleteConditions?.(element) ?? true;
debug && console.log("Onborda: Step Interaction", step, isComplete);
// Check if the step is complete based on the conditions, and not already marked as completed
if (isComplete && !Array.from(completedSteps).includes(currentStep)) {
debug && console.log("Onborda: Step Completed", step);
setCompletedSteps((prev) => {
return prev.add(currentStep);
});
// If callback is provided, call it
step?.onComplete && step.onComplete();
} // Check if the step is incomplete based on the conditions, and already marked as completed
else if (!isComplete && Array.from(completedSteps).includes(currentStep)) {
debug && console.log("Onborda: Step Incomplete", step);
setCompletedSteps((prev) => {
prev.delete(currentStep);
return prev;
});
}
};
// Initial check
handleInteraction();
// Enable pointer events on the element
if (step.interactable) {
const htmlElement = element;
htmlElement.style.pointerEvents = "auto";
// Add event listeners if the step is interactable and has conditions
if (step?.isCompleteConditions) {
element.addEventListener("click", handleInteraction);
element.addEventListener("input", handleInteraction);
element.addEventListener("change", handleInteraction);
debug && console.log("Onborda: Added event listeners for element", element);
cleanup.push(() => {
// Cleanup the event listeners
element.removeEventListener("click", handleInteraction);
element.removeEventListener("input", handleInteraction);
element.removeEventListener("change", handleInteraction);
debug && console.log("Onborda: Removed event listeners for element", element);
});
}
}
elementFound = true;
}
// Even if the element is already found, we still need to check if the route is different from the current route
// do we have a route to navigate to?
if (step.route) {
// Check if the route is set and different from the current route
if (currentRoute == null || !currentRoute?.endsWith(step.route)) {
debug && console.log("Onborda: Navigating to route", step.route);
// Trigger the next route
router.push(step.route);
// Use MutationObserver to detect when the target element is available in the DOM
const observer = new MutationObserver((mutations, observer) => {
const shouldSelect = hasSelector(currentTourSteps[currentStep]);
if (shouldSelect) {
const element = getStepSelectorElement(currentTourSteps[currentStep]);
if (element) {
// Once the element is found, update the step and scroll to the element
setPointerPosition(getElementPosition(element));
setElementToScroll(element);
currentElementRef.current = element;
const handleInteraction = () => {
const isComplete = step?.isCompleteConditions?.(element) ?? true;
debug && console.log("Onborda: Step Interaction", step, isComplete);
// Check if the step is complete based on the conditions, and not already marked as completed
if (isComplete && !Array.from(completedSteps).includes(currentStep)) {
debug && console.log("Onborda: Step Completed", step);
setCompletedSteps((prev) => {
return prev.add(currentStep);
});
// If callback is provided, call it
step?.onComplete && step.onComplete();
} // Check if the step is incomplete based on the conditions, and already marked as completed
else if (!isComplete && Array.from(completedSteps).includes(currentStep)) {
debug && console.log("Onborda: Step Incomplete", step);
setCompletedSteps((prev) => {
prev.delete(currentStep);
return prev;
});
}
};
// Initial check
handleInteraction();
// Enable pointer events on the element
if (step.interactable) {
const htmlElement = element;
htmlElement.style.pointerEvents = "auto";
}
// Stop observing after the element is found
observer.disconnect();
debug && console.log("Onborda: Observer disconnected after element found", element);
}
else {
debug && console.log("Onborda: Observing for element...", currentTourSteps[currentStep]);
}
}
else {
setCurrentStep(currentStep);
observer.disconnect();
debug && console.log("Onborda: Observer disconnected after no selector set", currentTourSteps[currentStep]);
}
});
// Start observing the document body for changes
observer.observe(document.body, {
childList: true,
subtree: true,
});
setPendingRouteChange(true);
// Set a timeout to disconnect the observer if the element is not found within a certain period
const timeoutId = setTimeout(() => {
observer.disconnect();
console.error("Onborda: Observer Timeout", currentTourSteps[currentStep]);
}, observerTimeout); // Adjust the timeout period as needed
// Clear the timeout if the observer disconnects successfully
const originalDisconnect = observer.disconnect.bind(observer);
observer.disconnect = () => {
setPendingRouteChange(false);
clearTimeout(timeoutId);
originalDisconnect();
};
}
}
}
else {
// no selector, but might still need to navigate to a route
if (step.route) {
// Check if the route is set and different from the current route
if (currentRoute == null || !currentRoute?.endsWith(step.route)) {
debug && console.log("Onborda: Navigating to route", step.route);
// Trigger the next route
router.push(step.route);
}
else {
// Mark the step as completed
step?.onComplete && step.onComplete();
setCompletedSteps((prev) => {
return prev.add(currentStep);
});
}
}
else {
// Mark the step as completed
step?.onComplete && step.onComplete();
setCompletedSteps((prev) => {
return prev.add(currentStep);
});
}
}
// No element set for this step? Place the pointer at the center of the screen
if (!elementFound) {
setPointerPosition({
x: window.innerWidth / 2,
y: window.innerHeight / 2,
width: 0,
height: 0,
});
setElementToScroll(null);
currentElementRef.current = null;
}
// Prefetch the next route
const nextStep = currentTourSteps[currentStep + 1];
if (nextStep && nextStep?.route) {
debug && console.log("Onborda: Prefetching Next Route", nextStep.route);
router.prefetch(nextStep.route);
}
}
}
return () => {
// Disable pointer events on the element on cleanup
if (currentElementRef.current) {
const htmlElement = currentElementRef.current;
htmlElement.style.pointerEvents = "";
}
// Cleanup any event listeners we may have added
cleanup.forEach(fn => fn());
};
}, [
currentStep, // Re-run the effect when the current step changes
currentTourSteps, // Re-run the effect when the current tour steps change
isOnbordaVisible, // Re-run the effect when the onborda visibility changes
currentRoute, // Re-run the effect when the current route changes
]);
// - -
// 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,
};
};
// - -
// Scroll to the element when the elementToScroll changes
useEffect(() => {
if (elementToScroll && isOnbordaVisible) {
debug && 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, isOnbordaVisible]);
// - -
// Update pointer position on window resize
const updatePointerPosition = () => {
if (currentTourSteps) {
const step = currentTourSteps[currentStep];
if (step) {
const element = getStepSelectorElement(step);
if (element) {
setPointerPosition(getElementPosition(element));
}
else {
// if the element is not found, place the pointer at the center of the screen
setPointerPosition({
x: window.innerWidth / 2,
y: window.innerHeight / 2,
width: 0,
height: 0,
});
setElementToScroll(null);
currentElementRef.current = null;
}
}
}
};
// - -
// 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 () => {
const nextStepIndex = currentStep + 1;
await setStep(nextStepIndex);
};
const prevStep = async () => {
const prevStepIndex = currentStep - 1;
await setStep(prevStepIndex);
};
const setStep = async (step) => {
const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step;
setCurrentStep(setStepIndex);
};
// - -
// Card Arrow
const CardArrow = ({ isVisible }) => {
if (!isVisible) {
return null;
}
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;
const pointerEvents = pointerPosition && isOnbordaVisible ? 'pointer-events-none' : '';
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-name": "onborda-site-wrapper", className: ` ${pointerEvents} `, children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsxs(Portal, { children: [_jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 pointer-events-none z-[997]", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[998]", 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-[999]", "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, setStep: setStep, closeOnborda: closeOnborda, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), completedSteps: Array.from(completedSteps), pendingRouteChange: pendingRouteChange }) }) }) }), TourComponent && currentTourObject !== undefined && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { tour: currentTourObject, currentTour: currentTour, currentStep: currentStep, setStep: setStep, completedSteps: Array.from(completedSteps), closeOnborda: closeOnborda }) }) }))] }))] }));
};
export default Onborda;