Skip to content

Commit b4220c4

Browse files
added snippet drop down
1 parent f888ea9 commit b4220c4

8 files changed

Lines changed: 451 additions & 195 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package page
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/gopherjs/gopherjs/js"
7+
8+
"github.com/gopherjs/gopherjs.github.io/playground/internal/react"
9+
)
10+
11+
func DropDown(id, className string, items []any, selected any, onSelect react.Setter) *react.Element {
12+
return react.CreateElement(dropDownComponent, react.Props{
13+
`id`: id,
14+
`className`: className,
15+
`items`: items,
16+
`selected`: selected,
17+
`onSelect`: onSelect,
18+
})
19+
}
20+
21+
func dropDownComponent(props react.Props) *react.Element {
22+
var (
23+
id = props.GetString(`id`)
24+
className = props.GetString(`className`)
25+
items = props.Get(`items`).([]any)
26+
selected = props.Get(`selected`)
27+
onSelect = props.GetFunc(`onSelect`)
28+
)
29+
30+
onChange := react.UseCallback(func(e *js.Object) {
31+
onSelect(e.Get(`target`).Get(`value`).String())
32+
}, []any{onSelect})
33+
34+
options := make([]react.Node, len(items))
35+
for i, item := range items {
36+
options[i] = react.CreateElement(`option`, react.Props{
37+
`key`: fmt.Sprintf("%s-%v", id, item),
38+
`value`: item,
39+
}, item)
40+
}
41+
42+
return react.CreateElement(`select`, react.Props{
43+
`id`: id,
44+
`value`: selected,
45+
`className`: className,
46+
`onChange`: onChange,
47+
}, options...)
48+
}

playground/internal/page/playground.go

Lines changed: 52 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,53 +14,21 @@ func Playground() *react.Element {
1414
func playgroundComponent(props react.Props) *react.Element {
1515
var (
1616
code, setCode = react.UseState(``)
17-
shareUrl, setShareUrl = react.UseState(``)
17+
urlHash, setUrlHash = react.UseState(``)
1818
output, setOutput = react.UseState([]any{})
1919
fmtImports, setFmtImports = react.UseState(true)
2020
lightTheme, setLightTheme = react.UseStateLazy(getDefaultToLightTheme)
2121
runButtonRef = react.UseRef()
22-
isLoadingSnippet = react.UseRef()
23-
settingUrlHash = react.UseRef()
2422
)
2523

2624
react.UseEffect(func() {
2725
setDataTheme(lightTheme)
2826
}, []any{lightTheme})
2927

30-
react.UseEffect(func() {
31-
if isLoadingSnippet.Current().Bool() {
32-
isLoadingSnippet.SetCurrent(false)
33-
return
34-
}
35-
36-
// code changed so clear share URL
37-
settingUrlHash.SetCurrent(true)
38-
setShareUrl.Invoke(``)
39-
getLocation().Set(`hash`, ``)
40-
}, []any{code})
41-
28+
// Add an effect to listen for the window's URL hash changing.
4229
react.UseEffectWithCleanup(func() func() {
4330
hashChanged := func() {
44-
// if the hash is being set programmatically, ignore this event
45-
if settingUrlHash.Current().Bool() {
46-
settingUrlHash.SetCurrent(false)
47-
return
48-
}
49-
50-
hash := getLocation().Get(`hash`).String()
51-
isLoadingSnippet.SetCurrent(true)
52-
globals.SnippetsStore().Read(hash, func(snippet string, err error) {
53-
o := Output(setOutput)
54-
o.Clear()
55-
if err != nil {
56-
setShareUrl.Invoke(``)
57-
o.AddError(err)
58-
} else {
59-
setShareUrl.Invoke(hash)
60-
}
61-
// even on error, set the code so the default code is shown.
62-
setCode.Invoke(snippet)
63-
})
31+
setUrlHash.Invoke(getLocation().Get(`hash`).String())
6432
}
6533
getTopWindow().Call(`addEventListener`, `hashchange`, hashChanged)
6634
hashChanged() // Load initial hash
@@ -69,12 +37,55 @@ func playgroundComponent(props react.Props) *react.Element {
6937
}
7038
}, []any{})
7139

40+
// Add an effect to update the window's URL hash when the URL hash state has changed.
41+
react.UseEffect(func() {
42+
getLocation().Set(`hash`, urlHash)
43+
}, []any{urlHash})
44+
45+
// Add an effect to load a snippet or shared code when the URL hash changes.
46+
react.UseEffect(func() {
47+
globals.SnippetsStore().Read(urlHash, func(snippet string, err error) {
48+
o := Output(setOutput)
49+
o.Clear()
50+
if err != nil {
51+
o.AddError(err)
52+
}
53+
// even on error, set the code so the default code is shown.
54+
setCode.Invoke(snippet)
55+
})
56+
}, []any{urlHash})
57+
58+
// Add an effect to clear the URL hash when the code changes.
59+
react.UseEffect(func() {
60+
setUrlHash.Invoke(``)
61+
}, []any{code})
62+
63+
// Create a callback for when the share button is clicked.
64+
onShareClick := react.UseCallback(func() {
65+
globals.SnippetsStore().Write(code, func(url string, err error) {
66+
output := Output(setOutput)
67+
output.Clear()
68+
if err != nil {
69+
output.AddError(err)
70+
setUrlHash.Invoke(``)
71+
return
72+
}
73+
setUrlHash.Invoke(url)
74+
})
75+
}, []any{code, setOutput, setUrlHash})
76+
77+
// Create a callback for when a predefined snippet is selected from the drop down.
78+
onSnippetSelected := react.UseCallback(func(selection string) {
79+
setUrlHash.Invoke(`#` + selection)
80+
}, []any{setUrlHash})
81+
82+
// Create a callback for when the escape key is pressed in the code box.
83+
// Escape will move focus to the run button.
84+
// This is for eccessability so that users can easily run the
85+
// code after editing. Or at minimum, users can exit the text area
86+
// like they would be able to with tab stops but our editor is consuming
87+
// tab key presses so tab stops won't work when in the text area.
7288
onEscapeCode := react.UseCallback(func() {
73-
// Escape will move focus to the run button.
74-
// This is for eccessability so that users can easily run the
75-
// code after editing. Or at minimum, users can exit the text area
76-
// like they would be able to with tab stops but our editor is consuming
77-
// tab key presses so tab stops won't work when in the text area.
7889
runButtonRef.Current().Call(`focus`)
7990
}, []any{runButtonRef})
8091

@@ -90,22 +101,6 @@ func playgroundComponent(props react.Props) *react.Element {
90101
println("Format clicked", fmtImports) // TODO(grantnelson-wf): Implement
91102
}, []any{fmtImports})
92103

93-
onShareClick := react.UseCallback(func() {
94-
globals.SnippetsStore().Write(code, func(url string, err error) {
95-
settingUrlHash.SetCurrent(true)
96-
output := Output(setOutput)
97-
output.Clear()
98-
if err != nil {
99-
setShareUrl.Invoke(``)
100-
getLocation().Set(`hash`, ``)
101-
output.AddError(err)
102-
return
103-
}
104-
setShareUrl.Invoke(url)
105-
getLocation().Set(`hash`, url)
106-
})
107-
}, []any{code, setOutput, setShareUrl})
108-
109104
return react.Fragment(
110105
react.Div(react.Props{
111106
`id`: `banner`,
@@ -117,8 +112,7 @@ func playgroundComponent(props react.Props) *react.Element {
117112
react.Button(`run-button`, `Run`, react.Props{`ref`: runButtonRef}, onRunClick),
118113
react.Button(`format-button`, `Format`, nil, onFormatClick),
119114
ToggleBox(`format-imports`, `Rewrite imports on Format`, `Imports`, fmtImports, setFmtImports),
120-
ShareUrlControl(shareUrl, onShareClick),
121-
// TODO(grantnelson-wf): Add a Snippet selection control / dropdown for loading predefined snippets.
115+
ShareUrlControl(urlHash, onShareClick, onSnippetSelected),
122116
ToggleBox(`color-theme`, `Change color-theme`, ``, lightTheme, setLightTheme),
123117
),
124118
),
Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
11
package page
22

33
import (
4+
"strings"
5+
46
"github.com/gopherjs/gopherjs/js"
57

68
"github.com/gopherjs/gopherjs.github.io/playground/internal/react"
9+
"github.com/gopherjs/gopherjs.github.io/playground/internal/snippets"
710
)
811

9-
func ShareUrlControl(shareUrl string, onShare react.Setter) *react.Element {
12+
func ShareUrlControl(urlHash string, onShare, onSnippetSelected react.Setter) *react.Element {
1013
return react.CreateElement(shareUrlComponent, react.Props{
11-
`shareUrl`: shareUrl,
12-
`onShare`: onShare,
14+
`urlHash`: urlHash,
15+
`onShare`: onShare,
16+
`onSnippetSelected`: onSnippetSelected,
1317
})
1418
}
1519

1620
func shareUrlComponent(props react.Props) *react.Element {
1721
var (
18-
shareUrl = props.GetString(`shareUrl`)
19-
onShare = props.GetFunc(`onShare`)
20-
shareUrlRef = react.UseRef()
22+
urlHash = props.GetString(`urlHash`)
23+
onShare = props.GetFunc(`onShare`)
24+
onSnippetSelected = props.GetFunc(`onSnippetSelected`)
25+
shareUrlRef = react.UseRef()
2126
)
2227

2328
react.UseEffect(func() {
24-
if len(shareUrl) > 0 {
29+
if len(urlHash) > 0 {
2530
shareUrlRef.Current().Call(`focus`)
2631
}
27-
}, []any{shareUrl})
32+
}, []any{urlHash})
2833

2934
onShareClick := react.UseCallback(func(e *js.Object) {
3035
onShare() // call without the dom target
@@ -34,21 +39,46 @@ func shareUrlComponent(props react.Props) *react.Element {
3439
e.Get(`target`).Call(`select`)
3540
}, []any{})
3641

37-
className := `share-url-hidden`
38-
if len(shareUrl) > 0 {
39-
className = `share-url-show`
42+
snippetItems := react.UseMemo(func() []any {
43+
names := snippets.SnippetNames()
44+
// because of the type system in JS used by react, convert []string to []any
45+
items := make([]any, len(names))
46+
for i, name := range names {
47+
items[i] = name
48+
}
49+
return items
50+
}, []any{})
51+
52+
onSelectSnippet := react.UseCallback(func(e *js.Object) {
53+
onSnippetSelected(e.String())
54+
}, []any{onSnippetSelected})
55+
56+
// based on the URL Hash, determine which UI elements to show.
57+
shareUrlClass := `share-url-hidden`
58+
snippetsClass := `snippets-drop-down-show`
59+
selSnippet := snippets.DefaultName
60+
shownSharedUrl := ``
61+
if len(urlHash) > 0 {
62+
if strings.HasPrefix(urlHash, `#/`) {
63+
loc := getLocation()
64+
shownSharedUrl = loc.Get(`origin`).String() + loc.Get(`pathname`).String() + urlHash
65+
shareUrlClass = `share-url-show`
66+
snippetsClass = `snippets-drop-down-hidden`
67+
} else if strings.HasPrefix(urlHash, `#`) {
68+
selSnippet = urlHash[1:]
69+
}
4070
}
4171

4272
return react.Fragment(
4373
react.Button(`share-button`, `Share`, nil, onShareClick),
4474
react.CreateElement(`input`, react.Props{
4575
`id`: `share-url`,
4676
`type`: `text`,
47-
`className`: className,
77+
`className`: shareUrlClass,
4878
`ref`: shareUrlRef,
49-
`value`: shareUrl,
79+
`value`: shownSharedUrl,
5080
`readOnly`: true,
5181
`onFocus`: onShareUrlFocus,
5282
}),
53-
)
83+
DropDown(`snippets-drop-down`, snippetsClass, snippetItems, selSnippet, onSelectSnippet))
5484
}

playground/internal/react/hooks.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ func UseRef() *Ref {
6868
return &Ref{holder: react().Call(`useRef`, nil)}
6969
}
7070

71+
// UseRefWith is similar to UseRef except initializes the `current` property
72+
// to the given initial value.
73+
func UseRefWith(initial any) *Ref {
74+
return &Ref{holder: react().Call(`useRef`, initial)}
75+
}
76+
7177
func (r *Ref) Current() *js.Object {
7278
if r != nil && r.holder != nil {
7379
return r.holder.Get(`current`)

playground/internal/snippets/snippets.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package snippets
22

3-
import _ "embed"
3+
import (
4+
_ "embed"
5+
"sort"
6+
)
47

58
// DefaultCode is the default code that is shown in the playground
69
// when no other snippet is loaded.
710
//
811
//go:embed default.go.txt
912
var DefaultCode string
1013

14+
// DefaultName is the name of the default snippet.
15+
const DefaultName = `Hello`
16+
1117
//go:embed workGroup.go.txt
1218
var workGroupCode string
1319

@@ -16,20 +22,31 @@ var workGroupCode string
1622
//
1723
// TODO(grantnelson-wf): Add more pre-defined snippets.
1824
var predefined = map[string]string{
19-
`Hello`: DefaultCode,
20-
`Work Group`: workGroupCode,
25+
`Clear`: ``,
26+
`Hello`: DefaultCode,
27+
`WorkGroup`: workGroupCode,
2128
}
2229

2330
// SnippetNames returns the names of all predefined snippets.
2431
// The names do not include the leading '#' character.
2532
func SnippetNames() []string {
33+
// TODO(grantnelson-wf): Update with maps.Keys when on go1.23.0
2634
names := make([]string, 0, len(predefined))
2735
for name := range predefined {
2836
names = append(names, name)
2937
}
38+
sort.Strings(names)
3039
return names
3140
}
3241

42+
// GetSnippet returns the predefined snippet code for the given name.
43+
// The name should not include the leading '#' character.
44+
// If no predefined snippet exists for the given name, false is returned.
45+
func GetSnippet(name string) (string, bool) {
46+
snippet, ok := predefined[name]
47+
return snippet, ok
48+
}
49+
3350
// getSnippetName returns the name of the predefined snippet that
3451
// matches the given code.
3552
// The returned name does not include the leading '#' character.

playground/playground.css

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ html, body {
150150
gap: var(--gap-banner-controls);
151151
}
152152

153-
#controls input[type=button] {
153+
#controls input[type=button],
154+
#controls input[type=text],
155+
#controls select {
154156
padding: 5px 8px 5px 8px;
155157
border-radius: 5px;
156158
font-family: var(--font-family-controls);
@@ -268,6 +270,20 @@ html, body {
268270
display: none;
269271
}
270272

273+
#snippets-drop-down {
274+
margin: 0px 5px 0px 5px;
275+
flex-grow: 3;
276+
font-family: var(--font-family-controls);
277+
font-size: var(--font-size-controls);
278+
border: 1px solid var(--color-share-url-boarder);
279+
background: var(--color-share-url-background);
280+
color: var(--color-share-url-text);
281+
}
282+
283+
.snippets-drop-down-hidden {
284+
display: none;
285+
}
286+
271287
/* The panel that contains everything below the banner */
272288
#code-output-box{
273289
display: flex;

0 commit comments

Comments
 (0)