@@ -7,7 +7,9 @@ const styleId = "copilot-tour-styles";
7
7
const copilotJoinedContainerId = "copilot-joined-container" ;
8
8
const copilotInfoBubbleId = "copilot-info-bubble" ;
9
9
10
- const arrowRightIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/></svg>` ;
10
+ const arrowRightIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
11
+ <path fill="currentColor" d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/>
12
+ </svg>` ;
11
13
12
14
function estimateReadTime ( text ) {
13
15
const wordsPerSecond = 3.6 ; // Average reading speed
@@ -25,7 +27,7 @@ function htmlToPlainText(html) {
25
27
function scrollToElement ( element ) {
26
28
if ( element ) {
27
29
element . scrollIntoView ( {
28
- behavior : "smooth" , // Ensures smooth scrolling
30
+ behavior : "smooth" ,
29
31
block : "center" ,
30
32
inline : "center" ,
31
33
} ) ;
@@ -83,7 +85,18 @@ async function canPlayAudio() {
83
85
}
84
86
}
85
87
86
- // === Helper: Get scrollable ancestors of an element ===
88
+ // Helper: Check if an element is fully visible in the viewport.
89
+ function isElementFullyVisible ( el ) {
90
+ const rect = el . getBoundingClientRect ( ) ;
91
+ return (
92
+ rect . top >= 0 &&
93
+ rect . left >= 0 &&
94
+ rect . bottom <= window . innerHeight &&
95
+ rect . right <= window . innerWidth
96
+ ) ;
97
+ }
98
+
99
+ // Helper: Get scrollable ancestors of an element.
87
100
function getScrollableAncestors ( el ) {
88
101
let ancestors = [ ] ;
89
102
let current = el . parentElement ;
@@ -110,13 +123,13 @@ export default class GleapCopilotTours {
110
123
audioMuted = false ;
111
124
currentAudio = undefined ;
112
125
113
- // Cached pointer container reference .
126
+ // Cached pointer container.
114
127
_pointerContainer = null ;
115
- // New properties for scroll handling.
128
+ // For scroll handling.
116
129
_scrollListeners = [ ] ;
117
130
_currentAnchor = null ;
118
131
_currentStep = null ;
119
- _updateScheduled = false ;
132
+ _scrollDebounceTimer = null ;
120
133
121
134
// GleapReplayRecorder singleton.
122
135
static instance ;
@@ -132,7 +145,7 @@ export default class GleapCopilotTours {
132
145
this . _scrollListeners = [ ] ;
133
146
this . _currentAnchor = null ;
134
147
this . _currentStep = null ;
135
- this . _updateScheduled = false ;
148
+ this . _scrollDebounceTimer = null ;
136
149
137
150
window . addEventListener ( "resize" , ( ) => {
138
151
if (
@@ -193,46 +206,47 @@ export default class GleapCopilotTours {
193
206
}
194
207
}
195
208
196
- // === Attach scroll listeners to update the pointer position on scroll ===
209
+ // Attach scroll listeners with a debounce to update the pointer position after scrolling stops.
197
210
attachScrollListeners ( anchor , currentStep ) {
198
211
if ( ! anchor ) return ;
199
212
const scrollableAncestors = getScrollableAncestors ( anchor ) ;
200
- // Also include window to catch any page-level scrolling .
213
+ // Also include window.
201
214
scrollableAncestors . push ( window ) ;
202
215
scrollableAncestors . forEach ( ( el ) => {
203
216
const handler = ( ) => {
204
- if ( ! this . _updateScheduled ) {
205
- this . _updateScheduled = true ;
206
- requestAnimationFrame ( ( ) => {
207
- this . updatePointerPosition ( anchor , currentStep ) ;
208
- this . _updateScheduled = false ;
209
- } ) ;
210
- }
217
+ clearTimeout ( this . _scrollDebounceTimer ) ;
218
+ this . _scrollDebounceTimer = setTimeout ( ( ) => {
219
+ this . updatePointerPosition ( anchor , currentStep ) ;
220
+ } , 150 ) ;
211
221
} ;
212
222
el . addEventListener ( "scroll" , handler , { passive : true } ) ;
213
223
this . _scrollListeners . push ( { el, handler } ) ;
214
224
} ) ;
215
225
}
216
226
217
- // === Remove previously attached scroll listeners ===
227
+ // Remove scroll listeners and clear debounce timer.
218
228
removeScrollListeners ( ) {
219
229
if ( this . _scrollListeners && this . _scrollListeners . length > 0 ) {
220
230
this . _scrollListeners . forEach ( ( { el, handler } ) => {
221
231
el . removeEventListener ( "scroll" , handler ) ;
222
232
} ) ;
223
233
this . _scrollListeners = [ ] ;
224
234
}
235
+ if ( this . _scrollDebounceTimer ) {
236
+ clearTimeout ( this . _scrollDebounceTimer ) ;
237
+ this . _scrollDebounceTimer = null ;
238
+ }
225
239
}
226
240
227
- // === Updated pointer position using fixed positioning and scroll listeners ===
241
+ // Updated pointer position:
242
+ // 1. Scroll the element into view.
243
+ // 2. After the element is fully visible (or after a maximum delay), update the pointer position to point towards the element.
228
244
updatePointerPosition ( anchor , currentStep ) {
229
245
try {
230
- // Use the cached pointer container if available.
231
246
const container =
232
247
this . _pointerContainer || document . getElementById ( pointerContainerId ) ;
233
248
if ( ! container ) return ;
234
249
235
- // If no anchor, center pointer in the viewport.
236
250
if ( ! anchor ) {
237
251
container . style . position = "fixed" ;
238
252
container . style . left = "50%" ;
@@ -244,44 +258,54 @@ export default class GleapCopilotTours {
244
258
this . _currentStep = null ;
245
259
return ;
246
260
}
247
- // Calculate element’s position relative to the viewport.
248
- const anchorRect = anchor . getBoundingClientRect ( ) ;
249
- let anchorCenterX = anchorRect . left + anchorRect . width / 2 ;
250
- let anchorCenterY = anchorRect . top + anchorRect . height / 2 ;
251
- if ( currentStep ?. mode === "INPUT" ) {
252
- anchorCenterX -= anchorRect . width / 2 - 10 ;
253
- anchorCenterY += anchorRect . height / 2 - 5 ;
254
- }
255
- container . style . position = "fixed" ;
256
- container . style . left = `${ anchorCenterX } px` ;
257
- container . style . top = `${ anchorCenterY } px` ;
258
- container . style . transform = "translate(-50%, -50%)" ;
259
-
260
- let containerWidthSpace = 350 ;
261
- if ( containerWidthSpace > window . innerWidth - 40 ) {
262
- containerWidthSpace = window . innerWidth - 40 ;
263
- }
264
- const windowWidth = window . innerWidth ;
265
- const isTooFarRight =
266
- anchorCenterX + containerWidthSpace > windowWidth - 20 ;
267
- if ( isTooFarRight ) {
268
- container . classList . add ( "copilot-pointer-container-right" ) ;
269
- } else {
270
- container . classList . remove ( "copilot-pointer-container-right" ) ;
271
- }
272
261
273
- // If desired, consider debouncing this auto-scroll to avoid jarring effects .
262
+ // Step 1: Scroll the element into view .
274
263
scrollToElement ( anchor ) ;
275
264
276
- // Reattach scroll listeners if the target or step has changed.
277
- if ( this . _currentAnchor !== anchor || this . _currentStep !== currentStep ) {
278
- this . removeScrollListeners ( ) ;
279
- this . _currentAnchor = anchor ;
280
- this . _currentStep = currentStep ;
281
- this . attachScrollListeners ( anchor , currentStep ) ;
282
- }
265
+ // Step 2: Poll until the element is fully visible (or after maximum polls).
266
+ const pollInterval = 100 ;
267
+ const maxPolls = 20 ;
268
+ let pollCount = 0 ;
269
+ const updateFinalPosition = ( ) => {
270
+ if ( isElementFullyVisible ( anchor ) || pollCount >= maxPolls ) {
271
+ // Compute final target coordinates.
272
+ const anchorRect = anchor . getBoundingClientRect ( ) ;
273
+ const targetX = anchorRect . left + anchorRect . width / 2 ;
274
+ const targetY = anchorRect . top + anchorRect . height / 2 + 10 ; // 10px downward offset.
275
+ container . style . position = "fixed" ;
276
+ container . style . left = `${ targetX } px` ;
277
+ container . style . top = `${ targetY } px` ;
278
+ container . style . transform = "translate(-50%, -50%)" ;
279
+
280
+ // Adjust container if too far right.
281
+ let containerWidthSpace = 350 ;
282
+ if ( containerWidthSpace > window . innerWidth - 40 ) {
283
+ containerWidthSpace = window . innerWidth - 40 ;
284
+ }
285
+ if ( targetX + containerWidthSpace > window . innerWidth - 20 ) {
286
+ container . classList . add ( "copilot-pointer-container-right" ) ;
287
+ } else {
288
+ container . classList . remove ( "copilot-pointer-container-right" ) ;
289
+ }
290
+
291
+ // Reattach scroll listeners if the target or step has changed.
292
+ if (
293
+ this . _currentAnchor !== anchor ||
294
+ this . _currentStep !== currentStep
295
+ ) {
296
+ this . removeScrollListeners ( ) ;
297
+ this . _currentAnchor = anchor ;
298
+ this . _currentStep = currentStep ;
299
+ this . attachScrollListeners ( anchor , currentStep ) ;
300
+ }
301
+ } else {
302
+ pollCount ++ ;
303
+ setTimeout ( updateFinalPosition , pollInterval ) ;
304
+ }
305
+ } ;
306
+ updateFinalPosition ( ) ;
283
307
} catch ( e ) {
284
- // Optionally log errors here .
308
+ // Optionally log errors.
285
309
}
286
310
}
287
311
@@ -563,7 +587,7 @@ export default class GleapCopilotTours {
563
587
const container = document . createElement ( "div" ) ;
564
588
container . id = pointerContainerId ;
565
589
container . style . opacity = 0 ;
566
- // Cache the pointer container reference .
590
+ // Cache the pointer container.
567
591
this . _pointerContainer = container ;
568
592
569
593
const svgMouse = document . createElementNS (
0 commit comments