@@ -7,7 +7,9 @@ const styleId = "copilot-tour-styles";
77const copilotJoinedContainerId = "copilot-joined-container" ;
88const copilotInfoBubbleId = "copilot-info-bubble" ;
99
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>` ;
1113
1214function estimateReadTime ( text ) {
1315 const wordsPerSecond = 3.6 ; // Average reading speed
@@ -25,7 +27,7 @@ function htmlToPlainText(html) {
2527function scrollToElement ( element ) {
2628 if ( element ) {
2729 element . scrollIntoView ( {
28- behavior : "smooth" , // Ensures smooth scrolling
30+ behavior : "smooth" ,
2931 block : "center" ,
3032 inline : "center" ,
3133 } ) ;
@@ -83,7 +85,18 @@ async function canPlayAudio() {
8385 }
8486}
8587
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.
87100function getScrollableAncestors ( el ) {
88101 let ancestors = [ ] ;
89102 let current = el . parentElement ;
@@ -110,13 +123,13 @@ export default class GleapCopilotTours {
110123 audioMuted = false ;
111124 currentAudio = undefined ;
112125
113- // Cached pointer container reference .
126+ // Cached pointer container.
114127 _pointerContainer = null ;
115- // New properties for scroll handling.
128+ // For scroll handling.
116129 _scrollListeners = [ ] ;
117130 _currentAnchor = null ;
118131 _currentStep = null ;
119- _updateScheduled = false ;
132+ _scrollDebounceTimer = null ;
120133
121134 // GleapReplayRecorder singleton.
122135 static instance ;
@@ -132,7 +145,7 @@ export default class GleapCopilotTours {
132145 this . _scrollListeners = [ ] ;
133146 this . _currentAnchor = null ;
134147 this . _currentStep = null ;
135- this . _updateScheduled = false ;
148+ this . _scrollDebounceTimer = null ;
136149
137150 window . addEventListener ( "resize" , ( ) => {
138151 if (
@@ -193,46 +206,47 @@ export default class GleapCopilotTours {
193206 }
194207 }
195208
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.
197210 attachScrollListeners ( anchor , currentStep ) {
198211 if ( ! anchor ) return ;
199212 const scrollableAncestors = getScrollableAncestors ( anchor ) ;
200- // Also include window to catch any page-level scrolling .
213+ // Also include window.
201214 scrollableAncestors . push ( window ) ;
202215 scrollableAncestors . forEach ( ( el ) => {
203216 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 ) ;
211221 } ;
212222 el . addEventListener ( "scroll" , handler , { passive : true } ) ;
213223 this . _scrollListeners . push ( { el, handler } ) ;
214224 } ) ;
215225 }
216226
217- // === Remove previously attached scroll listeners ===
227+ // Remove scroll listeners and clear debounce timer.
218228 removeScrollListeners ( ) {
219229 if ( this . _scrollListeners && this . _scrollListeners . length > 0 ) {
220230 this . _scrollListeners . forEach ( ( { el, handler } ) => {
221231 el . removeEventListener ( "scroll" , handler ) ;
222232 } ) ;
223233 this . _scrollListeners = [ ] ;
224234 }
235+ if ( this . _scrollDebounceTimer ) {
236+ clearTimeout ( this . _scrollDebounceTimer ) ;
237+ this . _scrollDebounceTimer = null ;
238+ }
225239 }
226240
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.
228244 updatePointerPosition ( anchor , currentStep ) {
229245 try {
230- // Use the cached pointer container if available.
231246 const container =
232247 this . _pointerContainer || document . getElementById ( pointerContainerId ) ;
233248 if ( ! container ) return ;
234249
235- // If no anchor, center pointer in the viewport.
236250 if ( ! anchor ) {
237251 container . style . position = "fixed" ;
238252 container . style . left = "50%" ;
@@ -244,44 +258,54 @@ export default class GleapCopilotTours {
244258 this . _currentStep = null ;
245259 return ;
246260 }
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- }
272261
273- // If desired, consider debouncing this auto-scroll to avoid jarring effects .
262+ // Step 1: Scroll the element into view .
274263 scrollToElement ( anchor ) ;
275264
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 ( ) ;
283307 } catch ( e ) {
284- // Optionally log errors here .
308+ // Optionally log errors.
285309 }
286310 }
287311
@@ -563,7 +587,7 @@ export default class GleapCopilotTours {
563587 const container = document . createElement ( "div" ) ;
564588 container . id = pointerContainerId ;
565589 container . style . opacity = 0 ;
566- // Cache the pointer container reference .
590+ // Cache the pointer container.
567591 this . _pointerContainer = container ;
568592
569593 const svgMouse = document . createElementNS (
0 commit comments