@@ -16,6 +16,46 @@ function htmlToPlainText(html) {
16
16
return tempDiv . textContent || "" ; // Extract and return plain text
17
17
}
18
18
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
+
19
59
export default class GleapCopilotTours {
20
60
productTourData = undefined ;
21
61
productTourId = undefined ;
@@ -34,7 +74,33 @@ export default class GleapCopilotTours {
34
74
}
35
75
}
36
76
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
+ }
38
104
39
105
startWithConfig ( tourId , config , delay = 0 ) {
40
106
// Prevent multiple tours from being started.
@@ -99,78 +165,68 @@ export default class GleapCopilotTours {
99
165
}
100
166
101
167
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
+ }
106
173
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 ;
108
178
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 ;
113
182
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%)` ;
117
187
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 ) ;
124
189
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 ( ) ;
159
195
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 ;
165
201
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` ;
170
224
}
171
- }
172
225
173
- console . log ( "Pointer placed at:" , containerLeft , containerTop ) ;
226
+ scrollToElement ( anchor ) ;
227
+ } catch ( e ) {
228
+ console . error ( "Error updating pointer position:" , e ) ;
229
+ }
174
230
}
175
231
176
232
cleanup ( ) {
@@ -212,20 +268,36 @@ export default class GleapCopilotTours {
212
268
top: 0;
213
269
left: 0;
214
270
display: flex;
215
- align-items: center ;
271
+ align-items: flex-start ;
216
272
pointer-events: none;
217
273
z-index: 9999;
218
- transition: transform 0.5s ease, top 0.5s ease, left 0.5s ease;;
274
+ transition: all 0.5s ease;;
219
275
}
220
276
221
277
#${ pointerContainerId } svg {
222
278
width: 20px;
223
279
height: auto;
224
280
fill: none;
225
281
}
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
+ }
226
297
227
298
#info-bubble {
228
- margin-left: 10px;
299
+ margin-left: 5px;
300
+ margin-top: 18px;
229
301
padding: 10px 15px;
230
302
border-radius: 20px;
231
303
background-color: black;
@@ -245,11 +317,11 @@ export default class GleapCopilotTours {
245
317
pointer-events: all;
246
318
z-index: 2147483610;
247
319
box-sizing: border-box;
248
- border: 6px solid transparent;
249
- filter: blur(15px );
320
+ border: 8px solid transparent;
321
+ filter: blur(20px );
250
322
border-image-slice: 1;
251
323
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;
253
325
}
254
326
255
327
body::after {
@@ -266,7 +338,7 @@ export default class GleapCopilotTours {
266
338
border: 2px solid transparent;
267
339
border-image-slice: 1;
268
340
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;
270
342
}
271
343
272
344
@keyframes animateBorder {
@@ -298,11 +370,13 @@ export default class GleapCopilotTours {
298
370
align-items: center;
299
371
gap: 10px;
300
372
border: 1px solid #e721b3;
373
+ max-width: min(330px, 100vw - 40px);
301
374
}
302
375
303
376
.copilot-info-container svg {
304
377
width: 24px;
305
378
height: 24px;
379
+ flex-shrink: 0;
306
380
}
307
381
` ;
308
382
document . head . appendChild ( styleNode ) ;
@@ -349,7 +423,6 @@ export default class GleapCopilotTours {
349
423
renderNextStep ( ) {
350
424
const config = this . productTourData ;
351
425
const steps = config . steps ;
352
- const self = this ;
353
426
354
427
// Check if we have reached the end of the tour.
355
428
if ( this . currentActiveIndex >= steps . length ) {
@@ -358,39 +431,43 @@ export default class GleapCopilotTours {
358
431
}
359
432
360
433
const currentStep = steps [ this . currentActiveIndex ] ;
361
- const element = document . querySelector ( currentStep . selector ) ;
362
434
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.
365
437
this . updatePointerPosition ( element ) ;
366
- } , 100 ) ;
367
438
368
- const message =
369
- currentStep && currentStep . message
439
+ const message = currentStep ?. message
370
440
? htmlToPlainText ( currentStep . message )
371
441
: "🤔" ;
372
442
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 ;
376
446
377
- // Estimate readtime in seconds.
378
- const readTime = estimateReadTime ( message ) ;
447
+ // Estimate read time in seconds.
448
+ const readTime = estimateReadTime ( message ) ;
379
449
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 ( ) ;
385
454
386
- if ( currentStep . mode === "CLICK" ) {
387
- // Perform click on element.
388
- setTimeout ( ( ) => {
455
+ if ( currentStep . mode === "CLICK" && element ) {
389
456
try {
390
457
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 ) ) ;
395
472
}
396
473
}
0 commit comments