1
+ import { DOCUMENT } from '@angular/common' ;
1
2
import {
2
3
AfterViewInit ,
3
4
ChangeDetectorRef ,
4
5
Component ,
5
6
DestroyRef ,
6
7
EventEmitter ,
8
+ Inject ,
7
9
Input ,
8
10
OnDestroy ,
9
11
Output
@@ -18,9 +20,10 @@ import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-
18
20
import { TextAnchor } from 'realtime-server/lib/esm/scriptureforge/models/text-anchor' ;
19
21
import { StringMap } from 'rich-text' ;
20
22
import { fromEvent , Subject , Subscription , timer } from 'rxjs' ;
21
- import { takeUntil } from 'rxjs/operators' ;
23
+ import { takeUntil , tap } from 'rxjs/operators' ;
22
24
import { LocalPresence , Presence } from 'sharedb/lib/sharedb' ;
23
25
import tinyColor from 'tinycolor2' ;
26
+ import { WINDOW } from 'xforge-common/browser-globals' ;
24
27
import { DialogService } from 'xforge-common/dialog.service' ;
25
28
import { LocaleDirection } from 'xforge-common/models/i18n-locale' ;
26
29
import { UserDoc } from 'xforge-common/models/user-doc' ;
@@ -110,12 +113,39 @@ export class TextComponent implements AfterViewInit, OnDestroy {
110
113
@Output ( ) editorCreated = new EventEmitter < void > ( ) ;
111
114
112
115
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
+
113
124
// only use USX formats and not default Quill formats
114
125
readonly allowedFormats : string [ ] = this . quillFormatRegistry . getRegisteredFormats ( ) ;
115
126
// allow for different CSS based on the browser engine
116
127
readonly browserEngine : string = getBrowserEngine ( ) ;
117
128
readonly cursorColor : string ;
118
129
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
+
119
149
private clickSubs : Map < string , Subscription [ ] > = new Map < string , Subscription [ ] > ( ) ;
120
150
private _isReadOnly : boolean = true ;
121
151
private _editorStyles : any = { fontSize : '1rem' } ;
@@ -263,6 +293,7 @@ export class TextComponent implements AfterViewInit, OnDestroy {
263
293
private presenceActiveEditor$ : Subject < boolean > = new Subject < boolean > ( ) ;
264
294
private onPresenceDocReceive = ( _presenceId : string , _range : Range | null ) : void => { } ;
265
295
private onPresenceChannelReceive = ( _presenceId : string , _presenceData : PresenceData | null ) : void => { } ;
296
+ private isShiftDown = false ;
266
297
267
298
constructor (
268
299
private readonly destroyRef : DestroyRef ,
@@ -274,7 +305,9 @@ export class TextComponent implements AfterViewInit, OnDestroy {
274
305
private readonly userService : UserService ,
275
306
readonly viewModel : TextViewModel ,
276
307
private readonly textDocService : TextDocService ,
277
- private readonly quillFormatRegistry : QuillFormatRegistryService
308
+ private readonly quillFormatRegistry : QuillFormatRegistryService ,
309
+ @Inject ( DOCUMENT ) private document : Document ,
310
+ @Inject ( WINDOW ) private window : Window
278
311
) {
279
312
let localCursorColor = localStorage . getItem ( this . cursorColorStorageKey ) ;
280
313
if ( localCursorColor == null ) {
@@ -520,11 +553,76 @@ export class TextComponent implements AfterViewInit, OnDestroy {
520
553
521
554
// Listening to document 'selectionchange' event allows local cursor to change position on mousedown,
522
555
// as opposed to quill 'onSelectionChange' event that doesn't fire until mouseup.
523
- fromEvent < MouseEvent > ( document , 'selectionchange' )
556
+ fromEvent < MouseEvent > ( this . document , 'selectionchange' )
524
557
. pipe ( quietTakeUntilDestroyed ( this . destroyRef ) )
525
558
. subscribe ( ( ) => {
526
559
this . updateLocalCursor ( ) ;
527
560
} ) ;
561
+
562
+ fromEvent < KeyboardEvent > ( this . document , 'keydown' )
563
+ . pipe (
564
+ quietTakeUntilDestroyed ( this . destroyRef ) ,
565
+ tap ( event => ( this . isShiftDown = event . shiftKey ) )
566
+ )
567
+ . subscribe ( event => {
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
+ }
583
+ } ) ;
584
+
585
+ fromEvent < KeyboardEvent > ( this . document , 'keyup' )
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
+ )
598
+ . subscribe ( event => {
599
+ this . pressedCursorMoveKeys . delete ( event . key ) ;
600
+
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
+ }
613
+ } ) ;
614
+
615
+ fromEvent < FocusEvent > ( this . window , 'blur' )
616
+ . pipe ( quietTakeUntilDestroyed ( this . destroyRef ) )
617
+ . subscribe ( ( ) => {
618
+ // Treat window blur as releasing all modifiers
619
+ const wasShiftDown : boolean = this . isShiftDown === true ;
620
+ if ( wasShiftDown ) {
621
+ this . update ( ) ;
622
+ }
623
+
624
+ this . isShiftDown = false ;
625
+ } ) ;
528
626
}
529
627
530
628
ngOnDestroy ( ) : void {
@@ -545,7 +643,7 @@ export class TextComponent implements AfterViewInit, OnDestroy {
545
643
fromEvent ( this . _editor . root , 'scroll' )
546
644
. pipe ( quietTakeUntilDestroyed ( this . destroyRef ) )
547
645
. subscribe ( ( ) => this . updateHighlightMarkerVisibility ( ) ) ;
548
- fromEvent ( window , 'resize' )
646
+ fromEvent ( this . window , 'resize' )
549
647
. pipe ( quietTakeUntilDestroyed ( this . destroyRef ) )
550
648
. subscribe ( ( ) => this . setHighlightMarkerPosition ( ) ) ;
551
649
this . viewModel . editor = editor ;
@@ -827,7 +925,13 @@ export class TextComponent implements AfterViewInit, OnDestroy {
827
925
}
828
926
829
927
async onSelectionChanged ( range : Range | null ) : Promise < void > {
830
- this . update ( ) ;
928
+ // During selection expansion (keyboard or mouse), avoid calling update()
929
+ // which can cause incorrect cursor position updates.
930
+ // Update will be called once the shift key is released.
931
+ if ( ! this . isShiftDown ) {
932
+ this . update ( ) ;
933
+ }
934
+
831
935
this . submitLocalPresenceDoc ( range ) ;
832
936
}
833
937
@@ -1119,7 +1223,7 @@ export class TextComponent implements AfterViewInit, OnDestroy {
1119
1223
const cursors : QuillCursors = this . editor . getModule ( 'cursors' ) as QuillCursors ;
1120
1224
cursors . createCursor ( this . presenceId , '' , '' ) ;
1121
1225
1122
- this . localCursorElement = document . querySelector ( `#ql-cursor-${ this . presenceId } ` ) ;
1226
+ this . localCursorElement = this . document . querySelector ( `#ql-cursor-${ this . presenceId } ` ) ;
1123
1227
1124
1228
// Add a specific class to the local cursor
1125
1229
if ( this . localCursorElement != null ) {
@@ -1133,7 +1237,7 @@ export class TextComponent implements AfterViewInit, OnDestroy {
1133
1237
return ;
1134
1238
}
1135
1239
1136
- const sel : Selection | null = window . getSelection ( ) ;
1240
+ const sel : Selection | null = this . window . getSelection ( ) ;
1137
1241
if ( sel == null ) {
1138
1242
return ;
1139
1243
}
0 commit comments