11package react
22
33import (
4+ "fmt"
5+ "strconv"
46 "strings"
57
68 "github.com/gopherjs/gopherjs/js"
79)
810
11+ // CodeBox creates a code editor box React element for editing
12+ // the given code state.
913func CodeBox (code string , setCode func (string )) * Element {
1014 return CreateElement (codeBoxComponent , Props {
11- `code` : code ,
15+ `curCode` : code ,
1216 `setCode` : setCode ,
1317 })
1418}
1519
1620func codeBoxComponent (props Props ) * Element {
1721 cba := & codeBoxAssistant {
18- code : As [string ](props , `code ` ),
22+ curCode : As [string ](props , `curCode ` ),
1923 setCode : AsFunc (props , `setCode` ),
2024 textAreaRef : UseRef (),
25+ lineNumsRef : UseRef (),
2126 }
2227
2328 return Div (Props {
@@ -29,11 +34,20 @@ func codeBoxComponent(props Props) *Element {
2934 `id` : `input` ,
3035 },
3136 CreateElement (`textarea` , Props {
37+ `id` : `line-nums` ,
38+ `ref` : cba .lineNumsRef ,
39+ `value` : cba .getLineNumbers (cba .curCode ),
40+ `readOnly` : true ,
41+ `disable` : `true` ,
42+ }),
43+ CreateElement (`textarea` , Props {
44+ `id` : `code` ,
3245 `ref` : cba .textAreaRef ,
33- `defaultValue ` : cba .code ,
46+ `value ` : cba .curCode ,
3447 `onInput` : cba .onInput ,
3548 `onKeyDown` : cba .onKeyDown ,
36- `id` : `code` ,
49+ `onScroll` : cba .onScroll ,
50+ `autoFocus` : true ,
3751 `autoCorrect` : `off` ,
3852 `autoComplete` : `off` ,
3953 `autoCapitalize` : `off` ,
@@ -44,13 +58,16 @@ func codeBoxComponent(props Props) *Element {
4458}
4559
4660type codeBoxAssistant struct {
47- code string
61+ curCode string
4862 setCode Func
4963 textAreaRef * Ref
64+ lineNumsRef * Ref
5065}
5166
5267func (cba * codeBoxAssistant ) onInput (e * js.Object ) {
53- cba .setCode (e .Get (`target` ).Get (`value` ).String ())
68+ code := e .Get (`target` ).Get (`value` ).String ()
69+ cba .setCode (code )
70+ //cba.updateLineNumbers(code)
5471}
5572
5673func (cba * codeBoxAssistant ) onKeyDown (e * js.Object ) {
@@ -59,6 +76,12 @@ func (cba *codeBoxAssistant) onKeyDown(e *js.Object) {
5976 }
6077}
6178
79+ func (cba * codeBoxAssistant ) onScroll (e * js.Object ) {
80+ scrollTop := e .Get ("target" ).Get ("scrollTop" ).Int ()
81+ println ("curScrollTop:" , scrollTop )
82+ cba .lineNumsRef .Set ("scrollTop" , scrollTop )
83+ }
84+
6285func (cba * codeBoxAssistant ) handleKeyDown (keyCode int ) bool {
6386 toInsert := ``
6487 switch keyCode {
@@ -71,9 +94,8 @@ func (cba *codeBoxAssistant) handleKeyDown(keyCode int) bool {
7194 return false
7295 }
7396
74- start := cba .textAreaRef .Get (`selectionStart` ).Int ()
75- end := cba .textAreaRef .Get (`selectionEnd` ).Int ()
76- code := cba .code
97+ start , end := cba .getSelection ()
98+ code := cba .curCode
7799
78100 if toInsert == "\n " {
79101 // Add auto-indent for new line.
@@ -92,8 +114,7 @@ func (cba *codeBoxAssistant) handleKeyDown(keyCode int) bool {
92114 newCaret := start + len (toInsert )
93115
94116 cba .setCode (code )
95- cba .textAreaRef .Set (`selectionStart` , newCaret )
96- cba .textAreaRef .Set (`selectionEnd` , newCaret )
117+ cba .setSelection (newCaret , code )
97118 return true
98119}
99120
@@ -109,3 +130,63 @@ func (cba *codeBoxAssistant) currentIndent(start int, code string) string {
109130 }
110131 return code [par :i ]
111132}
133+
134+ func (cba * codeBoxAssistant ) getLineNumbers (code string ) string {
135+ lines := strings .Count (code , "\n " ) + 1
136+ size := len (fmt .Sprintf ("%d" , lines ))
137+ var sb strings.Builder
138+ for i := 1 ; i <= lines ; i ++ {
139+ sb .WriteString (fmt .Sprintf ("%*d" , size , i ))
140+ if i < lines {
141+ sb .WriteString ("\n " )
142+ }
143+ }
144+ return sb .String ()
145+ }
146+
147+ func (cba * codeBoxAssistant ) getSelection () (int , int ) {
148+ start := cba .textAreaRef .Get (`selectionStart` ).Int ()
149+ end := cba .textAreaRef .Get (`selectionEnd` ).Int ()
150+ return start , end
151+ }
152+
153+ func (cba * codeBoxAssistant ) getLineHeight () int {
154+ style := js .Global .Get ("window" ).Call ("getComputedStyle" , cba .textAreaRef .Current ())
155+ // Get line-height property (returns string like "15px")
156+ lineHeightStr := style .Call ("getPropertyValue" , "line-height" ).String ()
157+ lineHeightStr = strings .TrimSuffix (lineHeightStr , "px" )
158+ lineHeight , err := strconv .Atoi (lineHeightStr )
159+ if err != nil {
160+ // Fallback to default if parsing fails (15px is about right for 11pt font)
161+ return 15
162+ }
163+ return lineHeight
164+ }
165+
166+ func (cba * codeBoxAssistant ) setSelection (caret int , code string ) {
167+ // Pre-update the textarea value so that the caret and scroll can be set
168+ // correctly before the next render so that the next render doesn't reset them.
169+ cba .textAreaRef .Set (`value` , code )
170+
171+ // Set selections
172+ cba .textAreaRef .Set (`selectionStart` , caret )
173+ cba .textAreaRef .Set (`selectionEnd` , caret )
174+
175+ // Auto-scroll to keep caret in view.
176+ curLineNum := strings .Count (code [:caret ], "\n " )
177+ lineHeight := cba .getLineHeight ()
178+ scrollTop := curLineNum * lineHeight
179+
180+ curTop := cba .textAreaRef .Get ("scrollTop" ).Int ()
181+ if scrollTop < curTop {
182+ cba .textAreaRef .Set ("scrollTop" , scrollTop )
183+ } else {
184+ height := cba .textAreaRef .Get ("clientHeight" ).Int ()
185+ scrollTop = scrollTop - height + lineHeight
186+
187+ println ("curLineNum:" , curLineNum , "lineHeight:" , lineHeight , "scrollTop:" , scrollTop , "curTop:" , curTop , "height:" , height )
188+ if scrollTop > curTop {
189+ cba .textAreaRef .Set ("scrollTop" , scrollTop )
190+ }
191+ }
192+ }
0 commit comments