11import { MarkdownEditorInput , MarkdownEditorInputSelection } from "./interface" ;
22import { MarkdownEditorShortcutMap } from "../shortcuts" ;
33import { MarkdownEditorEventMap } from "../dom-handlers" ;
4+ import { debounce } from "../../services/util" ;
45
6+ type UndoStackEntry = {
7+ content : string ;
8+ selection : MarkdownEditorInputSelection ;
9+ }
10+
11+ class UndoStack {
12+ protected onChangeDebounced : ( callback : ( ) => UndoStackEntry ) => void ;
13+
14+ protected stack : UndoStackEntry [ ] = [ ] ;
15+ protected pointer : number = - 1 ;
16+ protected lastActionTime : number = 0 ;
17+
18+ constructor ( ) {
19+ this . onChangeDebounced = debounce ( this . onChange , 1000 , false ) ;
20+ }
21+
22+ undo ( ) : UndoStackEntry | null {
23+ if ( this . pointer < 1 ) {
24+ return null ;
25+ }
26+
27+ this . lastActionTime = Date . now ( ) ;
28+ this . pointer -= 1 ;
29+ return this . stack [ this . pointer ] ;
30+ }
31+
32+ redo ( ) : UndoStackEntry | null {
33+ const atEnd = this . pointer === this . stack . length - 1 ;
34+ if ( atEnd ) {
35+ return null ;
36+ }
37+
38+ this . lastActionTime = Date . now ( ) ;
39+ this . pointer ++ ;
40+ return this . stack [ this . pointer ] ;
41+ }
42+
43+ push ( getValueCallback : ( ) => UndoStackEntry ) : void {
44+ // Ignore changes made via undo/redo actions
45+ if ( Date . now ( ) - this . lastActionTime < 100 ) {
46+ return ;
47+ }
48+
49+ this . onChangeDebounced ( getValueCallback ) ;
50+ }
51+
52+ protected onChange ( getValueCallback : ( ) => UndoStackEntry ) {
53+ // Trim the end of the stack from the pointer since we're branching away
54+ if ( this . pointer !== this . stack . length - 1 ) {
55+ this . stack = this . stack . slice ( 0 , this . pointer )
56+ }
57+
58+ this . stack . push ( getValueCallback ( ) ) ;
59+
60+ // Limit stack size
61+ if ( this . stack . length > 50 ) {
62+ this . stack = this . stack . slice ( this . stack . length - 50 ) ;
63+ }
64+
65+ this . pointer = this . stack . length - 1 ;
66+ }
67+ }
568
669export class TextareaInput implements MarkdownEditorInput {
770
@@ -10,6 +73,7 @@ export class TextareaInput implements MarkdownEditorInput {
1073 protected events : MarkdownEditorEventMap ;
1174 protected onChange : ( ) => void ;
1275 protected eventController = new AbortController ( ) ;
76+ protected undoStack = new UndoStack ( ) ;
1377
1478 protected textSizeCache : { x : number ; y : number } | null = null ;
1579
@@ -25,17 +89,34 @@ export class TextareaInput implements MarkdownEditorInput {
2589 this . onChange = onChange ;
2690
2791 this . onKeyDown = this . onKeyDown . bind ( this ) ;
92+ this . configureLocalShortcuts ( ) ;
2893 this . configureListeners ( ) ;
2994
30- // TODO - Undo/Redo
31-
3295 this . input . style . removeProperty ( "display" ) ;
96+ this . undoStack . push ( ( ) => ( { content : this . getText ( ) , selection : this . getSelection ( ) } ) ) ;
3397 }
3498
3599 teardown ( ) {
36100 this . eventController . abort ( 'teardown' ) ;
37101 }
38102
103+ configureLocalShortcuts ( ) : void {
104+ this . shortcuts [ 'Mod-z' ] = ( ) => {
105+ const undoEntry = this . undoStack . undo ( ) ;
106+ if ( undoEntry ) {
107+ this . setText ( undoEntry . content ) ;
108+ this . setSelection ( undoEntry . selection , false ) ;
109+ }
110+ } ;
111+ this . shortcuts [ 'Mod-y' ] = ( ) => {
112+ const redoContent = this . undoStack . redo ( ) ;
113+ if ( redoContent ) {
114+ this . setText ( redoContent . content ) ;
115+ this . setSelection ( redoContent . selection , false ) ;
116+ }
117+ }
118+ }
119+
39120 configureListeners ( ) : void {
40121 // Keyboard shortcuts
41122 this . input . addEventListener ( 'keydown' , this . onKeyDown , { signal : this . eventController . signal } ) ;
@@ -48,15 +129,8 @@ export class TextareaInput implements MarkdownEditorInput {
48129 // Input change handling
49130 this . input . addEventListener ( 'input' , ( ) => {
50131 this . onChange ( ) ;
132+ this . undoStack . push ( ( ) => ( { content : this . input . value , selection : this . getSelection ( ) } ) ) ;
51133 } , { signal : this . eventController . signal } ) ;
52-
53- this . input . addEventListener ( 'click' , ( event : MouseEvent ) => {
54- const x = event . clientX ;
55- const y = event . clientY ;
56- const range = this . eventToPosition ( event ) ;
57- const text = this . getText ( ) . split ( '' ) ;
58- console . log ( range , text . slice ( 0 , 20 ) ) ;
59- } ) ;
60134 }
61135
62136 onKeyDown ( e : KeyboardEvent ) {
@@ -83,33 +157,7 @@ export class TextareaInput implements MarkdownEditorInput {
83157
84158 eventToPosition ( event : MouseEvent ) : MarkdownEditorInputSelection {
85159 const eventCoords = this . mouseEventToTextRelativeCoords ( event ) ;
86- const textSize = this . measureTextSize ( ) ;
87- const lineWidth = this . measureLineCharCount ( textSize . x ) ;
88-
89- const lines = this . getText ( ) . split ( '\n' ) ;
90-
91- // TODO - Check this
92-
93- let currY = 0 ;
94- let currPos = 0 ;
95- for ( const line of lines ) {
96- let linePos = 0 ;
97- const wrapCount = Math . max ( Math . ceil ( line . length / lineWidth ) , 1 ) ;
98- for ( let i = 0 ; i < wrapCount ; i ++ ) {
99- currY += textSize . y ;
100- if ( currY > eventCoords . y ) {
101- const targetX = Math . floor ( eventCoords . x / textSize . x ) ;
102- const maxPos = Math . min ( currPos + linePos + targetX , currPos + line . length ) ;
103- return { from : maxPos , to : maxPos } ;
104- }
105-
106- linePos += lineWidth ;
107- }
108-
109- currPos += line . length + 1 ;
110- }
111-
112- return this . getSelection ( ) ;
160+ return this . inputPositionToSelection ( eventCoords . x , eventCoords . y ) ;
113161 }
114162
115163 focus ( ) : void {
@@ -153,15 +201,8 @@ export class TextareaInput implements MarkdownEditorInput {
153201
154202 getTextAboveView ( ) : string {
155203 const scrollTop = this . input . scrollTop ;
156- const computedStyles = window . getComputedStyle ( this . input ) ;
157- const lines = this . getText ( ) . split ( '\n' ) ;
158- const paddingTop = Number ( computedStyles . paddingTop . replace ( 'px' , '' ) ) ;
159- const paddingBottom = Number ( computedStyles . paddingBottom . replace ( 'px' , '' ) ) ;
160-
161- const avgLineHeight = ( this . input . scrollHeight - paddingBottom - paddingTop ) / lines . length ;
162- const roughLinePos = Math . max ( Math . floor ( ( scrollTop - paddingTop ) / avgLineHeight ) , 0 ) ;
163- const linesAbove = this . getText ( ) . split ( '\n' ) . slice ( 0 , roughLinePos ) ;
164- return linesAbove . join ( '\n' ) ;
204+ const selection = this . inputPositionToSelection ( 0 , scrollTop ) ;
205+ return this . getSelectionText ( { from : 0 , to : selection . to } ) ;
165206 }
166207
167208 searchForLineContaining ( text : string ) : MarkdownEditorInputSelection | null {
@@ -243,4 +284,32 @@ export class TextareaInput implements MarkdownEditorInput {
243284
244285 return { x : xPos , y : yPos } ;
245286 }
287+
288+ protected inputPositionToSelection ( x : number , y : number ) : MarkdownEditorInputSelection {
289+ const textSize = this . measureTextSize ( ) ;
290+ const lineWidth = this . measureLineCharCount ( textSize . x ) ;
291+
292+ const lines = this . getText ( ) . split ( '\n' ) ;
293+
294+ let currY = 0 ;
295+ let currPos = 0 ;
296+ for ( const line of lines ) {
297+ let linePos = 0 ;
298+ const wrapCount = Math . max ( Math . ceil ( line . length / lineWidth ) , 1 ) ;
299+ for ( let i = 0 ; i < wrapCount ; i ++ ) {
300+ currY += textSize . y ;
301+ if ( currY > y ) {
302+ const targetX = Math . floor ( x / textSize . x ) ;
303+ const maxPos = Math . min ( currPos + linePos + targetX , currPos + line . length ) ;
304+ return { from : maxPos , to : maxPos } ;
305+ }
306+
307+ linePos += lineWidth ;
308+ }
309+
310+ currPos += line . length + 1 ;
311+ }
312+
313+ return this . getSelection ( ) ;
314+ }
246315}
0 commit comments