@@ -16,6 +16,46 @@ function htmlToPlainText(html) {
1616 return tempDiv . textContent || "" ; // Extract and return plain text
1717}
1818
19+ function scrollToElement ( element ) {
20+ if ( element ) {
21+ element . scrollIntoView ( {
22+ behavior : "smooth" , // Ensures smooth scrolling
23+ block : "center" , // Aligns the element in the center of the viewport
24+ inline : "center" , // Aligns inline elements in the center horizontally
25+ } ) ;
26+ }
27+ }
28+
29+ function waitForElement ( selector , timeout = 5000 ) {
30+ const pollInterval = 100 ;
31+ const maxAttempts = timeout / pollInterval ;
32+ let attempts = 0 ;
33+
34+ return new Promise ( ( resolve , reject ) => {
35+ const interval = setInterval ( ( ) => {
36+ const element = document . querySelector ( selector ) ;
37+ if ( element ) {
38+ clearInterval ( interval ) ;
39+ resolve ( element ) ;
40+ } else if ( attempts >= maxAttempts ) {
41+ clearInterval ( interval ) ;
42+ reject ( new Error ( `Element not found for selector: ${ selector } ` ) ) ;
43+ }
44+ attempts ++ ;
45+ } , pollInterval ) ;
46+ } ) ;
47+ }
48+
49+ function smoothScrollToY ( yPosition ) {
50+ const viewportHeight = window . innerHeight ;
51+ const targetScrollPosition = yPosition - viewportHeight / 2 ;
52+
53+ window . scrollTo ( {
54+ top : targetScrollPosition ,
55+ behavior : "smooth" , // Ensures smooth scrolling
56+ } ) ;
57+ }
58+
1959export default class GleapCopilotTours {
2060 productTourData = undefined ;
2161 productTourId = undefined ;
@@ -34,7 +74,33 @@ export default class GleapCopilotTours {
3474 }
3575 }
3676
37- constructor ( ) { }
77+ constructor ( ) {
78+ const self = this ;
79+ // Add on window resize listener.
80+ window . addEventListener ( "resize" , ( ) => {
81+ // Check if we currently have a tour.
82+ if (
83+ self . productTourId &&
84+ self . currentActiveIndex >= 0 &&
85+ self . productTourData &&
86+ self . productTourData . steps
87+ ) {
88+ const steps = self . productTourData . steps ;
89+ const currentStep = steps [ self . currentActiveIndex ] ;
90+
91+ if (
92+ currentStep &&
93+ currentStep . selector &&
94+ currentStep . selector !== ""
95+ ) {
96+ // Wait for the element to be rendered.
97+ self . updatePointerPosition (
98+ document . querySelector ( currentStep . selector )
99+ ) ;
100+ }
101+ }
102+ } ) ;
103+ }
38104
39105 startWithConfig ( tourId , config , delay = 0 ) {
40106 // Prevent multiple tours from being started.
@@ -99,78 +165,68 @@ export default class GleapCopilotTours {
99165 }
100166
101167 updatePointerPosition ( anchor ) {
102- const container = document . getElementById ( pointerContainerId ) ;
103- if ( ! container ) {
104- return ;
105- }
168+ try {
169+ const container = document . getElementById ( pointerContainerId ) ;
170+ if ( ! container ) {
171+ return ;
172+ }
106173
107- const infoBubble = container . querySelector ( "#info-bubble" ) ;
174+ // If no anchor, center on screen.
175+ if ( ! anchor ) {
176+ const scrollX = window . scrollX || 0 ;
177+ const scrollY = window . scrollY || 0 ;
108178
109- // If no anchor, center on screen.
110- if ( ! anchor ) {
111- const scrollX = window . scrollX || 0 ;
112- const scrollY = window . scrollY || 0 ;
179+ // The center of the *viewport* in document coordinates:
180+ const centerX = scrollX + window . innerWidth / 2 ;
181+ const centerY = scrollY + window . innerHeight / 2 ;
113182
114- // The center of the *viewport* in document coordinates:
115- const centerX = scrollX + window . innerWidth / 2 ;
116- const centerY = scrollY + window . innerHeight / 2 ;
183+ container . style . position = "absolute" ;
184+ container . style . left = `${ centerX } px` ;
185+ container . style . top = `${ centerY } px` ;
186+ container . style . transform = `translate(-50%, -50%)` ;
117187
118- container . style . position = "absolute" ;
119- container . style . left = `${ centerX } px` ;
120- container . style . top = `${ centerY } px` ;
121- container . style . transform = `translate(-50%, -50%)` ;
122- return ;
123- }
188+ smoothScrollToY ( centerY ) ;
124189
125- // 1) Calculate the anchor’s position on the page (not just viewport).
126- const anchorRect = anchor . getBoundingClientRect ( ) ;
127- const containerRect = container . getBoundingClientRect ( ) ;
128-
129- // Suppose the arrow’s tip is ~15px from the top and left (tweak as needed).
130- const arrowTipOffsetX = 15 ;
131- const arrowTipOffsetY = 15 ;
132-
133- // Center of anchor:
134- const anchorCenterX =
135- anchorRect . left + anchorRect . width / 2 + window . scrollX ;
136- const anchorCenterY =
137- anchorRect . top + anchorRect . height / 2 + window . scrollY ;
138-
139- // We want the arrow’s tip at the anchorCenter.
140- // So offset the pointer container so that (container’s top-left + arrowTipOffset) = anchor center
141- const containerLeft = anchorCenterX - arrowTipOffsetX ;
142- const containerTop = anchorCenterY - arrowTipOffsetY ;
143-
144- // Position the pointer container (arrow + bubble).
145- container . style . left = `${ containerLeft } px` ;
146- container . style . top = `${ containerTop } px` ;
147- container . style . transform = "" ; // no translate needed, or clear any you might have
148-
149- // 2) Check if the info bubble goes off the right edge
150- if ( infoBubble ) {
151- // Reset bubble style so we can measure it properly
152- infoBubble . style . marginLeft = "10px" ; // default to the right side
153- infoBubble . style . marginRight = "" ; // clear any previous override
154- infoBubble . style . transform = "none" ;
155-
156- const bubbleRect = infoBubble . getBoundingClientRect ( ) ;
157- const bubbleRightEdge = bubbleRect . right ;
158- const windowWidth = window . innerWidth ;
190+ return ;
191+ }
192+
193+ // 1) Calculate the anchor’s position on the page (not just viewport).
194+ const anchorRect = anchor . getBoundingClientRect ( ) ;
159195
160- // If bubble extends past the right edge by any amount, flip it to the left side
161- if ( bubbleRightEdge > windowWidth ) {
162- // Move bubble to the left side of the arrow
163- // One approach: negative margin-left by the bubble’s width + some padding
164- // Another approach: transform: translateX(-100%) etc.
196+ // Center of anchor:
197+ let anchorCenterX =
198+ anchorRect . left + anchorRect . width / 2 + window . scrollX ;
199+ let anchorCenterY =
200+ anchorRect . top + anchorRect . height / 2 + window . scrollY ;
165201
166- infoBubble . style . marginLeft = "" ;
167- infoBubble . style . marginRight = "10px" ;
168- // Or do something like:
169- // infoBubble.style.transform = `translateX(-${bubbleRect.width + 10}px)`;
202+ let containerWidthSpace = 330 ;
203+ if ( containerWidthSpace > window . innerWidth - 40 ) {
204+ containerWidthSpace = window . innerWidth - 40 ;
205+ }
206+
207+ const windowWidth = window . innerWidth ;
208+ const isTooFarRight =
209+ anchorCenterX + containerWidthSpace > windowWidth - 20 ;
210+
211+ container . style . transform = "" ;
212+
213+ if ( isTooFarRight ) {
214+ container . classList . add ( "copilot-pointer-container-right" ) ;
215+
216+ // Reverse the arrow direction and recalculate the position.
217+ container . style . right = `${ windowWidth - anchorCenterX } px` ;
218+ container . style . top = `${ anchorCenterY } px` ;
219+ container . style . left = "" ;
220+ } else {
221+ container . classList . remove ( "copilot-pointer-container-right" ) ;
222+ container . style . left = `${ anchorCenterX } px` ;
223+ container . style . top = `${ anchorCenterY } px` ;
170224 }
171- }
172225
173- console . log ( "Pointer placed at:" , containerLeft , containerTop ) ;
226+ scrollToElement ( anchor ) ;
227+ } catch ( e ) {
228+ console . error ( "Error updating pointer position:" , e ) ;
229+ }
174230 }
175231
176232 cleanup ( ) {
@@ -212,20 +268,36 @@ export default class GleapCopilotTours {
212268 top: 0;
213269 left: 0;
214270 display: flex;
215- align-items: center ;
271+ align-items: flex-start ;
216272 pointer-events: none;
217273 z-index: 9999;
218- transition: transform 0.5s ease, top 0.5s ease, left 0.5s ease;;
274+ transition: all 0.5s ease;;
219275 }
220276
221277 #${ pointerContainerId } svg {
222278 width: 20px;
223279 height: auto;
224280 fill: none;
225281 }
282+
283+ .${ pointerContainerId } -right {
284+ left: auto;
285+ right: 0;
286+ flex-direction: row-reverse;
287+ }
288+
289+ .${ pointerContainerId } -right svg {
290+ transform: scaleX(-1);
291+ }
292+
293+ .${ pointerContainerId } -right #info-bubble {
294+ margin-left: 0px;
295+ margin-right: 5px;
296+ }
226297
227298 #info-bubble {
228- margin-left: 10px;
299+ margin-left: 5px;
300+ margin-top: 18px;
229301 padding: 10px 15px;
230302 border-radius: 20px;
231303 background-color: black;
@@ -245,11 +317,11 @@ export default class GleapCopilotTours {
245317 pointer-events: all;
246318 z-index: 2147483610;
247319 box-sizing: border-box;
248- border: 6px solid transparent;
249- filter: blur(15px );
320+ border: 8px solid transparent;
321+ filter: blur(20px );
250322 border-image-slice: 1;
251323 border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
252- animation: animateBorder 4s infinite alternate ease-in-out;
324+ animation: animateBorder 3s infinite alternate ease-in-out;
253325 }
254326
255327 body::after {
@@ -266,7 +338,7 @@ export default class GleapCopilotTours {
266338 border: 2px solid transparent;
267339 border-image-slice: 1;
268340 border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
269- animation: animateBorder 4s infinite alternate ease-in-out;
341+ animation: animateBorder 3s infinite alternate ease-in-out;
270342 }
271343
272344 @keyframes animateBorder {
@@ -298,11 +370,13 @@ export default class GleapCopilotTours {
298370 align-items: center;
299371 gap: 10px;
300372 border: 1px solid #e721b3;
373+ max-width: min(330px, 100vw - 40px);
301374 }
302375
303376 .copilot-info-container svg {
304377 width: 24px;
305378 height: 24px;
379+ flex-shrink: 0;
306380 }
307381 ` ;
308382 document . head . appendChild ( styleNode ) ;
@@ -349,7 +423,6 @@ export default class GleapCopilotTours {
349423 renderNextStep ( ) {
350424 const config = this . productTourData ;
351425 const steps = config . steps ;
352- const self = this ;
353426
354427 // Check if we have reached the end of the tour.
355428 if ( this . currentActiveIndex >= steps . length ) {
@@ -358,39 +431,43 @@ export default class GleapCopilotTours {
358431 }
359432
360433 const currentStep = steps [ this . currentActiveIndex ] ;
361- const element = document . querySelector ( currentStep . selector ) ;
362434
363- // Wait for the pointer to be rendered. (by checking if the pointer container exists)
364- setTimeout ( ( ) => {
435+ const handleStep = ( element ) => {
436+ // Update pointer position, even if element is null.
365437 this . updatePointerPosition ( element ) ;
366- } , 100 ) ;
367438
368- const message =
369- currentStep && currentStep . message
439+ const message = currentStep ?. message
370440 ? htmlToPlainText ( currentStep . message )
371441 : "🤔" ;
372442
373- // Set content of info bubble.
374- document . getElementById ( "info-bubble" ) . textContent = message ;
375- document . getElementById ( pointerContainerId ) . style . opacity = 1 ;
443+ // Set content of info bubble.
444+ document . getElementById ( "info-bubble" ) . textContent = message ;
445+ document . getElementById ( pointerContainerId ) . style . opacity = 1 ;
376446
377- // Estimate readtime in seconds.
378- const readTime = estimateReadTime ( message ) ;
447+ // Estimate read time in seconds.
448+ const readTime = estimateReadTime ( message ) ;
379449
380- // Automatically move to next step after 3 seconds.
381- setTimeout ( ( ) => {
382- self . currentActiveIndex ++ ;
383- self . renderNextStep ( ) ;
384- self . storeUncompletedTour ( ) ;
450+ // Automatically move to the next step after the estimated read time.
451+ setTimeout ( ( ) => {
452+ this . currentActiveIndex ++ ;
453+ this . storeUncompletedTour ( ) ;
385454
386- if ( currentStep . mode === "CLICK" ) {
387- // Perform click on element.
388- setTimeout ( ( ) => {
455+ if ( currentStep . mode === "CLICK" && element ) {
389456 try {
390457 element . click ( ) ;
391- } catch ( e ) { }
392- } , 1000 * 60 ) ;
393- }
394- } , readTime * 1000 ) ;
458+ } catch ( e ) {
459+ console . error ( "Error clicking the element:" , e ) ;
460+ }
461+ }
462+
463+ this . renderNextStep ( ) ;
464+ } , readTime * 1000 ) ;
465+ } ;
466+
467+ const elementPromise = currentStep . selector
468+ ? waitForElement ( currentStep . selector )
469+ : Promise . resolve ( null ) ;
470+
471+ elementPromise . then ( handleStep ) . catch ( ( ) => handleStep ( null ) ) ;
395472 }
396473}
0 commit comments