Skip to content

Commit 4b46efb

Browse files
fixing url history
1 parent 9ebf079 commit 4b46efb

9 files changed

Lines changed: 454 additions & 255 deletions

File tree

playground/internal/page/banner.go

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@ package page
22

33
import (
44
"github.com/gopherjs/gopherjs.github.io/playground/internal/react"
5-
"github.com/gopherjs/gopherjs/compiler"
6-
"github.com/gopherjs/gopherjs/js"
75
)
86

9-
func Banner(runButtonRef react.Ref, urlHash string, onRun, onFormat, onShare, onSnippetSelected react.Func) *react.Element {
7+
func Banner(urlHash string, onRun, onFormat, onShare, onSnippetSelected react.Func) *react.Element {
108
return react.CreateElement(bannerComponent, react.Props{
11-
`runButtonRef`: runButtonRef,
129
`onRun`: onRun,
1310
`onFormat`: onFormat,
1411
`urlHash`: urlHash,
@@ -19,7 +16,6 @@ func Banner(runButtonRef react.Ref, urlHash string, onRun, onFormat, onShare, on
1916

2017
func bannerComponent(props react.Props) *react.Element {
2118
var (
22-
runButtonRef = props.GetRef(`runButtonRef`)
2319
onRun = props.GetFunc(`onRun`)
2420
onFormat = props.GetFunc(`onFormat`)
2521
urlHash = props.GetString(`urlHash`)
@@ -40,27 +36,15 @@ func bannerComponent(props react.Props) *react.Element {
4036
return react.Div(react.Props{
4137
`id`: `banner`,
4238
},
43-
BannerTitle(compiler.Version),
39+
BannerTitle(),
4440
react.Span(react.Props{
4541
`id`: `controls`,
4642
},
47-
react.Button(`run-button`, `Run`, react.Props{`ref`: runButtonRef}, onRun),
43+
react.Button(`run-button`, `Run`, nil, onRun),
4844
react.Button(`format-button`, `Format`, nil, onFormatClick),
4945
ToggleBox(`format-imports`, `Rewrite imports on Format`, `Imports`, fmtImports, setFmtImports),
5046
ShareUrlControl(urlHash, onShare, onSnippetSelected),
5147
ToggleBox(`color-theme`, `Change color-theme`, ``, lightTheme, setLightTheme),
5248
),
5349
)
5450
}
55-
56-
func getDefaultToLightTheme() bool {
57-
return js.Global.Get(`window`).Call(`matchMedia`, `(prefers-color-scheme: light)`).Get(`matches`).Bool()
58-
}
59-
60-
func setDataTheme(lightTheme bool) {
61-
theme := `dark`
62-
if lightTheme {
63-
theme = `light`
64-
}
65-
js.Global.Get(`document`).Get(`documentElement`).Call(`setAttribute`, `data-theme`, theme)
66-
}

playground/internal/page/bannerTitle.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package page
22

3-
import "github.com/gopherjs/gopherjs.github.io/playground/internal/react"
3+
import (
4+
"github.com/gopherjs/gopherjs/compiler"
45

5-
func BannerTitle(version string) *react.Element {
6-
return react.CreateElement(bannerTitleComponent, react.Props{
7-
`version`: version,
8-
})
6+
"github.com/gopherjs/gopherjs.github.io/playground/internal/react"
7+
)
8+
9+
func BannerTitle() *react.Element {
10+
return react.CreateElement(bannerTitleComponent, react.Props{})
911
}
1012

1113
func bannerTitleComponent(props react.Props) *react.Element {
12-
version := props.GetString(`version`)
14+
version := react.UseMemo(func() string {
15+
return compiler.Version
16+
}, []any{})
17+
1318
return react.Span(react.Props{
1419
`id`: `banner-title`,
1520
},

playground/internal/page/codeBox.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,33 @@ import (
1111
"github.com/gopherjs/gopherjs.github.io/playground/internal/react"
1212
)
1313

14-
func CodeBox(code string, setCode, onSave, onEscape react.Func) *react.Element {
14+
func CodeBox(code string, setCode, onUserChange, onSave, onEscape react.Func) *react.Element {
1515
return react.CreateElement(codeBoxComponent, react.Props{
16-
`curCode`: code,
17-
`setCode`: setCode,
18-
`onSave`: onSave,
19-
`onEscape`: onEscape,
16+
`curCode`: code,
17+
`setCode`: setCode,
18+
`onUserChange`: onUserChange,
19+
`onSave`: onSave,
20+
`onEscape`: onEscape,
2021
})
2122
}
2223

2324
func codeBoxComponent(props react.Props) *react.Element {
2425
var (
25-
curCode = props.GetString(`curCode`)
26-
setCode = props.GetFunc(`setCode`)
27-
onSave = props.GetFunc(`onSave`)
28-
onEscape = props.GetFunc(`onEscape`)
29-
textAreaRef = react.UseRef()
30-
lineNumsRef = react.UseRef()
26+
curCode = props.GetString(`curCode`)
27+
setCode = props.GetFunc(`setCode`)
28+
onUserChange = props.GetFunc(`onUserChange`)
29+
onSave = props.GetFunc(`onSave`)
30+
onEscape = props.GetFunc(`onEscape`)
31+
textAreaRef = react.UseRef()
32+
lineNumsRef = react.UseRef()
3133
)
3234

3335
onInput := react.UseCallback(func(e *js.Object) {
3436
newCode := e.Get(`target`).Get(`value`).String()
3537
sel := getSelection(textAreaRef)
3638
globals.UndoRedo().RecordCodeChange(sel, curCode, newCode)
3739
setCode.Invoke(newCode)
40+
onUserChange.Invoke()
3841
}, []any{curCode, setCode, textAreaRef})
3942

4043
onKeyDown := react.UseCallback(func(e *js.Object) {
@@ -51,6 +54,7 @@ func codeBoxComponent(props react.Props) *react.Element {
5154
if editor.ProcessKeyDown(cba, key, shift, ctrl) {
5255
e.Call(`preventDefault`)
5356
e.Call(`stopPropagation`)
57+
onUserChange.Invoke()
5458
}
5559
}, []any{curCode, setCode, onSave, onEscape, textAreaRef})
5660

playground/internal/page/globals.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package page
22

33
import (
4+
"fmt"
5+
"strings"
46
"sync"
57

68
"github.com/gopherjs/gopherjs.github.io/playground/internal/common"
9+
"github.com/gopherjs/gopherjs.github.io/playground/internal/react"
710
"github.com/gopherjs/gopherjs.github.io/playground/internal/runner"
811
"github.com/gopherjs/gopherjs.github.io/playground/internal/snippets"
912
"github.com/gopherjs/gopherjs.github.io/playground/internal/undoRedo"
13+
"github.com/gopherjs/gopherjs/js"
1014
)
1115

1216
// globals are the global objects used by the react components.
@@ -46,3 +50,59 @@ func OnceValue[T any](init func() T) func() T {
4650
return value
4751
}
4852
}
53+
54+
func getTopWindow() *js.Object { return js.Global.Get(`window`).Get(`top`) }
55+
func getLocation() *js.Object { return getTopWindow().Get(`location`) }
56+
57+
func getUrlWithoutHash() string {
58+
href := getLocation().Get(`href`).String()
59+
index := strings.Index(href, `#`)
60+
if index == -1 {
61+
return href
62+
}
63+
return href[:index]
64+
}
65+
66+
func getUrlHash() string {
67+
return getLocation().Get(`hash`).String()
68+
}
69+
70+
func setUrlHash(hash string) {
71+
if hash != `` && !strings.HasPrefix(hash, `#`) {
72+
panic(fmt.Errorf(`invalid hash to set. Must be empty or start with a "#": %q`, hash))
73+
}
74+
75+
if history := getTopWindow().Get(`history`); history != js.Undefined {
76+
if pushState := history.Get(`pushState`); pushState != js.Undefined {
77+
newUrl := getUrlWithoutHash()
78+
if hash != `` {
79+
newUrl += hash
80+
}
81+
history.Call(`pushState`, nil, ``, newUrl)
82+
return
83+
}
84+
}
85+
86+
// Fallback to setting location.hash directly
87+
getLocation().Set(`hash`, hash)
88+
}
89+
90+
func addEventListener(event string, handler react.Func) {
91+
getTopWindow().Call(`addEventListener`, event, handler)
92+
}
93+
94+
func removeEventListener(event string, handler react.Func) {
95+
getTopWindow().Call(`removeEventListener`, event, handler)
96+
}
97+
98+
func getDefaultToLightTheme() bool {
99+
return js.Global.Get(`window`).Call(`matchMedia`, `(prefers-color-scheme: light)`).Get(`matches`).Bool()
100+
}
101+
102+
func setDataTheme(lightTheme bool) {
103+
theme := `dark`
104+
if lightTheme {
105+
theme = `light`
106+
}
107+
js.Global.Get(`document`).Get(`documentElement`).Call(`setAttribute`, `data-theme`, theme)
108+
}

playground/internal/page/playground.go

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ func playgroundComponent(props react.Props) *react.Element {
1515
code, setCode = react.UseState(``)
1616
shareHash, setShareHash = react.UseState(``)
1717
output, setOutput = react.UseState([]any{})
18-
runButtonRef = react.UseRef()
18+
asyncOpInProgress = react.UseRefWith(false)
1919
)
2020

2121
// readSnippet reads a snippet or shared code from the store based on the given hash.
2222
readSnippet := react.UseCallback(func(hash string) {
23+
if asyncOpInProgress.Current() {
24+
return
25+
}
26+
27+
asyncOpInProgress.SetCurrent(true)
2328
globals.SnippetsStore().Read(hash, func(snippet string, err error) {
2429
o := Output(setOutput)
2530
o.Clear()
@@ -31,8 +36,16 @@ func playgroundComponent(props react.Props) *react.Element {
3136
}
3237
// even on error, set the code so the default code is shown.
3338
setCode.Invoke(snippet)
39+
asyncOpInProgress.SetCurrent(false)
3440
})
35-
}, []any{setCode, setShareHash, setOutput})
41+
}, []any{setCode, setShareHash, setOutput, asyncOpInProgress})
42+
43+
// This is emitted once on component mount to read the initial snippet
44+
// and set the initial code based on the url hash or set to the default code.
45+
react.UseEffect(func() {
46+
hash := getUrlHash()
47+
readSnippet.Invoke(hash)
48+
}, []any{})
3649

3750
// This callback is invoked when the top window's URL hash changes.
3851
// When the window's URL hash changed before the share hash state,
@@ -58,24 +71,31 @@ func playgroundComponent(props react.Props) *react.Element {
5871
setUrlHash(shareHash)
5972
}, []any{shareHash})
6073

61-
// This clears the share hash when the code changes.
62-
react.UseEffect(func() {
74+
// onUserChangedCode is called by the code box to clear the share hash
75+
// when the user changes the code.
76+
onUserChangedCode := react.UseCallback(func() {
6377
setShareHash.Invoke(``)
64-
}, []any{code})
78+
}, []any{setShareHash})
6579

6680
// onShare writes the code to the store when the share button is clicked.
6781
onShare := react.UseCallback(func() {
68-
globals.SnippetsStore().Write(code, func(url string, err error) {
82+
if asyncOpInProgress.Current() {
83+
return
84+
}
85+
86+
asyncOpInProgress.SetCurrent(true)
87+
globals.SnippetsStore().Write(code, func(hash string, err error) {
6988
output := Output(setOutput)
7089
output.Clear()
7190
if err != nil {
7291
output.AddError(err)
7392
setShareHash.Invoke(``)
7493
return
7594
}
76-
setShareHash.Invoke(url)
95+
setShareHash.Invoke(hash)
96+
asyncOpInProgress.SetCurrent(false)
7797
})
78-
}, []any{code, setOutput, setShareHash})
98+
}, []any{code, setOutput, setShareHash, asyncOpInProgress})
7999

80100
// onSnippetSelected performs a read when a predefined snippet is selected from the drop down.
81101
onSnippetSelected := react.UseCallback(func(selection string) {
@@ -89,10 +109,13 @@ func playgroundComponent(props react.Props) *react.Element {
89109
// like they would be able to with tab stops but our editor is consuming
90110
// tab key presses so tab stops won't work when in the text area.
91111
onEscapeCode := react.UseCallback(func() {
92-
runButtonRef.Current().Call(`focus`)
93-
}, []any{runButtonRef})
112+
runButton := js.Global.Get(`document`).Call(`querySelector`, `#run-button`)
113+
if runButton != js.Undefined && runButton != nil {
114+
runButton.Call(`focus`)
115+
}
116+
}, []any{})
94117

95-
onSaveKeyPress := react.UseCallback(func() {
118+
onSave := react.UseCallback(func() {
96119
println("Save key pressed") // TODO(grantnelson-wf): Implement by running format or just do nothing?
97120
}, []any{})
98121

@@ -105,26 +128,12 @@ func playgroundComponent(props react.Props) *react.Element {
105128
}, []any{})
106129

107130
return react.Fragment(
108-
Banner(runButtonRef, shareHash, onRun, onFormat, onShare, onSnippetSelected),
131+
Banner(shareHash, onRun, onFormat, onShare, onSnippetSelected),
109132
react.Div(react.Props{
110133
`id`: `code-output-box`,
111134
},
112-
CodeBox(code, setCode, onSaveKeyPress, onEscapeCode),
135+
CodeBox(code, setCode, onUserChangedCode, onSave, onEscapeCode),
113136
OutputBox(output),
114137
),
115138
)
116139
}
117-
118-
func getTopWindow() *js.Object { return js.Global.Get(`window`).Get(`top`) }
119-
func getLocation() *js.Object { return getTopWindow().Get(`location`) }
120-
121-
func getUrlHash() string { return getLocation().Get(`hash`).String() }
122-
func setUrlHash(hash string) { getLocation().Set(`hash`, hash) }
123-
124-
func addEventListener(event string, handler react.Func) {
125-
getTopWindow().Call(`addEventListener`, event, handler)
126-
}
127-
128-
func removeEventListener(event string, handler react.Func) {
129-
getTopWindow().Call(`removeEventListener`, event, handler)
130-
}

playground/internal/page/shareUrlControl.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func shareUrlComponent(props react.Props) *react.Element {
2626
)
2727

2828
react.UseEffect(func() {
29-
if len(urlHash) > 0 {
29+
if len(urlHash) > 0 && strings.HasPrefix(urlHash, `#/`) {
3030
shareUrlRef.Current().Call(`focus`)
3131
}
3232
}, []any{urlHash})
@@ -60,8 +60,7 @@ func shareUrlComponent(props react.Props) *react.Element {
6060
shownSharedUrl := ``
6161
if len(urlHash) > 0 {
6262
if strings.HasPrefix(urlHash, `#/`) {
63-
loc := getLocation()
64-
shownSharedUrl = loc.Get(`origin`).String() + loc.Get(`pathname`).String() + urlHash
63+
shownSharedUrl = getUrlWithoutHash() + urlHash
6564
shareUrlClass = `share-url-show`
6665
snippetsClass = `snippets-drop-down-hidden`
6766
} else if strings.HasPrefix(urlHash, `#`) {

0 commit comments

Comments
 (0)