@@ -30,6 +30,8 @@ export default class TerminalTouchSelection {
3030 this . initialTouchPos = { x : 0 , y : 0 } ;
3131 this . tapHoldTimeout = null ;
3232 this . dragHandle = null ;
33+ this . isSelectionTouchActive = false ;
34+ this . pendingSelectionClearTouch = null ;
3335
3436 // Zoom tracking
3537 this . pinchStartDistance = 0 ;
@@ -59,6 +61,12 @@ export default class TerminalTouchSelection {
5961 this . selectionProtected = false ;
6062 this . protectionTimeout = null ;
6163
64+ // Scroll tracking
65+ this . scrollElement = null ;
66+ this . isTerminalScrolling = false ;
67+ this . scrollEndTimeout = null ;
68+ this . scrollEndDelay = 100 ;
69+
6270 this . init ( ) ;
6371 }
6472
@@ -189,15 +197,6 @@ export default class TerminalTouchSelection {
189197 this . boundHandlers . selectionChange = this . onSelectionChange . bind ( this ) ;
190198 this . terminal . onSelectionChange ( this . boundHandlers . selectionChange ) ;
191199
192- // Click outside to clear selection - only within terminal area
193- this . boundHandlers . terminalAreaTouchStart =
194- this . onTerminalAreaTouchStart . bind ( this ) ;
195- this . terminal . element . addEventListener (
196- "touchstart" ,
197- this . boundHandlers . terminalAreaTouchStart ,
198- { passive : false } ,
199- ) ;
200-
201200 // Orientation change
202201 this . boundHandlers . orientationChange = this . onOrientationChange . bind ( this ) ;
203202 window . addEventListener (
@@ -208,7 +207,10 @@ export default class TerminalTouchSelection {
208207
209208 // Terminal scroll listener
210209 this . boundHandlers . terminalScroll = this . onTerminalScroll . bind ( this ) ;
211- this . terminal . element . addEventListener (
210+ this . scrollElement =
211+ this . terminal . element . querySelector ( ".xterm-viewport" ) ||
212+ this . terminal . element ;
213+ this . scrollElement . addEventListener (
212214 "scroll" ,
213215 this . boundHandlers . terminalScroll ,
214216 { passive : true } ,
@@ -237,6 +239,14 @@ export default class TerminalTouchSelection {
237239
238240 // If already selecting, don't start new selection
239241 if ( this . isSelecting ) {
242+ this . isSelectionTouchActive = false ;
243+ this . pendingSelectionClearTouch = {
244+ x : touch . clientX ,
245+ y : touch . clientY ,
246+ moved : false ,
247+ } ;
248+ // Hide menu while user scrolls or repositions, then restore on touch end.
249+ this . hideContextMenu ( true ) ;
240250 return ;
241251 }
242252
@@ -251,6 +261,8 @@ export default class TerminalTouchSelection {
251261 }
252262
253263 // Start tap-hold timer
264+ this . pendingSelectionClearTouch = null ;
265+ this . isSelectionTouchActive = false ;
254266 this . tapHoldTimeout = setTimeout ( ( ) => {
255267 if ( ! this . isSelecting && ! this . isPinching ) {
256268 this . startSelection ( touch ) ;
@@ -275,6 +287,18 @@ export default class TerminalTouchSelection {
275287 const deltaX = Math . abs ( touch . clientX - this . touchStartPos . x ) ;
276288 const deltaY = Math . abs ( touch . clientY - this . touchStartPos . y ) ;
277289 const horizontalDelta = touch . clientX - this . touchStartPos . x ;
290+ const clearTouch = this . pendingSelectionClearTouch ;
291+
292+ if ( clearTouch ) {
293+ const clearDeltaX = Math . abs ( touch . clientX - clearTouch . x ) ;
294+ const clearDeltaY = Math . abs ( touch . clientY - clearTouch . y ) ;
295+ if (
296+ clearDeltaX > this . options . moveThreshold ||
297+ clearDeltaY > this . options . moveThreshold
298+ ) {
299+ clearTouch . moved = true ;
300+ }
301+ }
278302
279303 // Check if this looks like a back gesture (started near edge and moving horizontally inward)
280304 if (
@@ -301,7 +325,11 @@ export default class TerminalTouchSelection {
301325 }
302326
303327 // If we're selecting, extend selection
304- if ( this . isSelecting && ! this . isHandleDragging ) {
328+ if (
329+ this . isSelecting &&
330+ ! this . isHandleDragging &&
331+ this . isSelectionTouchActive
332+ ) {
305333 event . preventDefault ( ) ;
306334 this . extendSelection ( touch ) ;
307335 }
@@ -320,8 +348,25 @@ export default class TerminalTouchSelection {
320348 this . tapHoldTimeout = null ;
321349 }
322350
351+ const shouldClearSelectionByTap =
352+ this . isSelecting &&
353+ ! this . isHandleDragging &&
354+ this . pendingSelectionClearTouch &&
355+ ! this . pendingSelectionClearTouch . moved &&
356+ ! this . isTerminalScrolling &&
357+ ! this . selectionProtected ;
358+
359+ this . pendingSelectionClearTouch = null ;
360+ this . isSelectionTouchActive = false ;
361+
362+ if ( shouldClearSelectionByTap ) {
363+ this . clearSelection ( ) ;
364+ return ;
365+ }
366+
323367 // If we were selecting and not dragging handles, finalize selection
324368 if ( this . isSelecting && ! this . isHandleDragging ) {
369+ if ( this . isTerminalScrolling ) return ;
325370 this . finalizeSelection ( ) ;
326371 } else if ( ! this . isSelecting ) {
327372 // Only focus terminal on touch end if not selecting and terminal was already focused
@@ -365,6 +410,8 @@ export default class TerminalTouchSelection {
365410
366411 this . isHandleDragging = true ;
367412 this . dragHandle = handleType ;
413+ this . isSelectionTouchActive = false ;
414+ this . pendingSelectionClearTouch = null ;
368415
369416 // Store the initial touch position for delta calculations
370417 const touch = event . touches [ 0 ] ;
@@ -452,9 +499,6 @@ export default class TerminalTouchSelection {
452499 event . preventDefault ( ) ;
453500 event . stopPropagation ( ) ;
454501
455- // Store the current drag handle before clearing
456- const currentDragHandle = this . dragHandle ;
457-
458502 this . isHandleDragging = false ;
459503 this . dragHandle = null ;
460504
@@ -481,43 +525,6 @@ export default class TerminalTouchSelection {
481525 }
482526 }
483527
484- onTerminalAreaTouchStart ( event ) {
485- // Clear selection if touching terminal area while selecting, except on handles or context menu
486- if ( this . isSelecting ) {
487- // Don't clear selection if it's protected (during keyboard events)
488- if ( this . selectionProtected ) {
489- return ;
490- }
491-
492- // Don't interfere with context menu at all
493- if ( this . contextMenu && this . contextMenu . style . display === "flex" ) {
494- // Context menu is visible, check if touching it
495- const rect = this . contextMenu . getBoundingClientRect ( ) ;
496- const touchX = event . touches [ 0 ] . clientX ;
497- const touchY = event . touches [ 0 ] . clientY ;
498-
499- if (
500- touchX >= rect . left &&
501- touchX <= rect . right &&
502- touchY >= rect . top &&
503- touchY <= rect . bottom
504- ) {
505- // Touching context menu area, don't clear selection
506- return ;
507- }
508- }
509-
510- const isHandleTouch =
511- this . startHandle . contains ( event . target ) ||
512- this . endHandle . contains ( event . target ) ;
513-
514- // Only clear if touching within terminal but not on handles
515- if ( ! isHandleTouch && this . terminal . element . contains ( event . target ) ) {
516- this . clearSelection ( ) ;
517- }
518- }
519- }
520-
521528 onOrientationChange ( ) {
522529 // Update cell dimensions and handle positions after orientation change
523530 setTimeout ( ( ) => {
@@ -529,13 +536,28 @@ export default class TerminalTouchSelection {
529536 }
530537
531538 onTerminalScroll ( ) {
532- // Update handle positions when terminal is scrolled
533- if ( this . isSelecting ) {
534- this . updateHandlePositions ( ) ;
535- // Hide context menu if it's open during scroll
536- if ( this . contextMenu && this . contextMenu . style . display === "flex" ) {
537- this . hideContextMenu ( ) ;
538- }
539+ if ( ! this . isSelecting || this . isHandleDragging ) return ;
540+
541+ this . isTerminalScrolling = true ;
542+ this . hideHandles ( ) ;
543+ this . hideContextMenu ( true ) ;
544+
545+ if ( this . scrollEndTimeout ) {
546+ clearTimeout ( this . scrollEndTimeout ) ;
547+ }
548+ this . scrollEndTimeout = setTimeout ( ( ) => {
549+ this . onTerminalScrollEnd ( ) ;
550+ } , this . scrollEndDelay ) ;
551+ }
552+
553+ onTerminalScrollEnd ( ) {
554+ this . scrollEndTimeout = null ;
555+ this . isTerminalScrolling = false ;
556+ if ( ! this . isSelecting || this . isHandleDragging ) return ;
557+
558+ this . updateHandlePositions ( ) ;
559+ if ( this . contextMenuShouldStayVisible && this . options . showContextMenu ) {
560+ this . showContextMenu ( ) ;
539561 }
540562 }
541563
@@ -565,7 +587,7 @@ export default class TerminalTouchSelection {
565587 this . updateHandlePositions ( ) ;
566588 // Temporarily hide context menu during resize but keep selection
567589 if ( this . contextMenu && this . contextMenu . style . display === "flex" ) {
568- this . hideContextMenu ( ) ;
590+ this . hideContextMenu ( true ) ;
569591 }
570592 // Re-show context menu after resize if selection is still active
571593 setTimeout ( ( ) => {
@@ -596,6 +618,8 @@ export default class TerminalTouchSelection {
596618 } , 1000 ) ;
597619
598620 this . isSelecting = true ;
621+ this . isSelectionTouchActive = true ;
622+ this . pendingSelectionClearTouch = null ;
599623
600624 // Try to auto-select word at touch position
601625 const wordBounds = this . getWordBoundsAt ( coords ) ;
@@ -873,9 +897,9 @@ export default class TerminalTouchSelection {
873897 this . selectionOverlay . appendChild ( this . contextMenu ) ;
874898 }
875899
876- hideContextMenu ( ) {
900+ hideContextMenu ( force = false ) {
877901 // Only hide if explicitly requested or if context menu should not stay visible
878- if ( this . contextMenu && ! this . contextMenuShouldStayVisible ) {
902+ if ( this . contextMenu && ( force || ! this . contextMenuShouldStayVisible ) ) {
879903 this . contextMenu . style . display = "none" ;
880904 }
881905 }
@@ -930,6 +954,9 @@ export default class TerminalTouchSelection {
930954 this . selectionEnd = null ;
931955 this . currentSelection = null ;
932956 this . dragHandle = null ;
957+ this . pendingSelectionClearTouch = null ;
958+ this . isSelectionTouchActive = false ;
959+ this . isTerminalScrolling = false ;
933960
934961 this . terminal . clearSelection ( ) ;
935962 this . hideHandles ( ) ;
@@ -939,6 +966,10 @@ export default class TerminalTouchSelection {
939966 clearTimeout ( this . tapHoldTimeout ) ;
940967 this . tapHoldTimeout = null ;
941968 }
969+ if ( this . scrollEndTimeout ) {
970+ clearTimeout ( this . scrollEndTimeout ) ;
971+ this . scrollEndTimeout = null ;
972+ }
942973
943974 // Clear protection timeout
944975 if ( this . protectionTimeout ) {
@@ -963,7 +994,6 @@ export default class TerminalTouchSelection {
963994
964995 forceClearSelection ( ) {
965996 // Temporarily disable protection to force clear
966- const wasProtected = this . selectionProtected ;
967997 this . selectionProtected = false ;
968998 this . clearSelection ( ) ;
969999 // Don't restore protection state since we're clearing
@@ -1225,20 +1255,24 @@ export default class TerminalTouchSelection {
12251255 this . boundHandlers . handleTouchEnd ,
12261256 ) ;
12271257
1228- this . terminal . element . removeEventListener (
1229- "touchstart" ,
1230- this . boundHandlers . terminalAreaTouchStart ,
1231- ) ;
1232- this . terminal . element . removeEventListener (
1233- "scroll" ,
1234- this . boundHandlers . terminalScroll ,
1235- ) ;
1258+ if ( this . scrollElement ) {
1259+ this . scrollElement . removeEventListener (
1260+ "scroll" ,
1261+ this . boundHandlers . terminalScroll ,
1262+ ) ;
1263+ this . scrollElement = null ;
1264+ }
12361265 window . removeEventListener (
12371266 "orientationchange" ,
12381267 this . boundHandlers . orientationChange ,
12391268 ) ;
12401269 window . removeEventListener ( "resize" , this . boundHandlers . orientationChange ) ;
12411270
1271+ if ( this . scrollEndTimeout ) {
1272+ clearTimeout ( this . scrollEndTimeout ) ;
1273+ this . scrollEndTimeout = null ;
1274+ }
1275+
12421276 // Remove selection change listener
12431277 if ( this . terminal . onSelectionChange ) {
12441278 this . terminal . onSelectionChange ( null ) ;
0 commit comments