@@ -20,7 +20,7 @@ import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-
2020import { TextAnchor } from 'realtime-server/lib/esm/scriptureforge/models/text-anchor' ;
2121import { StringMap } from 'rich-text' ;
2222import { fromEvent , Subject , Subscription , timer } from 'rxjs' ;
23- import { takeUntil } from 'rxjs/operators' ;
23+ import { takeUntil , tap } from 'rxjs/operators' ;
2424import { LocalPresence , Presence } from 'sharedb/lib/sharedb' ;
2525import tinyColor from 'tinycolor2' ;
2626import { WINDOW } from 'xforge-common/browser-globals' ;
@@ -113,12 +113,39 @@ export class TextComponent implements AfterViewInit, OnDestroy {
113113 @Output ( ) editorCreated = new EventEmitter < void > ( ) ;
114114
115115 lang : string = '' ;
116+
117+ /**
118+ * Flag activated when user presses and holds keys that cause the cursor to move.
119+ * A true value will cause the system cursor to be used instead of
120+ * Quill custom local cursor in order to avoid cursor lag.
121+ */
122+ isCursorMoveKeyDown = false ;
123+
116124 // only use USX formats and not default Quill formats
117125 readonly allowedFormats : string [ ] = this . quillFormatRegistry . getRegisteredFormats ( ) ;
118126 // allow for different CSS based on the browser engine
119127 readonly browserEngine : string = getBrowserEngine ( ) ;
120128 readonly cursorColor : string ;
121129
130+ /** Set of currently pressed keys that move the cursor. */
131+ private readonly pressedCursorMoveKeys = new Set < string > ( ) ;
132+
133+ /** Set of non-printable keys that move the cursor. */
134+ private readonly nonPrintableCursorMoveKeys = new Set < string > ( [
135+ 'ArrowLeft' ,
136+ 'ArrowRight' ,
137+ 'ArrowUp' ,
138+ 'ArrowDown' ,
139+ 'Home' ,
140+ 'End' ,
141+ 'PageUp' ,
142+ 'PageDown' ,
143+ 'Tab'
144+ ] ) ;
145+
146+ private cursorMoveKeyHoldTimeout ?: any ;
147+ private cursorMoveKeyHoldDelay : number = 500 ; // Press and hold ms delay before switching to system cursor
148+
122149 private clickSubs : Map < string , Subscription [ ] > = new Map < string , Subscription [ ] > ( ) ;
123150 private _isReadOnly : boolean = true ;
124151 private _editorStyles : any = { fontSize : '1rem' } ;
@@ -533,21 +560,56 @@ export class TextComponent implements AfterViewInit, OnDestroy {
533560 } ) ;
534561
535562 fromEvent < KeyboardEvent > ( this . document , 'keydown' )
536- . pipe ( quietTakeUntilDestroyed ( this . destroyRef ) )
563+ . pipe (
564+ quietTakeUntilDestroyed ( this . destroyRef ) ,
565+ tap ( event => ( this . isShiftDown = event . shiftKey ) )
566+ )
537567 . subscribe ( event => {
538- this . isShiftDown = event . shiftKey ;
568+ // Set flag to use system cursor when any key is down that would move the cursor (avoids cursor lag issue)
569+ if ( this . nonPrintableCursorMoveKeys . has ( event . key ) || event . key . length === 1 ) {
570+ this . pressedCursorMoveKeys . add ( event . key ) ;
571+
572+ // Only set the flag when the user presses and holds (detect with a short timeout delay)
573+ if ( ! this . isCursorMoveKeyDown && this . cursorMoveKeyHoldTimeout == null ) {
574+ this . cursorMoveKeyHoldTimeout = setTimeout ( ( ) => {
575+ if ( this . pressedCursorMoveKeys . size > 0 ) {
576+ this . isCursorMoveKeyDown = true ;
577+ }
578+
579+ this . cursorMoveKeyHoldTimeout = undefined ;
580+ } , this . cursorMoveKeyHoldDelay ) ;
581+ }
582+ }
539583 } ) ;
540584
541585 fromEvent < KeyboardEvent > ( this . document , 'keyup' )
542- . pipe ( quietTakeUntilDestroyed ( this . destroyRef ) )
586+ . pipe (
587+ quietTakeUntilDestroyed ( this . destroyRef ) ,
588+ tap ( event => {
589+ // Call 'update()' when shift key is released, as update is disabled while shift is down
590+ // to prevent incorrect cursor position updates while selecting text.
591+ if ( this . isShiftDown && ! event . shiftKey ) {
592+ this . update ( ) ;
593+ }
594+
595+ this . isShiftDown = event . shiftKey ;
596+ } )
597+ )
543598 . subscribe ( event => {
544- // Call 'update()' when shift key is released, as update is disabled while shift is down
545- // to prevent incorrect cursor position updates while selecting text.
546- if ( this . isShiftDown && ! event . shiftKey ) {
547- this . update ( ) ;
548- }
599+ this . pressedCursorMoveKeys . delete ( event . key ) ;
549600
550- this . isShiftDown = event . shiftKey ;
601+ // If set is empty, all cursor movement keys are released
602+ if ( this . pressedCursorMoveKeys . size === 0 ) {
603+ if ( this . cursorMoveKeyHoldTimeout ) {
604+ clearTimeout ( this . cursorMoveKeyHoldTimeout ) ;
605+ this . cursorMoveKeyHoldTimeout = undefined ;
606+ }
607+
608+ // Helps to not yet show custom local cursor until it has caught up to system cursor that was just visible
609+ requestAnimationFrame ( ( ) => {
610+ this . isCursorMoveKeyDown = false ;
611+ } ) ;
612+ }
551613 } ) ;
552614
553615 fromEvent < FocusEvent > ( this . window , 'blur' )
0 commit comments