Skip to content

Commit 5bcf7ae

Browse files
Updated css
1 parent ee8796e commit 5bcf7ae

File tree

6 files changed

+1271
-214
lines changed

6 files changed

+1271
-214
lines changed

playground/internal/react/bindings.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ func AsFunc(props Props, name string) Func {
8989
panic(ErrUndefinedPropKey)
9090
}
9191

92+
// AsSetter retrieves the property with the given name from the Props
93+
// and converts it to a setter function of type func(any).
94+
func AsSetter(props Props, name string) func(any) {
95+
fn := AsFunc(props, name)
96+
return func(v any) { fn(v) }
97+
}
98+
9299
// CreateElement creates a React element of the given type with the given props and children.
93100
//
94101
// The type can be a string for HTML elements (e.g. `div`, `span`, etc.),
@@ -143,17 +150,21 @@ func Button(value string, props Props, onClick func(e *js.Object)) *Element {
143150
}
144151

145152
// UseState creates a state for the current component render.
153+
// Returns the current state value and a setter function to update the state.
154+
// The setter accepts any value, which will be the new state at the next render,
155+
// or a function that takes the current state and returns the new state.
146156
//
147157
// It must be called unconditionally at the top level of the component function.
148158
// See: https://react.dev/reference/react/useState
149159
//
150-
// NOTE: Some types like `State[int]` must instead be `State[float64]`,
151-
// and instead of `State[[]*foo]` it must be `State[[]any]`,
152-
// because of how the underlying React hook works.
153-
func UseState[T any](initial T) (T, func(T)) {
160+
// NOTE: Some types like `State[int]` might be `State[float64]`,
161+
// and instead of `State[[]*foo]` it might be `State[[]any]`,
162+
// because of how the underlying React hook and GopherJS works.
163+
func UseState[T any](initial T) (T, func(any)) {
154164
r := react().Call(`useState`, initial)
155165
current := r.Index(0).Interface().(T)
156-
setter := func(v T) { r.Index(1).Invoke(v) }
166+
setFn := r.Index(1)
167+
setter := func(v any) { setFn.Invoke(v) }
157168
return current, setter
158169
}
159170

playground/internal/react/codeBox.go

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package react
22

33
import (
4-
"fmt"
4+
"strconv"
55
"strings"
66

77
"github.com/gopherjs/gopherjs/js"
88
)
99

10+
const tabWidth = 4
11+
1012
// CodeBox creates a code editor box React element for editing
1113
// the given code state.
12-
func CodeBox(code string, setCode func(string)) *Element {
14+
func CodeBox(code string, setCode func(any)) *Element {
1315
return CreateElement(codeBoxComponent, Props{
1416
`curCode`: code,
1517
`setCode`: setCode,
@@ -19,53 +21,46 @@ func CodeBox(code string, setCode func(string)) *Element {
1921
func codeBoxComponent(props Props) *Element {
2022
cba := &codeBoxAssistant{
2123
curCode: As[string](props, `curCode`),
22-
setCode: AsFunc(props, `setCode`),
24+
setCode: AsSetter(props, `setCode`),
2325
textAreaRef: UseRef(),
2426
lineNumsRef: UseRef(),
2527
}
2628

2729
return Div(Props{
28-
`className`: `box`,
29-
`id`: `content`,
30+
`id`: `code-box`,
3031
},
31-
Div(Props{
32-
`className`: `box yellow`,
33-
`id`: `input`,
34-
},
35-
CreateElement(`textarea`, Props{
36-
`id`: `line-nums`,
37-
`ref`: cba.lineNumsRef,
38-
`value`: cba.getLineNumbers(),
39-
`readOnly`: true,
40-
`disable`: `true`,
41-
}),
42-
CreateElement(`textarea`, Props{
43-
`id`: `code`,
44-
`ref`: cba.textAreaRef,
45-
`value`: cba.curCode,
46-
`onInput`: cba.onInput,
47-
`onKeyDown`: cba.onKeyDown,
48-
`onScroll`: cba.onScroll,
49-
`autoFocus`: true,
50-
`autoCorrect`: `off`,
51-
`autoComplete`: `off`,
52-
`autoCapitalize`: `off`,
53-
`spellCheck`: false,
54-
}),
55-
),
32+
CreateElement(`textarea`, Props{
33+
`id`: `line-nums`,
34+
`ref`: cba.lineNumsRef,
35+
`value`: cba.getLineNumbers(),
36+
`readOnly`: true,
37+
`disable`: `true`,
38+
}),
39+
CreateElement(`textarea`, Props{
40+
`id`: `code`,
41+
`ref`: cba.textAreaRef,
42+
`value`: cba.curCode,
43+
`onInput`: cba.onInput,
44+
`onKeyDown`: cba.onKeyDown,
45+
`onScroll`: cba.onScroll,
46+
`autoFocus`: true,
47+
`autoCorrect`: `off`,
48+
`autoComplete`: `off`,
49+
`autoCapitalize`: `off`,
50+
`spellCheck`: false,
51+
}),
5652
)
5753
}
5854

5955
type codeBoxAssistant struct {
6056
curCode string
61-
setCode Func
57+
setCode func(any)
6258
textAreaRef *Ref
6359
lineNumsRef *Ref
6460
}
6561

6662
func (cba *codeBoxAssistant) onInput(e *js.Object) {
67-
code := e.Get(`target`).Get(`value`).String()
68-
cba.setCode(code)
63+
cba.setCode(e.Get(`target`).Get(`value`).String())
6964
}
7065

7166
func (cba *codeBoxAssistant) onKeyDown(e *js.Object) {
@@ -117,6 +112,11 @@ func (cba *codeBoxAssistant) handleKeyDown(keyCode int) bool {
117112
}
118113

119114
func (cba *codeBoxAssistant) currentIndent(start int, code string) string {
115+
if start < 0 || start > len(code) {
116+
return ``
117+
}
118+
119+
// get prior line's indent
120120
par := strings.LastIndex(code[:start], "\n") + 1
121121
i := par
122122
for i < start {
@@ -126,15 +126,21 @@ func (cba *codeBoxAssistant) currentIndent(start int, code string) string {
126126
}
127127
i++
128128
}
129-
return code[par:i]
129+
indent := code[par:i]
130+
131+
// adjust indent based on prior line's last character
132+
switch code[start] {
133+
case '{', '(', '[':
134+
indent += "\t"
135+
}
136+
return indent
130137
}
131138

132139
func (cba *codeBoxAssistant) getLineNumbers() string {
133140
lines := strings.Count(cba.curCode, "\n") + 1
134-
size := len(fmt.Sprintf(`%d`, lines))
135141
var sb strings.Builder
136142
for i := 1; i <= lines; i++ {
137-
sb.WriteString(fmt.Sprintf(`%*d`, size, i))
143+
sb.WriteString(strconv.Itoa(i))
138144
if i < lines {
139145
sb.WriteString("\n")
140146
}
@@ -194,23 +200,22 @@ func (cba *codeBoxAssistant) horizontallyAutoScroll(caret int, code string) {
194200
scrollLeft := int(float64(curLine) * float64(totalWidth) / float64(longestLine))
195201

196202
curLeft := cba.textAreaRef.Get(`scrollLeft`).Int()
197-
198-
println(fmt.Sprintf("HScroll: total=%d visible=%d longest=%d curLine=%d scrollLeft=%d curLeft=%d",
199-
totalWidth, visibleWidth, longestLine, curLine, scrollLeft, curLeft))
200-
201203
if scrollLeft < curLeft {
202204
cba.textAreaRef.Set(`scrollLeft`, scrollLeft)
203205
} else if scrollLeft -= visibleWidth; scrollLeft > curLeft {
204206
cba.textAreaRef.Set(`scrollLeft`, scrollLeft)
205207
}
206208
}
207209

210+
// measureLineLength returns the length of the line.
211+
// This counts tabs as multiple spaces so that the horizontal scrolling works correctly.
208212
func measureLineLength(line string) int {
209-
const tabWidth = 4
210213
tabAdjust := strings.Count(line, "\t") * (tabWidth - 1)
211214
return len(line) + tabAdjust
212215
}
213216

217+
// longestLineLength returns the length of the longest line in the given code.
218+
// This counts tabs as multiple spaces so that the horizontal scrolling works correctly.
214219
func longestLineLength(code string) int {
215220
longest := 0
216221
for {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package react
2+
3+
import (
4+
"fmt"
5+
"go/scanner"
6+
)
7+
8+
func ClearOutput(outputUpdater func(any)) {
9+
outputUpdater(func(items []any) []any {
10+
return []any{}
11+
})
12+
}
13+
14+
func WriteError(outputUpdater func(any), err error) {
15+
outputUpdater(func(items []any) []any {
16+
if list, ok := err.(scanner.ErrorList); ok {
17+
for _, entry := range list {
18+
items = append(items, map[string]any{`type`: `err`, `context`: entry.Error()})
19+
}
20+
return items
21+
}
22+
return append(items, map[string]any{`type`: `err`, `context`: err.Error()})
23+
})
24+
}
25+
26+
func WriteString(outputUpdater func(any), s string) {
27+
outputUpdater(func(items []any) []any {
28+
if maxItem := len(items) - 1; maxItem >= 0 {
29+
lastItem := items[maxItem].(map[string]any)
30+
if lastItem[`type`] == `text` {
31+
lastItem[`context`] = lastItem[`context`].(string) + s
32+
return items
33+
}
34+
}
35+
return append(items, map[string]any{`type`: `text`, `context`: s})
36+
})
37+
}
38+
39+
// OutputBox creates a box React element for displaying output strings and errors.
40+
func OutputBox(output []any) *Element {
41+
return CreateElement(outputBoxComponent, Props{
42+
`output`: output,
43+
})
44+
}
45+
46+
func outputBoxComponent(props Props) *Element {
47+
output := As[[]any](props, `output`)
48+
49+
children := make([]Node, 0, len(output))
50+
for _, item := range output {
51+
itemMap := item.(map[string]any)
52+
53+
classType := `output-text`
54+
if itemMap[`type`] == `err` {
55+
classType = `output-err`
56+
}
57+
id := fmt.Sprintf(`output-item-%d`, len(children))
58+
59+
child := CreateElement(`pre`, Props{
60+
`className`: classType,
61+
`id`: id,
62+
}, itemMap[`context`])
63+
children = append(children, child)
64+
}
65+
66+
return Div(Props{
67+
`id`: `output-box`,
68+
}, children...)
69+
}

playground/internal/react/playground.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ var initText = func() string {
1717

1818
func playgroundComponent() *Element {
1919
code, setCode := UseState(initText)
20+
output, setOutput := UseState([]any{})
2021

21-
//UseEffect(func() {
22-
// println(fmt.Sprintf(`Code: %q`, code))
23-
//}, code)
22+
UseEffect(func() { // TODO(grantnelson-wf): REMOVE
23+
if len(code)%10 == 0 {
24+
WriteError(setOutput, fmt.Errorf("code changed: %d", len(code)))
25+
} else {
26+
WriteString(setOutput, fmt.Sprintf("Code changed: %d\n", len(code)))
27+
}
28+
}, code)
2429

2530
return Div(Props{
2631
`id`: `playground`,
2732
},
2833
CodeBox(code, setCode),
34+
OutputBox(output),
2935
)
3036
}

0 commit comments

Comments
 (0)