├── .gitignore
├── LICENSE
├── README.md
├── actions
└── actions.go
├── index.html
├── index.jsgo.html
├── main.go
├── main_test.go
├── models
├── modals.go
└── share.go
├── stores
├── app.go
├── archive.go
├── builderjs
│ └── builderjs.go
├── compile.go
├── connection.go
├── deploy.go
├── editor.go
├── empty.go
├── history.go
├── local.go
├── page.go
├── request.go
├── scanner.go
├── share.go
└── source.go
├── testing
├── a
│ └── a.go
├── b
│ └── b.go
└── main
│ └── main.go
└── views
├── add-file.go
├── add-package.go
├── build-tags.go
├── clash-warning.go
├── delete-file.go
├── deploy-done.go
├── editor.go
├── help.go
├── load-package.go
├── menu.go
├── modal.go
├── page.go
└── remove-package.go
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .DS_Store
3 | .idea/
4 | coverage.out
5 | .coverage/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 David Brophy
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 2020 Update
4 |
5 | * The compile.jsgo.io and play.jsgo.io services have been shut down.
6 | * Anything deployed to jsgo.io or pkg.jsgo.io will continue to work fine.
7 |
8 | I created the jsgo.io system several years ago, and it costs about $150/month to host which I pay
9 | personally. I'm tightening up my finances right now, so this outgoing had to stop.
10 |
11 | If anyone would like to host it for me (it runs on a single GKE `n1-standard-2` instance), please
12 | let me know and we can get it back online!
13 |
14 | I had a plan for a big rewrite that would make is possible to run on App Engine, thus reduce the
15 | cost to almost zero. Unfortunately this is something I'm hesitant to start, because it seems that
16 | Go on the client is moving away from GopherJS and towards WASM.
17 |
18 | # play.jsgo.io
19 |
20 | Edit and run Go in the browser, supporting arbitrary import paths!
21 |
22 | https://play.jsgo.io/
23 |
24 | [ ](https://play.jsgo.io/)
25 |
26 | The jsgo playground is an extension of the jsgo compiler. The compiler allows you to easily compile Go
27 | to JS using GopherJS, and automatically host the results in an aggressively cached CDN. The playground
28 | adds an online editor and many other features (see below).
29 |
30 | The unique feature of the jsgo playground is that it supports arbitrary import paths. Other Go playgrounds
31 | are limited to just the Go standard library.
32 |
33 | For more for more info:
34 |
35 | * jsgo compiler: https://github.com/dave/jsgo
36 | * jsgo playground: https://github.com/dave/play
37 |
38 | ## Demos
39 |
40 | Here's the simplest demo - it just writes to the console and to the page:
41 |
42 | * https://play.jsgo.io/github.com/dave/jstest
43 |
44 | Here's a couple of simple demos that accept files by drag and drop. The first compresses dropped files to
45 | a zip. The second compresses images to jpg. They use the Go standard library zip / image libraries, which
46 | work flawlessly in the browser:
47 |
48 | * https://play.jsgo.io/github.com/dave/zip
49 | * https://play.jsgo.io/github.com/dave/img
50 |
51 | The amazing ebiten 2D games library is a perfect example of the power of Go in the browser. Here's some
52 | demos:
53 |
54 | * https://play.jsgo.io/github.com/hajimehoshi/ebiten/examples/2048
55 | * https://play.jsgo.io/github.com/hajimehoshi/go-inovation
56 | * https://play.jsgo.io/github.com/hajimehoshi/ebiten/examples/flappy
57 |
58 | ## Contact
59 |
60 | If you'd like to chat more about the project, feel free to [add an issue](https://github.com/dave/play/issues),
61 | mention [@dave](https://github.com/dave/) or post in the #gopherjs channel of the Gophers Slack. I'm
62 | happy to help!
63 |
64 | ## Features
65 |
66 | #### Initialise
67 | The URL can be used to initialise with code in several ways:
68 |
69 | * Load a Go package with `/{{ Package path }}`
70 | * Load a Github Gist with `/gist.github.com/{{ Gist ID }}`
71 | * Load a shared project with `/{{ Share ID }}`
72 | * Load a `play.golang.org` share with `/p/{{ Go playground ID }}`
73 |
74 |
75 |
76 | #### Run
77 | Click the `Run` button to run your code in the right-hand panel. If the imports have been changed recently,
78 | the dependencies will be refreshed before running.
79 |
80 |
81 |
82 |
83 |
84 | #### Format code
85 | Use the `Format code` option to run `gofmt` on your code. This is executed automatically when the `Run`,
86 | `Update`, `Share` or `Deploy` features are used.
87 |
88 |
89 |
90 |
91 |
92 | #### Update
93 | If you update a dependency, use the `Update` option, which does the equivalent of `go get -u` and refreshes
94 | the changes in any import or dependency.
95 |
96 |
97 |
98 |
99 |
100 | #### Share
101 | To share your project with others, use the `Share` option. Your project will be persisted to a json file
102 | on `src.jsgo.io` and the page will update to a sharable URL.
103 |
104 |
105 |
106 |
107 |
108 | #### Deploy
109 | To deploy your code to [jsgo.io](https://jsgo.io), use the `Deploy` feature. A modal will be displayed with the
110 | link to the page on `jsgo.io`, and the Loader JS on `pkg.jsgo.io`.
111 |
112 | Use the `jsgo.io` link for testing and toy projects. Remember you're sharing the `jsgo.io` domain with
113 | everyone else, so the browser environment should be considered toxic.
114 |
115 | The Loader JS on `pkg.jsgo.io` can be used in production, and should be added to a script tag on your
116 | own website. See [github.com/dave/jsgo](https://github.com/dave/jsgo) for more information.
117 |
118 |
119 |
120 |
121 |
122 | #### Console
123 | Writes to `os.Stdout` are redirected to a playground console, which can be toggled using the `Show console`
124 | option. The console will automatically appear the first time it's written to.
125 |
126 |
127 |
128 |
129 |
130 | #### Minify
131 | In normal usage, all JS is minified. For debugging, this can be toggled with the `Minify JS` option.
132 |
133 |
134 |
135 |
136 |
137 | #### Build tags
138 | The build tags used when compiling can be edited with the `Build tags...` option. The selected build
139 | tags are persisted when using the `Share` feature.
140 |
141 |
142 |
143 |
144 |
145 | #### Download
146 | The `Download` option downloads the project. Single file projects are downloaded as a single file, while
147 | multi-file projects download as a zip.
148 |
149 |
150 |
151 |
152 |
153 | #### Upload
154 | Files can be uploaded to the project simply by drag+drop. Zip files generated by the `Download` feature
155 | can be uploaded to restore a multi-file project.
156 |
157 |
158 |
159 |
160 |
161 | #### File menu
162 | Change the selected file with the file menu.
163 |
164 |
165 |
166 |
167 |
168 | #### Add file
169 | Add a file to the current package with the `Add file` option. Only `.go`, `.md` and `.inc.js` files are
170 | supported. If no extension is supplied, `.go` is added.
171 |
172 |
173 |
174 |
175 |
176 | #### Delete file
177 | Delete a file from the current package with the `Delete file` option.
178 |
179 |
180 |
181 |
182 |
183 | #### Package menu
184 | Change the selected package with the package menu.
185 |
186 |
187 |
188 |
189 |
190 | #### Add package
191 | Add an empty package with the `Add package` option.
192 |
193 |
194 |
195 |
196 |
197 | #### Load package
198 | The source for an import or dependency can be loaded with the `Load package` option. By default, only
199 | the direct imports of your project are listed. Use the `Show all dependencies` option to show the entire
200 | dependency tree.
201 |
202 |
203 |
204 |
205 |
206 | #### Remove package
207 | A package can be removed with the `Remove package` option.
208 |
209 | ## Run locally?
210 |
211 | If you'd like to run `play.jsgo.io` locally, take a look at [these instructions](https://github.com/dave/jsgo/blob/master/LOCAL.md).
--------------------------------------------------------------------------------
/actions/actions.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "github.com/dave/dropper"
5 | "github.com/dave/flux"
6 | "github.com/dave/play/models"
7 | "github.com/dave/services"
8 | )
9 |
10 | type Load struct{}
11 |
12 | type ConsoleFirstWrite struct{}
13 | type ConsoleToggleClick struct{}
14 | type MinifyToggleClick struct{}
15 |
16 | type ShowAllDepsChange struct{ State bool }
17 |
18 | type ChangeSplit struct{ Sizes []float64 }
19 | type ChangeFile struct {
20 | Path string
21 | Name string
22 | }
23 |
24 | type LoadSource struct {
25 | Source map[string]map[string]string
26 | Tags []string
27 | CurrentPackage string
28 | CurrentFile string
29 | Save bool // Save directly after loading? false during initialising, true for load package.
30 | Update bool // Update directly after loading?
31 | }
32 |
33 | type UserChangedSplit struct{ Sizes []float64 }
34 | type UserChangedText struct {
35 | Text string
36 | Changed bool
37 | }
38 | type UserChangedFile struct{ Name string }
39 | type UserChangedPackage struct{ Path string }
40 |
41 | type ModalOpen struct{ Modal models.Modal }
42 | type ModalClose struct{ Modal models.Modal }
43 |
44 | type DownloadClick struct{}
45 | type BuildTags struct{ Tags []string }
46 |
47 | type AddFile struct{ Name string }
48 | type AddPackage struct{ Path string }
49 | type DeleteFile struct{ Name string }
50 | type RemovePackage struct{ Path string }
51 |
52 | type FormatCode struct{ Then flux.ActionInterface }
53 |
54 | // CompileStart compiles the app and injects the js into the iframe
55 | type CompileStart struct{}
56 |
57 | type DragEnter struct{}
58 | type DragLeave struct{}
59 | type DragDrop struct {
60 | Files []dropper.File
61 | Changed map[string]map[string]bool
62 | }
63 |
64 | type Send struct{ Message services.Message }
65 | type Dial struct {
66 | Url string
67 | Open func() flux.ActionInterface
68 | Message func(interface{}) flux.ActionInterface
69 | Close func() flux.ActionInterface
70 | }
71 |
72 | type ShareStart struct{}
73 | type ShareOpen struct{}
74 | type ShareMessage struct{ Message interface{} }
75 | type ShareClose struct{}
76 |
77 | type DeployStart struct{}
78 | type DeployOpen struct{}
79 | type DeployMessage struct{ Message interface{} }
80 | type DeployClose struct{}
81 |
82 | type RequestStart struct {
83 | Type models.RequestType
84 | Path string // Path to get (for GetRequest and InitialiseRequest)
85 | Run bool // Run after update? (for UpdateRequest)
86 | }
87 | type RequestOpen struct {
88 | *RequestStart
89 | }
90 | type RequestMessage struct {
91 | *RequestStart
92 | Message interface{}
93 | }
94 | type RequestClose struct {
95 | *RequestStart
96 | }
97 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/index.jsgo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/dave/play/actions"
5 | "github.com/dave/play/stores"
6 | "github.com/dave/play/views"
7 | "github.com/gopherjs/vecty"
8 | "github.com/vincent-petithory/dataurl"
9 | "honnef.co/go/js/dom"
10 | )
11 |
12 | var document = dom.GetWindow().Document().(dom.HTMLDocument)
13 |
14 | func main() {
15 | if document.ReadyState() == "loading" {
16 | document.AddEventListener("DOMContentLoaded", false, func(dom.Event) {
17 | go run()
18 | })
19 | } else {
20 | go run()
21 | }
22 | }
23 |
24 | func run() {
25 |
26 | vecty.AddStylesheet(dataurl.New([]byte(views.Styles), "text/css").String())
27 |
28 | app := &stores.App{}
29 | app.Init()
30 | p := views.NewPage(app)
31 |
32 | app.Watch(nil, func(done chan struct{}) {
33 | defer close(done)
34 | vecty.Rerender(p)
35 | })
36 |
37 | app.Dispatch(&actions.Load{})
38 |
39 | vecty.RenderBody(p)
40 | }
41 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "testing"
4 |
5 | func TestPlay(t *testing.T) {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/models/modals.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type Modal string
4 |
5 | const (
6 | ClashWarningModal Modal = "clash-warning-modal"
7 | AddPackageModal Modal = "add-package-modal"
8 | AddFileModal Modal = "add-file-modal"
9 | DeleteFileModal Modal = "delete-file-modal"
10 | DeployDoneModal Modal = "deploy-done-modal"
11 | LoadPackageModal Modal = "load-package-modal"
12 | RemovePackageModal Modal = "remove-package-modal"
13 | BuildTagsModal Modal = "build-tags-modal"
14 | HelpModal Modal = "help-modal"
15 | )
16 |
17 | type RequestType string
18 |
19 | const (
20 | GetRequest RequestType = "get"
21 | UpdateRequest RequestType = "update"
22 | InitialiseRequest RequestType = "initialise"
23 | )
24 |
--------------------------------------------------------------------------------
/models/share.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | // SharePack is the structure of the data persisted on src.jsgo.io as json, so best to use json tags
4 | // to lower-case the names.
5 | type SharePack struct {
6 | Version int `json:"version"`
7 | Source map[string]map[string]string `json:"source"` // Source packages for this build: map[]map[]
8 | Tags []string `json:"tags"` // Build tags
9 | }
10 |
--------------------------------------------------------------------------------
/stores/app.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 |
7 | "honnef.co/go/js/dom"
8 |
9 | "strings"
10 |
11 | "time"
12 |
13 | "github.com/dave/flux"
14 | "github.com/gopherjs/gopherjs/js"
15 | )
16 |
17 | type App struct {
18 | Dispatcher flux.DispatcherInterface
19 | Watcher flux.WatcherInterface
20 | Notifier flux.NotifierInterface
21 |
22 | Archive *ArchiveStore
23 | Editor *EditorStore
24 | Connection *ConnectionStore
25 | Local *LocalStore
26 | Scanner *ScannerStore
27 | Compile *CompileStore
28 | Share *ShareStore
29 | Request *RequestStore
30 | Deploy *DeployStore
31 | Page *PageStore
32 | Source *SourceStore
33 | History *HistoryStore
34 | }
35 |
36 | func (a *App) Init() {
37 |
38 | n := flux.NewNotifier()
39 | a.Notifier = n
40 | a.Watcher = n
41 |
42 | a.Archive = NewArchiveStore(a)
43 | a.Editor = NewEditorStore(a)
44 | a.Connection = NewConnectionStore(a)
45 | a.Local = NewLocalStore(a)
46 | a.Scanner = NewScannerStore(a)
47 | a.Compile = NewCompileStore(a)
48 | a.Share = NewShareStore(a)
49 | a.Request = NewRequestStore(a)
50 | a.Deploy = NewDeployStore(a)
51 | a.Page = NewPageStore(a)
52 | a.Source = NewSourceStore(a)
53 | a.History = NewHistoryStore(a)
54 |
55 | a.Dispatcher = flux.NewDispatcher(
56 | // Notifier:
57 | a.Notifier,
58 | // Stores:
59 | a.Archive,
60 | a.Editor,
61 | a.Connection,
62 | a.Local,
63 | a.Scanner,
64 | a.Compile,
65 | a.Share,
66 | a.Request,
67 | a.Deploy,
68 | a.Page,
69 | a.Source,
70 | a.History,
71 | )
72 | }
73 |
74 | func (a *App) Dispatch(action flux.ActionInterface) chan struct{} {
75 | return a.Dispatcher.Dispatch(action)
76 | }
77 |
78 | func (a *App) Watch(key interface{}, f func(done chan struct{})) {
79 | a.Watcher.Watch(key, f)
80 | }
81 |
82 | func (a *App) Delete(key interface{}) {
83 | a.Watcher.Delete(key)
84 | }
85 |
86 | func (a *App) Fail(err error) {
87 | // TODO: improve this
88 | js.Global.Call("alert", err.Error())
89 | }
90 |
91 | func (a *App) Debug(message ...interface{}) {
92 | js.Global.Get("console").Call("log", message...)
93 | }
94 |
95 | var lastLog *struct{}
96 |
97 | // LogHide hides the message after 2 seconds
98 | func (a *App) LogHide(args ...interface{}) {
99 | a.Log(args...)
100 | if len(args) > 0 {
101 | // clear message after 2 sec if not changed
102 | before := lastLog
103 | go func() {
104 | <-time.After(time.Second * 2)
105 | if before == lastLog {
106 | a.Log()
107 | }
108 | }()
109 | }
110 | }
111 |
112 | func (a *App) Log(args ...interface{}) {
113 | m := dom.GetWindow().Document().GetElementByID("message")
114 | var message string
115 | if len(args) > 0 {
116 | message = strings.TrimSuffix(fmt.Sprintln(args...), "\n")
117 | }
118 | if m.InnerHTML() != message {
119 | if message != "" {
120 | js.Global.Get("console").Call("log", "Status", strconv.Quote(message))
121 | }
122 | requestAnimationFrame()
123 | m.SetInnerHTML(message)
124 | requestAnimationFrame()
125 | lastLog = &struct{}{}
126 | }
127 | }
128 |
129 | func (a *App) Logf(format string, args ...interface{}) {
130 | a.Log(fmt.Sprintf(format, args...))
131 | }
132 |
133 | func (a *App) LogHidef(format string, args ...interface{}) {
134 | a.LogHide(fmt.Sprintf(format, args...))
135 | }
136 |
137 | func requestAnimationFrame() {
138 | c := make(chan struct{})
139 | js.Global.Call("requestAnimationFrame", func() { close(c) })
140 | <-c
141 | }
142 |
--------------------------------------------------------------------------------
/stores/archive.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "go/types"
7 |
8 | "encoding/gob"
9 |
10 | "errors"
11 |
12 | "net/http"
13 |
14 | "sync"
15 |
16 | "io/ioutil"
17 |
18 | "github.com/dave/flux"
19 | "github.com/dave/jsgo/config"
20 | "github.com/dave/play/actions"
21 | "github.com/dave/play/models"
22 | "github.com/dave/play/stores/builderjs"
23 | "github.com/dave/services/deployer/deployermsg"
24 | "github.com/gopherjs/gopherjs/compiler"
25 | )
26 |
27 | type ArchiveStore struct {
28 | app *App
29 |
30 | // cache (path -> item) of archives
31 | cache map[string]CacheItem
32 |
33 | // index (path -> item) of the previously received update
34 | index deployermsg.ArchiveIndex
35 |
36 | wait sync.WaitGroup
37 | }
38 |
39 | type CacheItem struct {
40 | Hash string
41 | Archive *compiler.Archive // This archive is stripped of JS
42 | Js []byte
43 | }
44 |
45 | func NewArchiveStore(app *App) *ArchiveStore {
46 | s := &ArchiveStore{
47 | app: app,
48 | cache: map[string]CacheItem{},
49 | }
50 | return s
51 | }
52 |
53 | type Dep struct {
54 | Path string
55 | Js []byte
56 | }
57 |
58 | func (s *ArchiveStore) Compile(path string, tags []string) ([]Dep, error) {
59 | done := make(map[string]bool)
60 | archives := map[string]*compiler.Archive{}
61 | packages := map[string]*types.Package{}
62 | jsdeps := []Dep{
63 | // Always start with the prelude
64 | {Path: "prelude", Js: s.cache["prelude"].Js},
65 | }
66 | var deps []*compiler.Archive
67 | var compile func(path string) error
68 | compile = func(path string) error {
69 | if done[path] {
70 | return nil
71 | }
72 | if s.app.Source.HasPackage(path) {
73 | for _, imp := range s.app.Scanner.Imports(path) {
74 | if err := compile(imp); err != nil {
75 | return err
76 | }
77 | }
78 | archive, err := builderjs.BuildPackage(
79 | path,
80 | s.app.Source.Source(),
81 | tags,
82 | deps,
83 | s.app.Page.Minify(),
84 | archives,
85 | packages,
86 | )
87 | if err != nil {
88 | return err
89 | }
90 | js, _, err := builderjs.GetPackageCode(context.Background(), archive, false, true)
91 | if err != nil {
92 | return err
93 | }
94 | jsdeps = append(jsdeps, Dep{path, js})
95 | deps = append(deps, archive)
96 | done[path] = true
97 | return nil
98 | }
99 | item, ok := s.cache[path]
100 | if !ok {
101 | return fmt.Errorf("%s not found", path)
102 | }
103 | for _, imp := range item.Archive.Imports {
104 | if err := compile(imp); err != nil {
105 | return err
106 | }
107 | }
108 | jsdeps = append(jsdeps, Dep{path, item.Js})
109 | deps = append(deps, item.Archive)
110 | done[path] = true
111 | return nil
112 | }
113 | if err := compile("runtime"); err != nil {
114 | return nil, err
115 | }
116 | if err := compile(path); err != nil {
117 | return nil, err
118 | }
119 | return jsdeps, nil
120 | }
121 |
122 | func (s *ArchiveStore) AllFresh() bool {
123 | for path := range s.app.Scanner.MainPackages() {
124 | if !s.Fresh(path) {
125 | return false
126 | }
127 | }
128 | return true
129 | }
130 |
131 | // Fresh is true if current cache matches the previously downloaded archives
132 | func (s *ArchiveStore) Fresh(mainPath string) bool {
133 | // if index is nil, either the page has just loaded or we're in the middle of an update
134 | if s.index == nil {
135 | return false
136 | }
137 |
138 | // first check that all indexed packages are in the cache at the right versions. This would fail
139 | // if there was an error while downloading one of the archive files.
140 | for path, item := range s.index {
141 | cached, ok := s.cache[path]
142 | if !ok {
143 | return false
144 | }
145 | if cached.Hash != item.Hash {
146 | return false
147 | }
148 | }
149 |
150 | // then check that all the imports in all packages are found in the index, or in the source
151 | for _, path := range s.app.Scanner.Imports(mainPath) {
152 | _, inIndex := s.index[path]
153 | _, inSource := s.app.Source.Source()[path]
154 | if !inIndex && !inSource {
155 | return false
156 | }
157 | }
158 |
159 | return true
160 | }
161 |
162 | func (s *ArchiveStore) Cache() map[string]CacheItem {
163 | return s.cache
164 | }
165 |
166 | func (s *ArchiveStore) CacheStrings() map[string]string {
167 | hashes := map[string]string{}
168 | for path, item := range s.app.Archive.Cache() {
169 | hashes[path] = item.Hash
170 | }
171 | return hashes
172 | }
173 |
174 | func (s *ArchiveStore) Handle(payload *flux.Payload) bool {
175 | switch a := payload.Action.(type) {
176 | case *actions.MinifyToggleClick:
177 | payload.Wait(s.app.Page)
178 | s.index = nil
179 | s.app.Dispatch(&actions.RequestStart{Type: models.UpdateRequest, Run: false})
180 | case *actions.LoadSource:
181 | payload.Wait(s.app.Scanner)
182 | if a.Update && !s.AllFresh() {
183 | s.app.Dispatch(&actions.RequestStart{Type: models.UpdateRequest, Run: false})
184 | }
185 | case *actions.RequestMessage:
186 | switch message := a.Message.(type) {
187 | case deployermsg.Archive:
188 | s.wait.Add(1)
189 | go func() {
190 | defer s.wait.Done()
191 | c := CacheItem{
192 | Hash: message.Hash,
193 | }
194 | var getwait sync.WaitGroup
195 | getwait.Add(2)
196 | go func() {
197 | defer getwait.Done()
198 | if message.Path == "prelude" {
199 | // prelude doesn't have an archive file
200 | return
201 | }
202 | resp, err := http.Get(fmt.Sprintf("%s://%s/%s.%s.ax", config.Protocol[config.Pkg], config.Host[config.Pkg], message.Path, message.Hash))
203 | if err != nil {
204 | s.app.Fail(err)
205 | return
206 | }
207 | var a compiler.Archive
208 | if err := gob.NewDecoder(resp.Body).Decode(&a); err != nil {
209 | s.app.Fail(err)
210 | return
211 | }
212 | c.Archive = &a
213 | }()
214 | go func() {
215 | defer getwait.Done()
216 | resp, err := http.Get(fmt.Sprintf("%s://%s/%s.%s.js", config.Protocol[config.Pkg], config.Host[config.Pkg], message.Path, message.Hash))
217 | if err != nil {
218 | s.app.Fail(err)
219 | return
220 | }
221 | js, err := ioutil.ReadAll(resp.Body)
222 | if err != nil {
223 | s.app.Fail(err)
224 | return
225 | }
226 | c.Js = js
227 | }()
228 | getwait.Wait()
229 | s.cache[message.Path] = c
230 | if message.Path == "prelude" {
231 | // prelude doesn't have an archive file
232 | s.app.Log("prelude")
233 | } else {
234 | s.app.Log(c.Archive.Name)
235 | }
236 | }()
237 | return true
238 | case deployermsg.ArchiveIndex:
239 | s.index = message
240 | }
241 | case *actions.RequestClose:
242 |
243 | if a.Type == models.GetRequest {
244 | // get request doesn't do an update - just gets files
245 | return true
246 | }
247 |
248 | s.wait.Wait()
249 |
250 | if !s.AllFresh() {
251 | s.app.Fail(errors.New("websocket closed but archives not updated"))
252 | return true
253 | }
254 |
255 | if a.Run {
256 | s.app.Dispatch(&actions.CompileStart{})
257 | } else {
258 | var downloaded, unchanged int
259 | for _, v := range s.index {
260 | if v.Unchanged {
261 | unchanged++
262 | } else {
263 | downloaded++
264 | }
265 | }
266 | if downloaded == 0 && unchanged == 0 {
267 | s.app.Log()
268 | } else if downloaded > 0 && unchanged > 0 {
269 | s.app.LogHidef("%d downloaded, %d unchanged", downloaded, unchanged)
270 | } else if downloaded > 0 {
271 | s.app.LogHidef("%d downloaded", downloaded)
272 | } else if unchanged > 0 {
273 | s.app.LogHidef("%d unchanged", unchanged)
274 | }
275 | }
276 | payload.Notify()
277 | }
278 |
279 | return true
280 | }
281 |
--------------------------------------------------------------------------------
/stores/builderjs/builderjs.go:
--------------------------------------------------------------------------------
1 | package builderjs
2 |
3 | import (
4 | "context"
5 | "go/token"
6 | "go/types"
7 | "sort"
8 |
9 | "go/ast"
10 | "go/parser"
11 |
12 | "fmt"
13 |
14 | "strings"
15 |
16 | "bytes"
17 | "crypto/sha1"
18 |
19 | "github.com/dave/services/includer"
20 | "github.com/gopherjs/gopherjs/compiler"
21 | "golang.org/x/tools/go/gcexportdata"
22 | )
23 |
24 | func BuildPackage(path string, source map[string]map[string]string, tags []string, deps []*compiler.Archive, minify bool, archives map[string]*compiler.Archive, packages map[string]*types.Package) (*compiler.Archive, error) {
25 |
26 | for _, a := range deps {
27 | if archives[a.ImportPath] == nil {
28 | archives[a.ImportPath] = a
29 | }
30 | if packages[a.ImportPath] == nil {
31 | p, err := gcexportdata.Read(bytes.NewReader(a.ExportData), token.NewFileSet(), packages, a.ImportPath)
32 | if err != nil {
33 | return nil, err
34 | }
35 | packages[a.ImportPath] = p
36 | }
37 | }
38 |
39 | fset := token.NewFileSet()
40 |
41 | var importContext *compiler.ImportContext
42 | importContext = &compiler.ImportContext{
43 | Packages: packages,
44 | Import: func(imp string) (*compiler.Archive, error) {
45 | a, ok := archives[imp]
46 | if ok {
47 | return a, nil
48 | }
49 | sourceFiles, ok := source[imp]
50 | if ok {
51 | // We have the source for this dep
52 | archive, err := compileFiles(fset, imp, tags, sourceFiles, importContext, minify)
53 | if err != nil {
54 | return nil, err
55 | }
56 | return archive, nil
57 | }
58 | return nil, fmt.Errorf("%s not found", imp)
59 | },
60 | }
61 |
62 | archive, err := importContext.Import(path)
63 | if err != nil {
64 | return nil, err
65 | }
66 |
67 | return archive, nil
68 | }
69 |
70 | func compileFiles(fset *token.FileSet, path string, tags []string, sourceFiles map[string]string, importContext *compiler.ImportContext, minify bool) (*compiler.Archive, error) {
71 | var files []*ast.File
72 | inc := includer.New(sourceFiles, tags)
73 | for name, contents := range sourceFiles {
74 | include, err := inc.Include(name)
75 | if err != nil {
76 | return nil, err
77 | }
78 | if !include {
79 | continue
80 | }
81 | f, err := parser.ParseFile(fset, name, contents, parser.ParseComments)
82 | if err != nil {
83 | return nil, err
84 | }
85 | files = append(files, f)
86 | }
87 |
88 | if len(files) == 0 {
89 | return nil, fmt.Errorf("no buildable Go source files in %s", path)
90 | }
91 |
92 | // TODO: Remove this when https://github.com/gopherjs/gopherjs/pull/742 is merged
93 | // Files must be in the same order to get reproducible JS
94 | sort.Slice(files, func(i, j int) bool {
95 | return fset.File(files[i].Pos()).Name() > fset.File(files[j].Pos()).Name()
96 | })
97 |
98 | archive, err := compiler.Compile(path, files, fset, importContext, minify)
99 | if err != nil {
100 | return nil, err
101 | }
102 |
103 | for name, contents := range sourceFiles {
104 | if !strings.HasSuffix(name, ".inc.js") {
105 | continue
106 | }
107 | archive.IncJSCode = append(archive.IncJSCode, []byte("\t(function() {\n")...)
108 | archive.IncJSCode = append(archive.IncJSCode, []byte(contents)...)
109 | archive.IncJSCode = append(archive.IncJSCode, []byte("\n\t}).call($global);\n")...)
110 | }
111 |
112 | return archive, nil
113 | }
114 |
115 | func GetPackageCode(ctx context.Context, archive *compiler.Archive, minify, initializer bool) (contents []byte, hash []byte, err error) {
116 | dceSelection := make(map[*compiler.Decl]struct{})
117 | for _, d := range archive.Declarations {
118 | dceSelection[d] = struct{}{}
119 | }
120 | buf := new(bytes.Buffer)
121 |
122 | if initializer {
123 | var s string
124 | if minify {
125 | s = `$load["%s"]=function(){`
126 | } else {
127 | s = `$load["%s"] = function () {` + "\n"
128 | }
129 | if _, err := fmt.Fprintf(buf, s, archive.ImportPath); err != nil {
130 | return nil, nil, err
131 | }
132 | }
133 |
134 | if err := compiler.WritePkgCode(archive, dceSelection, minify, &compiler.SourceMapFilter{Writer: buf}); err != nil {
135 | return nil, nil, err
136 | }
137 |
138 | if minify {
139 | // compiler.WritePkgCode always finishes with a "\n". In minified mode we should remove this.
140 | buf.Truncate(buf.Len() - 1)
141 | }
142 |
143 | if initializer {
144 | /*
145 | var s string
146 | if minify {
147 | s = "};$done();"
148 | } else {
149 | s = "};\n$done();"
150 | }
151 | if _, err := fmt.Fprint(buf, s); err != nil {
152 | return nil, nil, err
153 | }
154 | */
155 | if _, err := fmt.Fprint(buf, "};"); err != nil {
156 | return nil, nil, err
157 | }
158 | }
159 |
160 | sha := sha1.New()
161 | if _, err := sha.Write(buf.Bytes()); err != nil {
162 | return nil, nil, err
163 | }
164 | return buf.Bytes(), sha.Sum(nil), nil
165 | }
166 |
--------------------------------------------------------------------------------
/stores/compile.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "errors"
5 |
6 | "bytes"
7 | "text/template"
8 |
9 | "strconv"
10 |
11 | "fmt"
12 |
13 | "github.com/dave/flux"
14 | "github.com/dave/play/actions"
15 | "github.com/dave/play/models"
16 | "github.com/gopherjs/gopherjs/js"
17 | "honnef.co/go/js/dom"
18 | )
19 |
20 | func NewCompileStore(app *App) *CompileStore {
21 | s := &CompileStore{
22 | app: app,
23 | }
24 | return s
25 | }
26 |
27 | type CompileStore struct {
28 | app *App
29 | compiling bool
30 | compiled bool
31 | consoleWritten bool
32 | tags []string
33 | }
34 |
35 | func (s *CompileStore) Tags() []string {
36 | return s.tags
37 | }
38 |
39 | func (s *CompileStore) Compiling() bool {
40 | return s.compiling
41 | }
42 |
43 | func (s *CompileStore) Compiled() bool {
44 | return s.compiled
45 | }
46 |
47 | func (s *CompileStore) Handle(payload *flux.Payload) bool {
48 | switch a := payload.Action.(type) {
49 | case *actions.LoadSource:
50 | s.tags = append(s.tags, a.Tags...)
51 | payload.Notify()
52 | case *actions.CompileStart:
53 | if err := s.compile(); err != nil {
54 | s.app.Fail(err)
55 | return true
56 | }
57 | payload.Notify()
58 | case *actions.BuildTags:
59 | s.tags = a.Tags
60 | payload.Notify()
61 | }
62 | return true
63 | }
64 |
65 | func (s *CompileStore) compile() error {
66 | path, count := s.app.Scanner.Main()
67 | if path == "" {
68 | if count == 0 {
69 | return errors.New("project has no main package")
70 | } else {
71 | return fmt.Errorf("project has %d main packages - select one and retry", count)
72 | }
73 | }
74 |
75 | if !s.app.Archive.Fresh(path) {
76 | s.app.Dispatch(
77 | &actions.RequestStart{Type: models.UpdateRequest, Run: true},
78 | )
79 | return nil
80 | }
81 |
82 | s.compiling = true
83 | defer func() {
84 | s.compiling = false
85 | }()
86 |
87 | s.app.Log("compiling")
88 |
89 | deps, err := s.app.Archive.Compile(path, s.Tags())
90 | if err != nil {
91 | return err
92 | }
93 |
94 | s.app.Log("running")
95 |
96 | doc := dom.GetWindow().Document()
97 | holder := doc.GetElementByID("iframe-holder")
98 | for _, v := range holder.ChildNodes() {
99 | v.Underlying().Call("remove")
100 | }
101 | frame := doc.CreateElement("iframe").(*dom.HTMLIFrameElement)
102 | frame.SetID("iframe")
103 | frame.Style().Set("width", "100%")
104 | frame.Style().Set("height", "100%")
105 | frame.Style().Set("border", "0")
106 |
107 | // We need to wait for the iframe to load before adding contents or Firefox will clear the iframe
108 | // after momentarily flashing up the contents.
109 | c := make(chan struct{})
110 | listener := frame.AddEventListener("load", false, func(event dom.Event) {
111 | close(c)
112 | })
113 |
114 | holder.AppendChild(frame)
115 | <-c
116 |
117 | // remove the listener so if it's triggered again we don't close the closed channel
118 | frame.RemoveEventListener("load", false, listener)
119 |
120 | console := dom.GetWindow().Document().GetElementByID("console")
121 | console.SetInnerHTML("")
122 | frame.Get("contentWindow").Set("goPrintToConsole", js.InternalObject(func(b []byte) {
123 | console.SetInnerHTML(console.InnerHTML() + string(b))
124 | if !s.consoleWritten {
125 | s.consoleWritten = true
126 | s.app.Dispatch(&actions.ConsoleFirstWrite{})
127 | }
128 | }))
129 |
130 | frameDoc := frame.ContentDocument()
131 |
132 | if index, ok := s.app.Source.Files(path)["index.jsgo.html"]; ok {
133 | // has index
134 |
135 | indexTemplate, err := template.New("index").Parse(index)
136 | if err != nil {
137 | return err
138 | }
139 | data := struct{ Script string }{Script: ""}
140 | buf := &bytes.Buffer{}
141 | if err := indexTemplate.Execute(buf, data); err != nil {
142 | return err
143 | }
144 |
145 | frameDoc.Underlying().Call("open")
146 | frameDoc.Underlying().Call("write", buf.String())
147 | frameDoc.Underlying().Call("close")
148 | }
149 |
150 | head := frameDoc.GetElementsByTagName("head")[0].(*dom.BasicHTMLElement)
151 |
152 | loaderJs := ""
153 | for _, dep := range deps {
154 | loaderJs += "$load[" + strconv.Quote(dep.Path) + "]();\n"
155 | }
156 | scriptLoad := frameDoc.CreateElement("script")
157 | scriptLoad.SetID("loader")
158 | scriptLoad.SetInnerHTML(`
159 | var $load = {};
160 | var $count = 0;
161 | var $total = ` + fmt.Sprint(len(deps)) + `;
162 | var $finished = function() {
163 | ` + loaderJs + `
164 | $mainPkg = $packages[` + strconv.Quote(path) + `];
165 | $synthesizeMethods();
166 | $packages["runtime"].$init();
167 | $go($mainPkg.$init, []);
168 | $flushConsole();
169 | };
170 | var $done = function() {
171 | $count++;
172 | if ($count == $total) {
173 | $finished();
174 | }
175 | };
176 | `)
177 | head.AppendChild(scriptLoad)
178 |
179 | for _, dep := range deps {
180 | scriptDep := frameDoc.CreateElement("script")
181 | scriptDep.SetID(dep.Path)
182 | scriptDep.SetInnerHTML(string(dep.Js) + "$done();")
183 | //scriptDep.AppendChild(doc.CreateTextNode(string(dep.Js) + "$done();"))
184 | head.AppendChild(scriptDep)
185 | }
186 |
187 | s.compiled = true
188 | s.app.Log()
189 | return nil
190 | }
191 |
--------------------------------------------------------------------------------
/stores/connection.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "errors"
5 |
6 | "honnef.co/go/js/dom"
7 |
8 | "fmt"
9 |
10 | "strings"
11 |
12 | "github.com/dave/flux"
13 | "github.com/dave/jsgo/server/play/messages"
14 | "github.com/dave/jsgo/server/servermsg"
15 | "github.com/dave/play/actions"
16 | "github.com/gopherjs/gopherjs/js"
17 | "github.com/gopherjs/websocket/websocketjs"
18 | )
19 |
20 | type ConnectionStore struct {
21 | app *App
22 |
23 | open bool
24 | ws *websocketjs.WebSocket
25 | }
26 |
27 | func NewConnectionStore(app *App) *ConnectionStore {
28 | s := &ConnectionStore{
29 | app: app,
30 | }
31 | return s
32 | }
33 |
34 | func (s *ConnectionStore) Open() bool {
35 | return s.open
36 | }
37 |
38 | func (s *ConnectionStore) Handle(payload *flux.Payload) bool {
39 | switch action := payload.Action.(type) {
40 | case *actions.Send:
41 | s.app.Debug(fmt.Sprintf("Sending %T", action.Message), action.Message)
42 | if !s.open {
43 | s.app.Fail(errors.New("connection closed"))
44 | return true
45 | }
46 | b, _, err := messages.Marshal(action.Message)
47 | if err != nil {
48 | s.app.Fail(err)
49 | return true
50 | }
51 | if err := s.ws.Send(string(b)); err != nil {
52 | s.app.Fail(err)
53 | return true
54 | }
55 | case *actions.Dial:
56 | if s.open {
57 | s.app.Fail(errors.New("connection already open"))
58 | return true
59 | }
60 | s.app.Debug("Web socket dialing", action.Url)
61 | var err error
62 | if s.ws, err = websocketjs.New(action.Url); err != nil {
63 | s.app.Fail(err)
64 | return true
65 | }
66 | s.open = true
67 | s.ws.AddEventListener("open", false, func(ev *js.Object) {
68 | go func() {
69 | s.app.Debug("Web socket open")
70 | s.app.Dispatch(action.Open())
71 | }()
72 | })
73 | s.ws.AddEventListener("message", false, func(ev *js.Object) {
74 | go func() {
75 | m, err := messages.Unmarshal([]byte(ev.Get("data").String()))
76 | if err != nil {
77 | s.app.Fail(err)
78 | return
79 | }
80 | s.app.Debug(fmt.Sprintf("Received %T", m), m)
81 | if e, ok := m.(servermsg.Error); ok {
82 | s.app.Fail(errors.New(e.Message))
83 | return
84 | }
85 | s.app.Dispatch(action.Message(m))
86 | }()
87 | })
88 | s.ws.AddEventListener("close", false, func(ev *js.Object) {
89 | go func() {
90 | s.app.Debug("Web socket closed")
91 | s.app.Dispatch(action.Close())
92 | s.ws.Close()
93 | s.open = false
94 | }()
95 | })
96 | s.ws.AddEventListener("error", false, func(ev *js.Object) {
97 | go func() {
98 | s.app.Debug("Web socket error")
99 | s.app.Fail(errors.New("error from server"))
100 | s.ws.Close()
101 | s.open = false
102 | }()
103 | })
104 | }
105 | return true
106 | }
107 |
108 | func defaultUrl() string {
109 | var url string
110 | if strings.HasPrefix(dom.GetWindow().Document().DocumentURI(), "https://") {
111 | url = "wss://compile.jsgo.io/_play/"
112 | } else {
113 | url = "ws://localhost:8081/_play/"
114 | }
115 | return url
116 | }
117 |
--------------------------------------------------------------------------------
/stores/deploy.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/dave/flux"
8 | "github.com/dave/jsgo/config"
9 | "github.com/dave/jsgo/server/play/messages"
10 | "github.com/dave/play/actions"
11 | "github.com/dave/play/models"
12 | "github.com/dave/services/builder/buildermsg"
13 | "github.com/dave/services/constor/constormsg"
14 | "github.com/dave/services/getter/gettermsg"
15 | )
16 |
17 | func NewDeployStore(app *App) *DeployStore {
18 | s := &DeployStore{
19 | app: app,
20 | }
21 | return s
22 | }
23 |
24 | type DeployStore struct {
25 | app *App
26 | mainHash, indexHash, mainPath string
27 | }
28 |
29 | func (s *DeployStore) LoaderJs() string {
30 | return fmt.Sprintf("%s://%s/%s.%s.js", config.Protocol[config.Pkg], config.Host[config.Pkg], s.mainPath, s.mainHash)
31 | }
32 |
33 | func (s *DeployStore) Index() string {
34 | return fmt.Sprintf("%s://%s/%s", config.Protocol[config.Index], config.Host[config.Index], s.indexHash)
35 | }
36 |
37 | func (s *DeployStore) Handle(payload *flux.Payload) bool {
38 | switch action := payload.Action.(type) {
39 | case *actions.DeployStart:
40 | path, count := s.app.Scanner.Main()
41 | if path == "" {
42 | if count == 0 {
43 | s.app.Fail(errors.New("project has no main package"))
44 | return true
45 | } else {
46 | s.app.Fail(fmt.Errorf("project has %d main packages - select one and retry", count))
47 | return true
48 | }
49 | }
50 | s.app.Log("deploying")
51 | s.mainHash = ""
52 | s.indexHash = ""
53 | s.mainPath = path
54 | s.app.Dispatch(&actions.Dial{
55 | Url: defaultUrl(),
56 | Open: func() flux.ActionInterface { return &actions.DeployOpen{} },
57 | Message: func(m interface{}) flux.ActionInterface { return &actions.DeployMessage{Message: m} },
58 | Close: func() flux.ActionInterface { return &actions.DeployClose{} },
59 | })
60 | payload.Notify()
61 | case *actions.DeployOpen:
62 | message := messages.Deploy{
63 | Main: s.mainPath,
64 | Imports: s.app.Scanner.Imports(s.mainPath),
65 | Source: s.app.Source.Source(),
66 | Tags: s.app.Compile.Tags(),
67 | }
68 | s.app.Dispatch(&actions.Send{
69 | Message: message,
70 | })
71 | case *actions.DeployMessage:
72 | switch message := action.Message.(type) {
73 | case gettermsg.Downloading:
74 | if message.Message != "" {
75 | s.app.Log(message.Message)
76 | }
77 | case buildermsg.Building:
78 | if message.Message != "" {
79 | s.app.Log(message.Message)
80 | }
81 | case constormsg.Storing:
82 | s.app.Log("storing")
83 | case messages.DeployComplete:
84 | s.mainHash = message.Main
85 | s.indexHash = message.Index
86 | s.app.Dispatch(&actions.ModalOpen{Modal: models.DeployDoneModal})
87 | s.app.LogHide("deployed")
88 | payload.Notify()
89 | }
90 | case *actions.DeployClose:
91 | // nothing
92 | }
93 | return true
94 | }
95 |
--------------------------------------------------------------------------------
/stores/editor.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "github.com/dave/flux"
5 | "github.com/dave/play/actions"
6 | )
7 |
8 | func NewEditorStore(app *App) *EditorStore {
9 | s := &EditorStore{
10 | app: app,
11 | currentFiles: map[string]string{},
12 | }
13 | return s
14 | }
15 |
16 | type EditorStore struct {
17 | app *App
18 |
19 | sizes []float64
20 | currentPackage string
21 | currentFiles map[string]string // tracks the currently selected file in each package
22 | loaded bool
23 | }
24 |
25 | func (s *EditorStore) Loaded() bool {
26 | return s.loaded
27 | }
28 |
29 | func (s *EditorStore) Sizes() []float64 {
30 | return s.sizes
31 | }
32 |
33 | func (s *EditorStore) CurrentPackage() string {
34 | return s.currentPackage
35 | }
36 |
37 | func (s *EditorStore) CurrentFile() string {
38 | return s.currentFiles[s.currentPackage]
39 | }
40 |
41 | func (s *EditorStore) defaultPackage() string {
42 | if len(s.app.Source.Packages()) == 0 {
43 | return ""
44 | }
45 | var path string
46 |
47 | // default to the first main package
48 | for p, n := range s.app.Scanner.Names() {
49 | if n == "main" {
50 | path = p
51 | break
52 | }
53 | }
54 |
55 | if path == "" {
56 | // if no main package, choose first package ordered by name
57 | path = s.app.Source.Packages()[0]
58 | }
59 |
60 | return path
61 | }
62 |
63 | func (s *EditorStore) defaultFile(path string) string {
64 | if len(s.app.Source.Files(path)) == 0 {
65 | return ""
66 | }
67 | if _, ok := s.app.Source.Files(path)["README.md"]; ok {
68 | return "README.md"
69 | }
70 | if _, ok := s.app.Source.Files(path)["readme.md"]; ok {
71 | return "readme.md"
72 | }
73 | if _, ok := s.app.Source.Files(path)["main.go"]; ok {
74 | return "main.go"
75 | }
76 | return s.app.Source.Filenames(path)[0]
77 | }
78 |
79 | func (s *EditorStore) Handle(payload *flux.Payload) bool {
80 | switch a := payload.Action.(type) {
81 | case *actions.DragDrop:
82 | payload.Wait(s.app.Source)
83 | s.currentPackage = s.defaultPackage()
84 | s.currentFiles[s.currentPackage] = s.defaultFile(s.currentPackage)
85 | payload.Notify()
86 | case *actions.LoadSource:
87 | payload.Wait(s.app.Scanner)
88 |
89 | defer func() {
90 | s.loaded = true
91 | }()
92 |
93 | var switchPackage string
94 | for path := range a.Source {
95 | switchPackage = path
96 | if s.app.Scanner.Name(path) == "main" {
97 | // break if we find a main package
98 | break
99 | }
100 | }
101 |
102 | // Switch to the right package.
103 | if a.CurrentPackage != "" && s.app.Source.HasPackage(a.CurrentPackage) {
104 | s.currentPackage = a.CurrentPackage
105 | } else {
106 | s.currentPackage = switchPackage
107 | }
108 |
109 | // Switch to the right file.
110 | if a.CurrentFile != "" && s.app.Source.HasFile(s.currentPackage, a.CurrentFile) {
111 | s.currentFiles[s.currentPackage] = a.CurrentFile
112 | } else {
113 | s.currentFiles[s.currentPackage] = s.defaultFile(s.currentPackage)
114 | }
115 |
116 | payload.Notify()
117 |
118 | case *actions.ChangeSplit:
119 | s.sizes = a.Sizes
120 | payload.Notify()
121 | case *actions.UserChangedSplit:
122 | s.sizes = a.Sizes
123 | case *actions.UserChangedFile:
124 | s.currentFiles[s.currentPackage] = a.Name
125 | payload.Notify()
126 | case *actions.ChangeFile:
127 | s.currentPackage = a.Path
128 | s.currentFiles[a.Path] = a.Name
129 | payload.Notify()
130 | case *actions.UserChangedPackage:
131 | s.currentPackage = a.Path
132 | if s.currentFiles[a.Path] == "" {
133 | s.currentFiles[a.Path] = s.defaultFile(a.Path)
134 | }
135 | payload.Notify()
136 | case *actions.AddFile:
137 | payload.Wait(s.app.Source)
138 | s.currentFiles[s.currentPackage] = a.Name
139 | payload.Notify()
140 | case *actions.DeleteFile:
141 | payload.Wait(s.app.Source)
142 | if s.CurrentFile() == a.Name {
143 | s.currentFiles[s.currentPackage] = s.defaultFile(s.currentPackage)
144 | payload.Notify()
145 | }
146 | case *actions.AddPackage:
147 | payload.Wait(s.app.Source)
148 | s.currentPackage = a.Path
149 | payload.Notify()
150 | case *actions.RemovePackage:
151 | payload.Wait(s.app.Scanner)
152 | if s.currentPackage == a.Path {
153 | s.currentPackage = s.defaultPackage()
154 | if s.currentFiles[s.currentPackage] == "" {
155 | s.currentFiles[s.currentPackage] = s.defaultFile(s.currentPackage)
156 | }
157 | payload.Notify()
158 | }
159 | }
160 | return true
161 | }
162 |
--------------------------------------------------------------------------------
/stores/empty.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "github.com/dave/flux"
5 | )
6 |
7 | func NewEmptyStore(app *App) *EmptyStore {
8 | s := &EmptyStore{
9 | app: app,
10 | }
11 | return s
12 | }
13 |
14 | type EmptyStore struct {
15 | app *App
16 | }
17 |
18 | func (s *EmptyStore) Handle(payload *flux.Payload) bool {
19 | switch action := payload.Action.(type) {
20 | default:
21 | _ = action
22 | }
23 | return true
24 | }
25 |
--------------------------------------------------------------------------------
/stores/history.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "github.com/dave/flux"
5 | "github.com/dave/play/actions"
6 | "github.com/gopherjs/gopherjs/js"
7 | )
8 |
9 | func NewHistoryStore(app *App) *HistoryStore {
10 | s := &HistoryStore{
11 | app: app,
12 | }
13 | return s
14 | }
15 |
16 | type HistoryStore struct {
17 | app *App
18 | }
19 |
20 | func (s *HistoryStore) Handle(payload *flux.Payload) bool {
21 | switch a := payload.Action.(type) {
22 | case *actions.UserChangedText,
23 | *actions.AddFile,
24 | *actions.DeleteFile,
25 | *actions.AddPackage,
26 | *actions.RemovePackage,
27 | *actions.DragDrop,
28 | *actions.BuildTags:
29 | js.Global.Get("history").Call("replaceState", js.M{}, "", "/")
30 | case *actions.LoadSource:
31 | if a.Save {
32 | js.Global.Get("history").Call("replaceState", js.M{}, "", "/")
33 | }
34 | }
35 | return true
36 | }
37 |
--------------------------------------------------------------------------------
/stores/local.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 |
8 | "encoding/json"
9 |
10 | "regexp"
11 |
12 | "github.com/dave/flux"
13 | "github.com/dave/jsgo/config"
14 | "github.com/dave/locstor"
15 | "github.com/dave/play/actions"
16 | "github.com/dave/play/models"
17 | "honnef.co/go/js/dom"
18 | )
19 |
20 | type LocalStore struct {
21 | app *App
22 |
23 | local *locstor.DataStore
24 | initialized bool
25 | }
26 |
27 | func NewLocalStore(app *App) *LocalStore {
28 | s := &LocalStore{
29 | app: app,
30 | local: locstor.NewDataStore(locstor.JSONEncoding),
31 | }
32 | return s
33 | }
34 |
35 | func (s *LocalStore) Initialized() bool {
36 | return s.initialized
37 | }
38 |
39 | func (s *LocalStore) Handle(payload *flux.Payload) bool {
40 | switch action := payload.Action.(type) {
41 | case *actions.Load:
42 | var sizes []float64
43 | found, err := s.local.Find("split-sizes", &sizes)
44 | if err != nil {
45 | s.app.Fail(err)
46 | return true
47 | }
48 | if !found {
49 | sizes = defaultSizes
50 | }
51 | s.app.Dispatch(&actions.ChangeSplit{Sizes: sizes})
52 |
53 | location := strings.Trim(dom.GetWindow().Location().Pathname, "/")
54 |
55 | var seenHelp bool
56 | if _, err := s.local.Find("seen-help", &seenHelp); err != nil {
57 | s.app.Fail(err)
58 | return true
59 | }
60 | if !seenHelp {
61 | s.app.Dispatch(&actions.ModalOpen{Modal: models.HelpModal})
62 | if err := s.local.Save("seen-help", true); err != nil {
63 | s.app.Fail(err)
64 | return true
65 | }
66 | }
67 |
68 | // No page path -> load files from local storage or use default files
69 | if location == "" {
70 | var currentPackage, currentFile string
71 | var buildTags []string
72 | var source map[string]map[string]string
73 | found, err = s.local.Find("source", &source)
74 | if err != nil {
75 | s.app.Fail(err)
76 | return true
77 | }
78 | if !found {
79 | // old format for storing files
80 | var files map[string]string
81 | found, err = s.local.Find("files", &files)
82 | if err != nil {
83 | s.app.Fail(err)
84 | return true
85 | }
86 | if found {
87 | source = map[string]map[string]string{"main": files}
88 | }
89 | }
90 | if found {
91 | // if we found files in local storage, also load the current file and package
92 | if _, err := s.local.Find("current-file", ¤tFile); err != nil {
93 | s.app.Fail(err)
94 | return true
95 | }
96 | if _, err := s.local.Find("current-package", ¤tPackage); err != nil {
97 | s.app.Fail(err)
98 | return true
99 | }
100 | if _, err := s.local.Find("build-tags", &buildTags); err != nil {
101 | s.app.Fail(err)
102 | return true
103 | }
104 | } else {
105 | // if we didn't find source in the local storage, add the default file
106 | source = map[string]map[string]string{"main": {"main.go": defaultFile}}
107 | currentFile = "main.go"
108 | currentPackage = "main"
109 | }
110 | s.app.Dispatch(&actions.LoadSource{
111 | Source: source,
112 | CurrentFile: currentFile,
113 | CurrentPackage: currentPackage,
114 | Tags: buildTags,
115 | Update: true,
116 | })
117 | break
118 | }
119 |
120 | // Hash in page path -> load files from src.jsgo.io json blob
121 | if shaRegex.MatchString(location) {
122 | resp, err := http.Get(fmt.Sprintf("%s://%s/%s.json", config.Protocol[config.Src], config.Host[config.Src], location))
123 | if err != nil {
124 | s.app.Fail(err)
125 | return true
126 | }
127 | if resp.StatusCode != 200 {
128 | s.app.Fail(fmt.Errorf("error %d loading source", resp.StatusCode))
129 | return true
130 | }
131 | var sp models.SharePack
132 | if err := json.NewDecoder(resp.Body).Decode(&sp); err != nil {
133 | s.app.Fail(err)
134 | return true
135 | }
136 | s.app.Dispatch(&actions.LoadSource{
137 | Source: sp.Source,
138 | Tags: sp.Tags,
139 | Update: true,
140 | })
141 | break
142 | }
143 |
144 | // Package path in page path -> open websocket and load files
145 | s.app.Dispatch(&actions.RequestStart{Type: models.InitialiseRequest, Path: location})
146 |
147 | case *actions.UserChangedSplit:
148 | if err := s.saveSplitSizes(action.Sizes); err != nil {
149 | s.app.Fail(err)
150 | return true
151 | }
152 | case *actions.UserChangedText, *actions.FormatCode:
153 | payload.Wait(s.app.Editor)
154 | if err := s.saveSource(); err != nil {
155 | s.app.Fail(err)
156 | return true
157 | }
158 | case *actions.BuildTags:
159 | payload.Wait(s.app.Compile)
160 | if err := s.saveSource(); err != nil {
161 | s.app.Fail(err)
162 | return true
163 | }
164 | case *actions.UserChangedFile:
165 | payload.Wait(s.app.Editor)
166 | if err := s.saveSource(); err != nil {
167 | s.app.Fail(err)
168 | return true
169 | }
170 | case *actions.UserChangedPackage:
171 | payload.Wait(s.app.Editor)
172 | if err := s.saveSource(); err != nil {
173 | s.app.Fail(err)
174 | return true
175 | }
176 | case *actions.AddFile, *actions.DeleteFile:
177 | payload.Wait(s.app.Source)
178 | if err := s.saveSource(); err != nil {
179 | s.app.Fail(err)
180 | return true
181 | }
182 | case *actions.AddPackage, *actions.RemovePackage, *actions.DragDrop:
183 | payload.Wait(s.app.Editor)
184 | if err := s.saveSource(); err != nil {
185 | s.app.Fail(err)
186 | return true
187 | }
188 | case *actions.LoadSource:
189 | if action.Save {
190 | payload.Wait(s.app.Editor)
191 | if err := s.saveSource(); err != nil {
192 | s.app.Fail(err)
193 | return true
194 | }
195 | }
196 | }
197 | return true
198 | }
199 |
200 | func (s *LocalStore) saveSource() error {
201 | s.local.Delete("files") // delete old format file storage location
202 | if err := s.local.Save("source", s.app.Source.Source()); err != nil {
203 | return err
204 | }
205 | if err := s.local.Save("current-package", s.app.Editor.CurrentPackage()); err != nil {
206 | return err
207 | }
208 | if err := s.local.Save("current-file", s.app.Editor.CurrentFile()); err != nil {
209 | return err
210 | }
211 | if err := s.local.Save("build-tags", s.app.Compile.Tags()); err != nil {
212 | return err
213 | }
214 | return nil
215 | }
216 |
217 | func (s *LocalStore) saveSplitSizes(sizes []float64) error {
218 | return s.local.Save("split-sizes", sizes)
219 | }
220 |
221 | var (
222 | defaultSizes = []float64{50, 50}
223 | defaultFile = `package main
224 |
225 | import (
226 | "fmt"
227 | "honnef.co/go/js/dom"
228 | )
229 |
230 | func main() {
231 | body := dom.GetWindow().Document().GetElementsByTagName("body")[0]
232 | body.SetInnerHTML("Hello, HTML!")
233 | fmt.Println("Hello, console!")
234 | }`
235 | )
236 |
237 | var shaRegex = regexp.MustCompile("^[0-9a-f]{40}$")
238 |
--------------------------------------------------------------------------------
/stores/page.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "github.com/dave/flux"
5 | "github.com/dave/play/actions"
6 | "github.com/dave/play/models"
7 | )
8 |
9 | func NewPageStore(app *App) *PageStore {
10 | s := &PageStore{
11 | app: app,
12 | autoOpen: true,
13 | modals: map[models.Modal]bool{},
14 | minify: true,
15 | }
16 | return s
17 | }
18 |
19 | type PageStore struct {
20 | app *App
21 | console bool
22 | minify bool
23 | autoOpen bool
24 | modals map[models.Modal]bool
25 | showAllDeps bool // show all dependencies in the load package modal
26 | }
27 |
28 | func (s *PageStore) ShowAllDeps() bool {
29 | return s.showAllDeps
30 | }
31 |
32 | func (s *PageStore) ModalOpen(modal models.Modal) bool {
33 | return s.modals[modal]
34 | }
35 |
36 | func (s *PageStore) Console() bool {
37 | return s.console
38 | }
39 |
40 | func (s *PageStore) Minify() bool {
41 | return s.minify
42 | }
43 |
44 | func (s *PageStore) Handle(payload *flux.Payload) bool {
45 | switch a := payload.Action.(type) {
46 | case *actions.ModalOpen:
47 | s.modals[a.Modal] = true
48 | payload.Notify()
49 | case *actions.ModalClose:
50 | s.modals[a.Modal] = false
51 | payload.Notify()
52 | case *actions.ConsoleToggleClick:
53 | s.console = !s.console
54 | payload.Notify()
55 | case *actions.MinifyToggleClick:
56 | s.minify = !s.minify
57 | payload.Notify()
58 | case *actions.ConsoleFirstWrite:
59 | if s.autoOpen {
60 | s.console = true
61 | s.autoOpen = false
62 | payload.Notify()
63 | }
64 | case *actions.ShowAllDepsChange:
65 | s.showAllDeps = a.State
66 | payload.Notify()
67 | }
68 | return true
69 | }
70 |
--------------------------------------------------------------------------------
/stores/request.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "github.com/dave/flux"
5 | "github.com/dave/jsgo/server/play/messages"
6 | "github.com/dave/jsgo/server/servermsg"
7 | "github.com/dave/play/actions"
8 | "github.com/dave/play/models"
9 | "github.com/dave/services"
10 | "github.com/dave/services/getter/gettermsg"
11 | )
12 |
13 | func NewRequestStore(app *App) *RequestStore {
14 | s := &RequestStore{
15 | app: app,
16 | }
17 | return s
18 | }
19 |
20 | type RequestStore struct {
21 | app *App
22 | }
23 |
24 | func (s *RequestStore) Handle(payload *flux.Payload) bool {
25 | switch action := payload.Action.(type) {
26 | case *actions.RequestStart:
27 | s.app.Log("downloading")
28 | s.app.Dispatch(&actions.Dial{
29 | Url: defaultUrl(),
30 | Open: func() flux.ActionInterface { return &actions.RequestOpen{RequestStart: action} },
31 | Message: func(m interface{}) flux.ActionInterface {
32 | return &actions.RequestMessage{RequestStart: action, Message: m}
33 | },
34 | Close: func() flux.ActionInterface { return &actions.RequestClose{RequestStart: action} },
35 | })
36 | payload.Notify()
37 | case *actions.RequestOpen:
38 | var message services.Message
39 | switch action.Type {
40 | case models.GetRequest:
41 | message = messages.Get{
42 | Path: action.Path,
43 | }
44 | case models.UpdateRequest:
45 | message = messages.Update{
46 | Source: s.app.Source.Source(),
47 | Cache: s.app.Archive.CacheStrings(),
48 | Minify: s.app.Page.Minify(),
49 | Tags: s.app.Compile.Tags(),
50 | }
51 | case models.InitialiseRequest:
52 | message = messages.Initialise{
53 | Path: action.Path,
54 | Minify: s.app.Page.Minify(),
55 | }
56 | }
57 | s.app.Dispatch(&actions.Send{
58 | Message: message,
59 | })
60 | case *actions.RequestMessage:
61 | switch message := action.Message.(type) {
62 | case servermsg.Queueing:
63 | if message.Position > 1 {
64 | s.app.Logf("queued position %d", message.Position)
65 | }
66 | case gettermsg.Downloading:
67 | if len(message.Message) > 0 {
68 | s.app.Log(message.Message)
69 | }
70 | case messages.GetComplete:
71 | s.app.Dispatch(&actions.LoadSource{
72 | Source: message.Source,
73 | Save: action.Type == models.GetRequest,
74 | Update: action.Type != models.InitialiseRequest, // never update after initialise (will be updated by initialise)
75 | })
76 | var count int
77 | for _, files := range message.Source {
78 | count += len(files)
79 | }
80 | if count == 1 {
81 | s.app.LogHide("got 1 file")
82 | } else {
83 | s.app.LogHidef("got %d files", count)
84 | }
85 | }
86 | case *actions.RequestClose:
87 | // nothing
88 | }
89 | return true
90 | }
91 |
--------------------------------------------------------------------------------
/stores/scanner.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "go/parser"
5 | "go/token"
6 |
7 | "strconv"
8 |
9 | "sort"
10 |
11 | "strings"
12 |
13 | "github.com/dave/flux"
14 | "github.com/dave/play/actions"
15 | "github.com/dave/services/includer"
16 | )
17 |
18 | func NewScannerStore(app *App) *ScannerStore {
19 | s := &ScannerStore{
20 | app: app,
21 | imports: map[string]map[string][]string{},
22 | names: map[string]string{},
23 | clashes: map[string]map[string]bool{},
24 | }
25 | return s
26 | }
27 |
28 | type ScannerStore struct {
29 | app *App
30 | imports map[string]map[string][]string
31 | names map[string]string
32 | clashes map[string]map[string]bool
33 | }
34 |
35 | func (s *ScannerStore) Clashes() map[string]map[string]bool {
36 | return s.clashes
37 | }
38 |
39 | // Main is the path of the main package
40 | func (s *ScannerStore) Main() (string, int) {
41 | if s.names[s.app.Editor.CurrentPackage()] == "main" {
42 | return s.app.Editor.CurrentPackage(), 0
43 | }
44 | // count the main packages
45 | var count int
46 | var path string
47 | for p, n := range s.names {
48 | if n == "main" {
49 | count++
50 | path = p
51 | }
52 | }
53 | if count == 1 {
54 | return path, 1
55 | }
56 | return "", count
57 | }
58 |
59 | func (s *ScannerStore) MainPackages() map[string]bool {
60 | m := map[string]bool{}
61 | for p, n := range s.names {
62 | if n == "main" {
63 | m[p] = true
64 | }
65 | }
66 | return m
67 | }
68 |
69 | func (s *ScannerStore) DisplayPath(path string) string {
70 | parts := strings.Split(path, "/")
71 | guessed := parts[len(parts)-1]
72 | name := s.names[path]
73 | suffix := ""
74 | if guessed != name && name != "" {
75 | suffix = " (" + name + ")"
76 | }
77 | return path + suffix
78 | }
79 |
80 | func (s *ScannerStore) DisplayName(path string) string {
81 | if s.names[path] != "" {
82 | return s.names[path]
83 | }
84 | parts := strings.Split(path, "/")
85 | return parts[len(parts)-1]
86 | }
87 |
88 | func (s *ScannerStore) Name(path string) string {
89 | return s.names[path]
90 | }
91 |
92 | func (s *ScannerStore) Names() map[string]string {
93 | return s.names
94 | }
95 |
96 | // Imports returns all the imports from all files in a package
97 | func (s *ScannerStore) Imports(path string) []string {
98 | var a []string
99 | for _, f := range s.imports[path] {
100 | for _, i := range f {
101 | a = append(a, i)
102 | }
103 | }
104 | return a
105 | }
106 |
107 | func (s *ScannerStore) AllImports() map[string]bool {
108 | m := map[string]bool{}
109 | for _, imps := range s.imports {
110 | for _, file := range imps {
111 | for _, imp := range file {
112 | m[imp] = true
113 | }
114 | }
115 | }
116 | return m
117 | }
118 |
119 | func (s *ScannerStore) AllImportsOrdered() []string {
120 | var a []string
121 | m := map[string]bool{}
122 | for _, imps := range s.imports {
123 | for _, file := range imps {
124 | for _, imp := range file {
125 | if !m[imp] {
126 | m[imp] = true
127 | a = append(a, imp)
128 | }
129 | }
130 | }
131 | }
132 | sort.Strings(a)
133 | return a
134 | }
135 |
136 | func (s *ScannerStore) Handle(payload *flux.Payload) bool {
137 | switch action := payload.Action.(type) {
138 | case *actions.BuildTags:
139 | payload.Wait(s.app.Compile)
140 |
141 | s.imports = map[string]map[string][]string{}
142 | s.names = map[string]string{}
143 |
144 | var changed bool
145 | for path, files := range s.app.Source.Source() {
146 | for name, contents := range files {
147 | if s.refresh(path, name, contents) {
148 | changed = true
149 | }
150 | }
151 | }
152 |
153 | if s.checkForClash() {
154 | changed = true
155 | }
156 |
157 | if changed {
158 | payload.Notify()
159 | }
160 |
161 | case *actions.DeleteFile:
162 | delete(s.imports[s.app.Editor.CurrentPackage()], action.Name)
163 | payload.Notify()
164 | case *actions.RemovePackage:
165 | payload.Wait(s.app.Source)
166 | delete(s.imports, action.Path)
167 | delete(s.names, action.Path)
168 | s.checkForClash()
169 | payload.Notify()
170 | case *actions.AddPackage:
171 | payload.Wait(s.app.Source)
172 | if s.checkForClash() {
173 | payload.Notify()
174 | }
175 | case *actions.LoadSource:
176 | payload.Wait(s.app.Source)
177 | payload.Wait(s.app.Compile)
178 |
179 | for path := range action.Source {
180 | delete(s.imports, path)
181 | delete(s.names, path)
182 | }
183 |
184 | var changed bool
185 | for path, files := range action.Source {
186 | for name, contents := range files {
187 | if s.refresh(path, name, contents) {
188 | changed = true
189 | }
190 | }
191 | }
192 |
193 | if s.checkForClash() {
194 | changed = true
195 | }
196 |
197 | if changed {
198 | payload.Notify()
199 | }
200 | case *actions.RequestClose:
201 | payload.Wait(s.app.Archive)
202 | if s.checkForClash() {
203 | payload.Notify()
204 | }
205 | case *actions.DragDrop:
206 | payload.Wait(s.app.Source)
207 | var changed bool
208 | for path, files := range action.Changed {
209 | for name := range files {
210 | if s.refresh(path, name, s.app.Source.Contents(path, name)) {
211 | changed = true
212 | }
213 | }
214 | }
215 | if changed {
216 | payload.Notify()
217 | }
218 | case *actions.UserChangedText:
219 | payload.Wait(s.app.Source)
220 | if action.Changed {
221 | if s.refresh(s.app.Editor.CurrentPackage(), s.app.Editor.CurrentFile(), s.app.Source.Current()) {
222 | payload.Notify()
223 | }
224 | }
225 | }
226 | return true
227 | }
228 |
229 | func (s *ScannerStore) checkForClash() bool {
230 |
231 | clashes := map[string]map[string]bool{}
232 |
233 | var check func(path string)
234 | check = func(path string) {
235 | imports := map[string]bool{}
236 | if s.app.Source.HasPackage(path) {
237 | for _, imp := range s.app.Scanner.Imports(path) {
238 | imports[imp] = true
239 | }
240 | } else if ci, ok := s.app.Archive.Cache()[path]; ok {
241 | for _, imp := range ci.Archive.Imports {
242 | imports[imp] = true
243 | }
244 | }
245 | for imp := range imports {
246 | check(imp)
247 | if !s.app.Source.HasPackage(path) && s.app.Source.HasPackage(imp) {
248 | if clashes[imp] == nil {
249 | clashes[imp] = map[string]bool{}
250 | }
251 | clashes[imp][path] = true
252 | }
253 | }
254 | }
255 | for path := range s.MainPackages() {
256 | check(path)
257 | }
258 |
259 | hadClashesBefore := len(s.clashes) > 0
260 | hasClashesAfter := len(clashes) > 0
261 |
262 | s.clashes = clashes
263 |
264 | return hadClashesBefore || hasClashesAfter
265 | }
266 |
267 | func (s *ScannerStore) refresh(path, filename, contents string) bool {
268 |
269 | include, err := includer.New(map[string]string{filename: contents}, s.app.Compile.Tags()).Include(filename)
270 | if err != nil {
271 | // ignore errors (we never want to throw an error while we're scanning the source)
272 | return false
273 | }
274 | if !include {
275 | return false
276 | }
277 |
278 | fset := token.NewFileSet()
279 |
280 | f, err := parser.ParseFile(fset, filename, contents, parser.ImportsOnly)
281 | if err != nil {
282 | // ignore errors (we never want to throw an error while we're scanning the source)
283 | return false
284 | }
285 |
286 | name := f.Name.Name
287 |
288 | var nameChanged bool
289 | if s.names[path] != name {
290 | nameChanged = true
291 | s.names[path] = name
292 | }
293 |
294 | var imports []string
295 | for _, v := range f.Imports {
296 | unquoted, err := strconv.Unquote(v.Path.Value)
297 | if err != nil {
298 | // ignore errors (we never want to throw an error while we're scanning the source)
299 | continue
300 | }
301 | imports = append(imports, unquoted)
302 | }
303 | sort.Strings(imports)
304 |
305 | var importsChanged bool
306 | if s.imports[path] == nil {
307 | s.imports[path] = map[string][]string{}
308 | }
309 | if s.changed(s.imports[path][filename], imports) {
310 | importsChanged = true
311 | s.imports[path][filename] = imports
312 | }
313 |
314 | return nameChanged || importsChanged
315 | }
316 |
317 | func (s *ScannerStore) changed(imports, compare []string) bool {
318 | if len(compare) != len(imports) {
319 | return true
320 | }
321 | for i := range compare {
322 | if imports[i] != compare[i] {
323 | return true
324 | }
325 | }
326 | return false
327 | }
328 |
--------------------------------------------------------------------------------
/stores/share.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/dave/flux"
7 | "github.com/dave/jsgo/server/play/messages"
8 | "github.com/dave/play/actions"
9 | "github.com/dave/services/constor/constormsg"
10 | "github.com/gopherjs/gopherjs/js"
11 | )
12 |
13 | func NewShareStore(app *App) *ShareStore {
14 | s := &ShareStore{
15 | app: app,
16 | }
17 | return s
18 | }
19 |
20 | type ShareStore struct {
21 | app *App
22 | }
23 |
24 | func (s *ShareStore) Handle(payload *flux.Payload) bool {
25 | switch action := payload.Action.(type) {
26 | case *actions.ShareStart:
27 | s.app.Log("sharing")
28 | s.app.Dispatch(&actions.Dial{
29 | Url: defaultUrl(),
30 | Open: func() flux.ActionInterface { return &actions.ShareOpen{} },
31 | Message: func(m interface{}) flux.ActionInterface { return &actions.ShareMessage{Message: m} },
32 | Close: func() flux.ActionInterface { return &actions.ShareClose{} },
33 | })
34 | payload.Notify()
35 | case *actions.ShareOpen:
36 | message := messages.Share{
37 | Source: s.app.Source.Source(),
38 | Tags: s.app.Compile.Tags(),
39 | }
40 | s.app.Dispatch(&actions.Send{
41 | Message: message,
42 | })
43 | case *actions.ShareMessage:
44 | switch message := action.Message.(type) {
45 | case constormsg.Storing:
46 | s.app.Log("storing")
47 | case messages.ShareComplete:
48 | js.Global.Get("history").Call("replaceState", js.M{}, "", fmt.Sprintf("/%s", message.Hash))
49 | s.app.LogHide("shared")
50 | }
51 | case *actions.ShareClose:
52 | // nothing
53 | }
54 | return true
55 | }
56 |
--------------------------------------------------------------------------------
/stores/source.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "archive/zip"
5 | "sort"
6 |
7 | "go/format"
8 |
9 | "strings"
10 |
11 | "bytes"
12 | "io"
13 |
14 | "path/filepath"
15 |
16 | "io/ioutil"
17 |
18 | "errors"
19 |
20 | "fmt"
21 |
22 | "github.com/dave/flux"
23 | "github.com/dave/jsgo/config"
24 | "github.com/dave/play/actions"
25 | "github.com/dave/saver"
26 | )
27 |
28 | func NewSourceStore(app *App) *SourceStore {
29 | s := &SourceStore{
30 | app: app,
31 | source: map[string]map[string]string{},
32 | }
33 | return s
34 | }
35 |
36 | type SourceStore struct {
37 | app *App
38 |
39 | source map[string]map[string]string
40 | }
41 |
42 | func (s *SourceStore) Current() string {
43 | return s.source[s.app.Editor.CurrentPackage()][s.app.Editor.CurrentFile()]
44 | }
45 |
46 | func (s *SourceStore) Contents(path, name string) string {
47 | return s.source[path][name]
48 | }
49 |
50 | func (s *SourceStore) Files(path string) map[string]string {
51 | return s.source[path]
52 | }
53 |
54 | func (s *SourceStore) Source() map[string]map[string]string {
55 | return s.source
56 | }
57 |
58 | func (s *SourceStore) HasPackage(path string) bool {
59 | return s.source[path] != nil
60 | }
61 |
62 | func (s *SourceStore) HasFile(path, name string) bool {
63 | if !s.HasPackage(path) {
64 | return false
65 | }
66 | _, ok := s.source[path][name]
67 | return ok
68 | }
69 |
70 | func (s *SourceStore) Count() int {
71 | var count int
72 | for _, pkg := range s.source {
73 | count += len(pkg)
74 | }
75 | return count
76 | }
77 |
78 | // SinglePackage returns a random package. Use for when len(s.source) == 1
79 | func (s *SourceStore) SinglePackage() (path string, files map[string]string) {
80 | for path, files := range s.source {
81 | return path, files
82 | }
83 | return "", nil
84 | }
85 |
86 | // SingleFile returns a random file in a random package. Use for when Count() == 1
87 | func (s *SourceStore) SingleFile() (path, name, contents string) {
88 | for path, files := range s.source {
89 | for name, contents := range files {
90 | return path, name, contents
91 | }
92 | }
93 | return "", "", ""
94 | }
95 |
96 | func (s *SourceStore) Packages() []string {
97 | var paths []string
98 | for p := range s.source {
99 | paths = append(paths, p)
100 | }
101 | sort.Strings(paths)
102 | return paths
103 | }
104 |
105 | func (s *SourceStore) Filenames(path string) []string {
106 | var f []string
107 | for k := range s.source[path] {
108 | f = append(f, k)
109 | }
110 | sort.Strings(f)
111 | return f
112 | }
113 |
114 | func (s *SourceStore) Handle(payload *flux.Payload) bool {
115 | switch a := payload.Action.(type) {
116 | case *actions.DragEnter:
117 | s.app.Log("Drop to upload")
118 | case *actions.DragLeave:
119 | s.app.Log()
120 | case *actions.DragDrop:
121 | s.app.Log()
122 | packages := map[string]map[string][]byte{}
123 | if len(a.Files) == 1 && strings.HasSuffix(a.Files[0].Name(), ".zip") {
124 | b, err := ioutil.ReadAll(a.Files[0].Reader())
125 | if err != nil {
126 | s.app.Fail(err)
127 | return true
128 | }
129 | zr, err := zip.NewReader(bytes.NewReader(b), int64(a.Files[0].Len()))
130 | if err != nil {
131 | s.app.Fail(err)
132 | return true
133 | }
134 | for _, file := range zr.File {
135 | path, name := filepath.Split(file.Name)
136 | if !isValidFile(name) {
137 | continue
138 | }
139 | path = strings.Trim(path, "/")
140 | if path == "" {
141 | path = s.app.Editor.CurrentPackage()
142 | }
143 | if path == "" {
144 | path = "main"
145 | }
146 | fr, err := file.Open()
147 | if err != nil {
148 | s.app.Fail(err)
149 | return true
150 | }
151 | b, err := ioutil.ReadAll(fr)
152 | if err != nil {
153 | fr.Close()
154 | s.app.Fail(err)
155 | return true
156 | }
157 | fr.Close()
158 | if packages[path] == nil {
159 | packages[path] = map[string][]byte{}
160 | }
161 | packages[path][name] = b
162 | }
163 | } else {
164 | for _, f := range a.Files {
165 | if !isValidFile(f.Name()) {
166 | continue
167 | }
168 | path := strings.Trim(f.Dir(), "/")
169 | if path == "" {
170 | path = s.app.Editor.CurrentPackage()
171 | }
172 | if path == "" {
173 | path = "main"
174 | }
175 | b, err := ioutil.ReadAll(f.Reader())
176 | if err != nil {
177 | s.app.Fail(err)
178 | return true
179 | }
180 | if packages[path] == nil {
181 | packages[path] = map[string][]byte{}
182 | }
183 | packages[path][f.Name()] = b
184 | }
185 | }
186 |
187 | changed := map[string]map[string]bool{}
188 | for path, files := range packages {
189 | for name, contents := range files {
190 | if s.source[path] == nil {
191 | s.source[path] = map[string]string{}
192 | }
193 | if s.source[path][name] != string(contents) {
194 | s.source[path][name] = string(contents)
195 |
196 | // track changed files to pass to the scanner
197 | if changed[path] == nil {
198 | changed[path] = map[string]bool{}
199 | }
200 | changed[path][name] = true
201 | }
202 | }
203 | }
204 |
205 | a.Changed = changed
206 |
207 | payload.Notify()
208 |
209 | case *actions.DownloadClick:
210 | if s.Count() == 1 {
211 | _, name, contents := s.SingleFile()
212 | saver.Save(name, "text/plain", []byte(contents))
213 | break
214 | }
215 | buf := &bytes.Buffer{}
216 | zw := zip.NewWriter(buf)
217 | if len(s.source) == 1 {
218 | _, files := s.SinglePackage()
219 | for name, contents := range files {
220 | w, err := zw.Create(name)
221 | if err != nil {
222 | s.app.Fail(err)
223 | return true
224 | }
225 | if _, err := io.Copy(w, strings.NewReader(contents)); err != nil {
226 | s.app.Fail(err)
227 | return true
228 | }
229 | }
230 | } else {
231 | for path, files := range s.source {
232 | for name, contents := range files {
233 | w, err := zw.Create(filepath.Join(path, name))
234 | if err != nil {
235 | s.app.Fail(err)
236 | return true
237 | }
238 | if _, err := io.Copy(w, strings.NewReader(contents)); err != nil {
239 | s.app.Fail(err)
240 | return true
241 | }
242 | }
243 | }
244 | }
245 | zw.Close()
246 | saver.Save("src.zip", "application/zip", buf.Bytes())
247 | case *actions.UserChangedText:
248 | p := s.app.Editor.CurrentPackage()
249 | f := s.app.Editor.CurrentFile()
250 | if p == "" {
251 | s.app.Fail(errors.New("no package selected"))
252 | return true
253 | }
254 | if f == "" {
255 | s.app.Fail(errors.New("no file selected"))
256 | return true
257 | }
258 | if s.source[p] == nil {
259 | s.source[p] = map[string]string{}
260 | }
261 | if s.source[p][f] != a.Text {
262 | s.source[p][f] = a.Text
263 | a.Changed = true
264 | }
265 | case *actions.AddFile:
266 | p := s.app.Editor.CurrentPackage()
267 | if p == "" {
268 | s.app.Fail(errors.New("no package selected"))
269 | return true
270 | }
271 | if s.source[p] == nil {
272 | s.source[p] = map[string]string{}
273 | }
274 | if s.app.Scanner.Name(p) != "" && strings.HasSuffix(a.Name, ".go") {
275 | s.source[p][a.Name] = "package " + s.app.Scanner.Name(p) + "\n\n"
276 | } else {
277 | s.source[p][a.Name] = ""
278 | }
279 | payload.Notify()
280 | case *actions.AddPackage:
281 | if s.source[a.Path] == nil {
282 | s.source[a.Path] = map[string]string{}
283 | }
284 | payload.Notify()
285 | case *actions.DeleteFile:
286 | p := s.app.Editor.CurrentPackage()
287 | if p == "" {
288 | s.app.Fail(errors.New("no package selected"))
289 | return true
290 | }
291 | if !s.HasPackage(p) {
292 | s.app.Fail(fmt.Errorf("package %s not found", p))
293 | return true
294 | }
295 | if !s.HasFile(p, a.Name) {
296 | s.app.Fail(fmt.Errorf("%s not found", a.Name))
297 | return true
298 | }
299 | delete(s.source[p], a.Name)
300 | payload.Notify()
301 | case *actions.RemovePackage:
302 | if !s.HasPackage(a.Path) {
303 | s.app.Fail(fmt.Errorf("%s not found", a.Path))
304 | return true
305 | }
306 | delete(s.source, a.Path)
307 | payload.Notify()
308 | case *actions.LoadSource:
309 | for path, files := range a.Source {
310 | if s.source[path] == nil {
311 | s.source[path] = files
312 | }
313 | }
314 | payload.Notify()
315 | case *actions.FormatCode:
316 | p := s.app.Editor.CurrentPackage()
317 | f := s.app.Editor.CurrentFile()
318 | if strings.HasSuffix(f, ".go") {
319 | b, err := format.Source([]byte(s.Contents(p, f)))
320 | if err != nil {
321 | s.app.Fail(err)
322 | return true
323 | }
324 | s.source[p][f] = string(b)
325 | payload.Notify()
326 | }
327 | if a.Then != nil {
328 | s.app.Dispatch(a.Then)
329 | }
330 | }
331 | return true
332 | }
333 |
334 | func isValidFile(name string) bool {
335 | for _, ext := range config.ValidExtensions {
336 | if strings.HasSuffix(name, ext) {
337 | return true
338 | }
339 | }
340 | return false
341 | }
342 |
--------------------------------------------------------------------------------
/testing/a/a.go:
--------------------------------------------------------------------------------
1 | package a
2 |
3 | type A struct {
4 | A int
5 | }
6 |
--------------------------------------------------------------------------------
/testing/b/b.go:
--------------------------------------------------------------------------------
1 | package b
2 |
3 | import "github.com/dave/play/testing/a"
4 |
5 | func B(in a.A) a.A {
6 | return in
7 | }
8 |
--------------------------------------------------------------------------------
/testing/main/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/dave/play/testing/a"
7 | "github.com/dave/play/testing/b"
8 | )
9 |
10 | func main() {
11 | fmt.Println(b.B(a.A{A: 1}))
12 | }
13 |
--------------------------------------------------------------------------------
/views/add-file.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "fmt"
5 |
6 | "strings"
7 |
8 | "github.com/dave/play/actions"
9 | "github.com/dave/play/models"
10 | "github.com/dave/play/stores"
11 | "github.com/gopherjs/gopherjs/js"
12 | "github.com/gopherjs/vecty"
13 | "github.com/gopherjs/vecty/elem"
14 | "github.com/gopherjs/vecty/event"
15 | "github.com/gopherjs/vecty/prop"
16 | )
17 |
18 | type AddFileModal struct {
19 | *Modal
20 | input *vecty.HTML
21 | }
22 |
23 | func NewAddFileModal(app *stores.App) *AddFileModal {
24 | v := &AddFileModal{}
25 | v.Modal = &Modal{
26 | app: app,
27 | id: models.AddFileModal,
28 | title: "Add file",
29 | action: v.save,
30 | shown: func() {
31 | js.Global.Call("$", "#add-file-input").Call("focus")
32 | js.Global.Call("$", "#add-file-input").Call("val", "")
33 | },
34 | }
35 | return v
36 | }
37 |
38 | func (v *AddFileModal) Render() vecty.ComponentOrHTML {
39 | v.input = elem.Input(
40 | vecty.Markup(
41 | prop.Type(prop.TypeText),
42 | vecty.Class("form-control"),
43 | prop.ID("add-file-input"),
44 | event.KeyPress(func(ev *vecty.Event) {
45 | if ev.Get("keyCode").Int() == 13 {
46 | ev.Call("preventDefault")
47 | v.save(ev)
48 | }
49 | }),
50 | ),
51 | )
52 | return v.Body(
53 | elem.Form(
54 | elem.Div(
55 | vecty.Markup(vecty.Class("form-group")),
56 | elem.Label(
57 | vecty.Markup(
58 | vecty.Property("for", "add-file-input"),
59 | vecty.Class("col-form-label"),
60 | ),
61 | vecty.Text("Filename"),
62 | ),
63 | v.input,
64 | ),
65 | ),
66 | ).Build()
67 | }
68 |
69 | func (v *AddFileModal) save(*vecty.Event) {
70 | value := v.input.Node().Get("value").String()
71 | if strings.Contains(value, "/") {
72 | v.app.Fail(fmt.Errorf("filename %s must not contain a slash", value))
73 | return
74 | }
75 | if !strings.HasSuffix(value, ".go") && !strings.Contains(value, ".") {
76 | value = value + ".go"
77 | }
78 | if v.app.Source.HasFile(v.app.Editor.CurrentPackage(), value) {
79 | v.app.Fail(fmt.Errorf("%s already exists", value))
80 | return
81 | }
82 | v.app.Dispatch(&actions.ModalClose{Modal: models.AddFileModal})
83 | v.app.Dispatch(&actions.AddFile{Name: value})
84 | }
85 |
--------------------------------------------------------------------------------
/views/add-package.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/dave/play/actions"
7 | "github.com/dave/play/models"
8 | "github.com/dave/play/stores"
9 | "github.com/gopherjs/gopherjs/js"
10 | "github.com/gopherjs/vecty"
11 | "github.com/gopherjs/vecty/elem"
12 | "github.com/gopherjs/vecty/event"
13 | "github.com/gopherjs/vecty/prop"
14 | )
15 |
16 | type AddPackageModal struct {
17 | *Modal
18 | input *vecty.HTML
19 | }
20 |
21 | func NewAddPackageModal(app *stores.App) *AddPackageModal {
22 | v := &AddPackageModal{}
23 | v.Modal = &Modal{
24 | app: app,
25 | id: models.AddPackageModal,
26 | title: "Add package",
27 | action: v.save,
28 | shown: func() {
29 | js.Global.Call("$", "#add-package-input").Call("focus")
30 | js.Global.Call("$", "#add-package-input").Call("val", "")
31 | },
32 | }
33 | return v
34 | }
35 |
36 | func (v *AddPackageModal) Render() vecty.ComponentOrHTML {
37 | v.input = elem.Input(
38 | vecty.Markup(
39 | prop.Type(prop.TypeText),
40 | vecty.Class("form-control"),
41 | prop.ID("add-package-input"),
42 | event.KeyPress(func(ev *vecty.Event) {
43 | if ev.Get("keyCode").Int() == 13 {
44 | ev.Call("preventDefault")
45 | v.save(ev)
46 | }
47 | }),
48 | ),
49 | )
50 | return v.Body(
51 | elem.Form(
52 | elem.Div(
53 | vecty.Markup(vecty.Class("form-group")),
54 | elem.Label(
55 | vecty.Markup(
56 | vecty.Property("for", "add-package-input"),
57 | vecty.Class("col-form-label"),
58 | ),
59 | vecty.Text("Package path"),
60 | ),
61 | v.input,
62 | ),
63 | ),
64 | ).Build()
65 | }
66 |
67 | func (v *AddPackageModal) save(*vecty.Event) {
68 | value := v.input.Node().Get("value").String()
69 | if v.app.Source.HasPackage(value) {
70 | v.app.Fail(fmt.Errorf("%s already exists", value))
71 | return
72 | }
73 | v.app.Dispatch(&actions.ModalClose{Modal: models.AddPackageModal})
74 | v.app.Dispatch(&actions.AddPackage{Path: value})
75 | }
76 |
--------------------------------------------------------------------------------
/views/build-tags.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/dave/play/actions"
7 | "github.com/dave/play/models"
8 | "github.com/dave/play/stores"
9 | "github.com/gopherjs/vecty"
10 | "github.com/gopherjs/vecty/elem"
11 | "github.com/gopherjs/vecty/prop"
12 | )
13 |
14 | type BuildTagsModal struct {
15 | *Modal
16 | input *vecty.HTML
17 | }
18 |
19 | func NewBuildTagsModal(app *stores.App) *BuildTagsModal {
20 | v := &BuildTagsModal{}
21 | v.Modal = &Modal{
22 | app: app,
23 | id: models.BuildTagsModal,
24 | title: "Build tags",
25 | action: v.action,
26 | }
27 | return v
28 | }
29 |
30 | func (v *BuildTagsModal) Render() vecty.ComponentOrHTML {
31 | v.input = elem.Input(vecty.Markup(
32 | vecty.Class("form-control"),
33 | prop.Type(prop.TypeText),
34 | prop.ID("build-tags-input"),
35 | prop.Value(strings.Join(v.app.Compile.Tags(), " ")),
36 | ))
37 |
38 | return v.Body(
39 | elem.Form(
40 | elem.Div(
41 | vecty.Markup(
42 | vecty.Class("form-group"),
43 | ),
44 | elem.Label(
45 | vecty.Markup(
46 | vecty.Property("for", "build-tags-input"),
47 | vecty.Class("col-form-label"),
48 | ),
49 | vecty.Text("Build tags"),
50 | ),
51 | v.input,
52 | ),
53 | ),
54 | ).Build()
55 | }
56 |
57 | func (v *BuildTagsModal) action(*vecty.Event) {
58 | tags := strings.Fields(v.input.Node().Get("value").String())
59 | v.app.Dispatch(&actions.ModalClose{Modal: models.BuildTagsModal})
60 | if !compare(v.app.Compile.Tags(), tags) {
61 | v.app.Dispatch(&actions.BuildTags{Tags: tags})
62 | }
63 | }
64 |
65 | func compare(a, b []string) bool {
66 | if len(a) != len(b) {
67 | return false
68 | }
69 | for i := range a {
70 | if a[i] != b[i] {
71 | return false
72 | }
73 | }
74 | return true
75 | }
76 |
--------------------------------------------------------------------------------
/views/clash-warning.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 |
7 | "github.com/dave/play/models"
8 | "github.com/dave/play/stores"
9 | "github.com/gopherjs/vecty"
10 | "github.com/gopherjs/vecty/elem"
11 | )
12 |
13 | type ClashWarningModal struct {
14 | *Modal
15 | }
16 |
17 | func NewClashWarningModal(app *stores.App) *ClashWarningModal {
18 | v := &ClashWarningModal{
19 | &Modal{
20 | app: app,
21 | id: models.ClashWarningModal,
22 | title: "Warning",
23 | action: nil,
24 | },
25 | }
26 | return v
27 | }
28 |
29 | func (v *ClashWarningModal) Render() vecty.ComponentOrHTML {
30 | var paragraphs []vecty.MarkupOrChild
31 | for source, compiled := range v.app.Scanner.Clashes() {
32 | var paths []string
33 | for p := range compiled {
34 | paths = append(paths, p)
35 | }
36 | var text string
37 | if len(compiled) == 0 {
38 | continue
39 | } else {
40 | sort.Strings(paths)
41 | if len(compiled) == 1 {
42 | text = fmt.Sprintf("Source package %s is imported by this pre-compiled package:", source)
43 | } else {
44 | text = fmt.Sprintf("Source package %s is imported by these pre-compiled packages:", source)
45 | }
46 | var items []vecty.MarkupOrChild
47 | for _, path := range paths {
48 | items = append(items, elem.ListItem(
49 | vecty.Text(path),
50 | ))
51 | }
52 | paragraphs = append(paragraphs, elem.Paragraph(
53 | vecty.Text(text),
54 | elem.UnorderedList(items...),
55 | ))
56 | }
57 |
58 | }
59 | var text string
60 | if len(v.app.Scanner.Clashes()) == 1 {
61 | text = "If the external interface this source packages changes, your code may break at run-time. To solve this, either load the source for all pre-compiled packages, or use the Update feature each time you change the external interface of the source package."
62 | } else {
63 | text = "If the external interface these source packages change, your code may break at run-time. To solve this, either load the source for all pre-compiled packages, or use the Update feature each time you change the external interface of the source packages."
64 | }
65 | paragraphs = append(paragraphs, elem.Paragraph(
66 | vecty.Text(text),
67 | ))
68 |
69 | return v.Body(
70 | paragraphs...,
71 | ).Build()
72 | }
73 |
--------------------------------------------------------------------------------
/views/delete-file.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "github.com/dave/play/actions"
5 | "github.com/dave/play/models"
6 | "github.com/dave/play/stores"
7 | "github.com/gopherjs/vecty"
8 | "github.com/gopherjs/vecty/elem"
9 | "github.com/gopherjs/vecty/prop"
10 | )
11 |
12 | type DeleteFileModal struct {
13 | *Modal
14 | sel *vecty.HTML
15 | }
16 |
17 | func NewDeleteFileModal(app *stores.App) *DeleteFileModal {
18 | v := &DeleteFileModal{}
19 | v.Modal = &Modal{
20 | app: app,
21 | id: models.DeleteFileModal,
22 | title: "Delete file",
23 | action: v.action,
24 | }
25 | return v
26 | }
27 |
28 | func (v *DeleteFileModal) Render() vecty.ComponentOrHTML {
29 | items := []vecty.MarkupOrChild{
30 | vecty.Markup(
31 | vecty.Class("form-control"),
32 | prop.ID("delete-file-select"),
33 | ),
34 | }
35 | for _, name := range v.app.Source.Filenames(v.app.Editor.CurrentPackage()) {
36 | items = append(items,
37 | elem.Option(
38 | vecty.Markup(
39 | prop.Value(name),
40 | vecty.Property("selected", v.app.Editor.CurrentFile() == name),
41 | ),
42 | vecty.Text(name),
43 | ),
44 | )
45 | }
46 | v.sel = elem.Select(items...)
47 |
48 | return v.Body(
49 | elem.Form(
50 | elem.Div(
51 | vecty.Markup(
52 | vecty.Class("form-group"),
53 | ),
54 | elem.Label(
55 | vecty.Markup(
56 | vecty.Property("for", "delete-file-select"),
57 | vecty.Class("col-form-label"),
58 | ),
59 | vecty.Text("File"),
60 | ),
61 | v.sel,
62 | ),
63 | ),
64 | ).Build()
65 | }
66 |
67 | func (v *DeleteFileModal) action(*vecty.Event) {
68 | n := v.sel.Node()
69 | i := n.Get("selectedIndex").Int()
70 | value := n.Get("options").Index(i).Get("value").String()
71 | v.app.Dispatch(&actions.ModalClose{Modal: models.DeleteFileModal})
72 | v.app.Dispatch(&actions.DeleteFile{Name: value})
73 | }
74 |
--------------------------------------------------------------------------------
/views/deploy-done.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "github.com/dave/play/models"
5 | "github.com/dave/play/stores"
6 | "github.com/gopherjs/vecty"
7 | "github.com/gopherjs/vecty/elem"
8 | "github.com/gopherjs/vecty/event"
9 | "github.com/gopherjs/vecty/prop"
10 | )
11 |
12 | type DeployDoneModal struct {
13 | *Modal
14 | }
15 |
16 | func NewDeployDoneModal(app *stores.App) *DeployDoneModal {
17 | v := &DeployDoneModal{
18 | &Modal{
19 | app: app,
20 | id: models.DeployDoneModal,
21 | title: "Deployed",
22 | },
23 | }
24 | return v
25 | }
26 |
27 | func (v *DeployDoneModal) Render() vecty.ComponentOrHTML {
28 | return v.Body(
29 | elem.Form(
30 | elem.Div(
31 | vecty.Markup(vecty.Class("form-group")),
32 | elem.Label(
33 | vecty.Markup(
34 | vecty.Property("for", "deploy-done-modal"),
35 | vecty.Class("col-form-label"),
36 | ),
37 | vecty.Text("Link"),
38 | ),
39 | elem.Input(
40 | vecty.Markup(
41 | prop.Type(prop.TypeText),
42 | vecty.Class("form-control"),
43 | prop.ID("deploy-done-input-link"),
44 | event.Focus(func(ev *vecty.Event) {
45 | ev.Target.Call("select")
46 | }).PreventDefault(),
47 | prop.Value(v.app.Deploy.Index()),
48 | ),
49 | ),
50 | elem.Small(
51 | vecty.Markup(
52 | vecty.Class("form-text", "text-muted"),
53 | ),
54 | elem.Anchor(
55 | vecty.Markup(
56 | prop.Href(v.app.Deploy.Index()),
57 | vecty.Property("target", "_blank"),
58 | ),
59 | vecty.Text("Click here"),
60 | ),
61 | vecty.Text(". Use the link for testing and toy projects. Remember you're sharing the jsgo.io domain with everyone else, so the browser environment should be considered toxic."),
62 | ),
63 | elem.Label(
64 | vecty.Markup(
65 | vecty.Property("for", "deploy-done-modal"),
66 | vecty.Class("col-form-label"),
67 | ),
68 | vecty.Text("Loader JS"),
69 | ),
70 | elem.Input(
71 | vecty.Markup(
72 | prop.Type(prop.TypeText),
73 | vecty.Class("form-control"),
74 | prop.ID("deploy-done-input-loader"),
75 | event.Focus(func(ev *vecty.Event) {
76 | ev.Target.Call("select")
77 | }).PreventDefault(),
78 | prop.Value(v.app.Deploy.LoaderJs()),
79 | ),
80 | ),
81 | elem.Small(
82 | vecty.Markup(
83 | vecty.Class("form-text", "text-muted"),
84 | ),
85 | vecty.Text("For production, use the Loader JS in a script tag on your own site."),
86 | ),
87 | ),
88 | ),
89 | ).Build()
90 | }
91 |
--------------------------------------------------------------------------------
/views/editor.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "time"
5 |
6 | "strings"
7 |
8 | "github.com/dave/play/actions"
9 | "github.com/dave/play/stores"
10 | "github.com/gopherjs/gopherjs/js"
11 | "github.com/gopherjs/vecty"
12 | "github.com/gopherjs/vecty/elem"
13 | "github.com/gopherjs/vecty/prop"
14 | "github.com/tulir/gopher-ace"
15 | "honnef.co/go/js/dom"
16 | )
17 |
18 | type Editor struct {
19 | vecty.Core
20 | app *stores.App
21 |
22 | editor ace.Editor
23 | }
24 |
25 | func NewEditor(app *stores.App) *Editor {
26 | v := &Editor{
27 | app: app,
28 | }
29 | return v
30 | }
31 |
32 | func getEditorMode(filename string) string {
33 | filename = strings.ToLower(filename)
34 | switch {
35 | case strings.HasSuffix(filename, ".go"):
36 | return "ace/mode/golang"
37 | case strings.HasSuffix(filename, ".html"):
38 | return "ace/mode/html"
39 | case strings.HasSuffix(filename, ".js"):
40 | return "ace/mode/javascript"
41 | case strings.HasSuffix(filename, ".md"):
42 | return "ace/mode/markdown"
43 | default:
44 | return "ace/mode/plain_text"
45 | }
46 | }
47 |
48 | func (v *Editor) Mount() {
49 | v.app.Watch(v, func(done chan struct{}) {
50 | defer close(done)
51 | if v.app.Source.Current() != v.editor.GetValue() {
52 | // only update the editor if the text is changed
53 | v.editor.SetValue(v.app.Source.Current())
54 | v.editor.ClearSelection()
55 | v.editor.MoveCursorTo(0, 0)
56 | }
57 | correctMode := getEditorMode(v.app.Editor.CurrentFile())
58 | currentMode := v.editor.GetOption("mode").String()
59 | if correctMode != currentMode {
60 | v.editor.SetOptions(map[string]interface{}{
61 | "mode": correctMode,
62 | })
63 | }
64 | })
65 |
66 | v.editor = ace.Edit("editor")
67 | v.editor.SetOptions(map[string]interface{}{
68 | "mode": "ace/mode/golang",
69 | "enableLinking": true,
70 | })
71 | v.editor.On("linkClick", func(d *js.Object) {
72 | data, ok := d.Interface().(map[string]interface{})
73 | if !ok {
74 | return
75 | }
76 | token, ok := data["token"].(map[string]interface{})
77 | if !ok {
78 | return
79 | }
80 | t, ok := token["type"].(string)
81 | if !ok {
82 | return
83 | }
84 | if t != "markup.underline" {
85 | return
86 | }
87 | value, ok := token["value"].(string)
88 | if !ok {
89 | return
90 | }
91 | if v.app.Source.HasFile(v.app.Editor.CurrentPackage(), value) {
92 | v.app.Dispatch(&actions.ChangeFile{
93 | Path: v.app.Editor.CurrentPackage(),
94 | Name: value,
95 | })
96 | }
97 | })
98 |
99 | dom.GetWindow().AddEventListener("resize", false, func(event dom.Event) {
100 | v.Resize()
101 | })
102 |
103 | v.editor.Get("renderer").Call("on", "afterRender", func() {
104 | v.Resize()
105 | })
106 |
107 | var last *struct{}
108 | v.editor.OnChange(func(ev *js.Object) {
109 | last = &struct{}{}
110 | before := last
111 | go func() {
112 | <-time.After(time.Millisecond * 250)
113 | if before == last {
114 | value := v.editor.GetValue()
115 | if value == v.app.Source.Current() {
116 | // don't fire event if text hasn't changed
117 | return
118 | }
119 | v.app.Dispatch(&actions.UserChangedText{
120 | Text: value,
121 | })
122 | }
123 | }()
124 | })
125 | }
126 |
127 | func (v *Editor) Resize() {
128 | if v.editor.Object != nil {
129 | v.editor.Call("resize")
130 | } else {
131 | v.app.Debug("************************************************************")
132 | v.app.Debug("*** skipped editor resize because v.editor.Object == nil ***")
133 | v.app.Debug("************************************************************")
134 | }
135 | }
136 |
137 | func (v *Editor) Unmount() {
138 | v.app.Delete(v)
139 | }
140 |
141 | func (v *Editor) Render() vecty.ComponentOrHTML {
142 |
143 | editorDisplay := "none"
144 | if len(v.app.Source.Packages()) > 0 && len(v.app.Source.Files(v.app.Editor.CurrentPackage())) > 0 {
145 | editorDisplay = ""
146 | }
147 |
148 | return elem.Div(
149 | vecty.Markup(
150 | prop.ID("editor"),
151 | vecty.Class("editor"),
152 | vecty.Style("display", editorDisplay),
153 | ),
154 | )
155 | }
156 |
--------------------------------------------------------------------------------
/views/help.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "github.com/dave/play/actions"
5 | "github.com/dave/play/models"
6 | "github.com/dave/play/stores"
7 | "github.com/gopherjs/vecty"
8 | "github.com/gopherjs/vecty/elem"
9 | "github.com/russross/blackfriday"
10 | )
11 |
12 | type HelpModal struct {
13 | *Modal
14 | }
15 |
16 | func NewHelpModal(app *stores.App) *HelpModal {
17 | v := &HelpModal{}
18 | v.Modal = &Modal{
19 | app: app,
20 | id: models.HelpModal,
21 | title: "Help",
22 | action: v.action,
23 | large: true,
24 | }
25 | return v
26 | }
27 |
28 | func (v *HelpModal) Render() vecty.ComponentOrHTML {
29 |
30 | // Render the markdown input into HTML using Blackfriday.
31 | unsafeHTML := blackfriday.MarkdownCommon([]byte(helpMarkdown))
32 |
33 | // Sanitize the HTML.
34 | //safeHTML := string(bluemonday.UGCPolicy().SanitizeBytes(unsafeHTML))
35 |
36 | return v.Body(
37 | elem.Div(
38 | vecty.Markup(
39 | vecty.UnsafeHTML(string(unsafeHTML)),
40 | ),
41 | ),
42 | ).Build()
43 | }
44 |
45 | func (v *HelpModal) action(*vecty.Event) {
46 | v.app.Dispatch(&actions.ModalClose{Modal: models.HelpModal})
47 | }
48 |
49 | var helpMarkdown = `
50 | Edit and run Go in the browser, supporting arbitrary import paths!
51 |
52 | https://play.jsgo.io/
53 |
54 | [ ](https://play.jsgo.io/)
55 |
56 | The jsgo playground is an extension of the jsgo compiler. The compiler allows you to easily compile Go
57 | to JS using GopherJS, and automatically host the results in an aggressively cached CDN. The playground
58 | adds an online editor and many other features (see below).
59 |
60 | The unique feature of the jsgo playground is that it supports arbitrary import paths. Other Go playgrounds
61 | are limited to just the Go standard library.
62 |
63 | For more for more info:
64 |
65 | * jsgo compiler: https://github.com/dave/jsgo
66 | * jsgo playground: https://github.com/dave/play
67 |
68 | ## Demos
69 |
70 | Here's the simplest demo - it just writes to the console and to the page:
71 |
72 | * https://play.jsgo.io/github.com/dave/jstest
73 |
74 | Here's a couple of simple demos that accept files by drag and drop. The first compresses dropped files to
75 | a zip. The second compresses images to jpg. They use the Go standard library zip / image libraries, which
76 | work flawlessly in the browser:
77 |
78 | * https://play.jsgo.io/github.com/dave/zip
79 | * https://play.jsgo.io/github.com/dave/img
80 |
81 | The amazing ebiten 2D games library is a perfect example of the power of Go in the browser. Here's some
82 | demos:
83 |
84 | * https://play.jsgo.io/github.com/hajimehoshi/ebiten/examples/2048
85 | * https://play.jsgo.io/github.com/hajimehoshi/go-inovation
86 | * https://play.jsgo.io/github.com/hajimehoshi/ebiten/examples/flappy
87 |
88 | ## Contact
89 |
90 | If you'd like to chat more about the project, feel free to [add an issue](https://github.com/dave/play/issues),
91 | mention [@dave](https://github.com/dave/) or post in the #gopherjs channel of the Gophers Slack. I'm
92 | happy to help!
93 |
94 | ## Features
95 |
96 | #### Initialise
97 | The URL can be used to initialise with code in several ways:
98 |
99 | * Load a Go package with ` + "`" + `/{{ Package path }}` + "`" + `
100 | * Load a Github Gist with ` + "`" + `/gist.github.com/{{ Gist ID }}` + "`" + `
101 | * Load a shared project with ` + "`" + `/{{ Share ID }}` + "`" + `
102 | * Load a ` + "`" + `play.golang.org` + "`" + ` share with ` + "`" + `/p/{{ Go playground ID }}` + "`" + `
103 |
104 |
105 |
106 | #### Run
107 | Click the ` + "`" + `Run` + "`" + ` button to run your code in the right-hand panel. If the imports have been changed recently,
108 | the dependencies will be refreshed before running.
109 |
110 |
111 |
112 |
113 |
114 | #### Format code
115 | Use the ` + "`" + `Format code` + "`" + ` option to run ` + "`" + `gofmt` + "`" + ` on your code. This is executed automatically when the ` + "`" + `Run` + "`" + `,
116 | ` + "`" + `Update` + "`" + `, ` + "`" + `Share` + "`" + ` or ` + "`" + `Deploy` + "`" + ` features are used.
117 |
118 |
119 |
120 |
121 |
122 | #### Update
123 | If you update a dependency, use the ` + "`" + `Update` + "`" + ` option, which does the equivalent of ` + "`" + `go get -u` + "`" + ` and refreshes
124 | the changes in any import or dependency.
125 |
126 |
127 |
128 |
129 |
130 | #### Share
131 | To share your project with others, use the ` + "`" + `Share` + "`" + ` option. Your project will be persisted to a json file
132 | on ` + "`" + `src.jsgo.io` + "`" + ` and the page will update to a sharable URL.
133 |
134 |
135 |
136 |
137 |
138 | #### Deploy
139 | To deploy your code to [jsgo.io](https://jsgo.io), use the ` + "`" + `Deploy` + "`" + ` feature. A modal will be displayed with the
140 | link to the page on ` + "`" + `jsgo.io` + "`" + `, and the Loader JS on ` + "`" + `pkg.jsgo.io` + "`" + `.
141 |
142 | Use the ` + "`" + `jsgo.io` + "`" + ` link for testing and toy projects. Remember you're sharing the ` + "`" + `jsgo.io` + "`" + ` domain with
143 | everyone else, so the browser environment should be considered toxic.
144 |
145 | The Loader JS on ` + "`" + `pkg.jsgo.io` + "`" + ` can be used in production, and should be added to a script tag on your
146 | own website. See [github.com/dave/jsgo](https://github.com/dave/jsgo) for more information.
147 |
148 |
149 |
150 |
151 |
152 | #### Console
153 | Writes to ` + "`" + `os.Stdout` + "`" + ` are redirected to a playground console, which can be toggled using the ` + "`" + `Show console` + "`" + `
154 | option. The console will automatically appear the first time it's written to.
155 |
156 |
157 |
158 |
159 |
160 | #### Minify
161 | In normal usage, all JS is minified. For debugging, this can be toggled with the ` + "`" + `Minify JS` + "`" + ` option.
162 |
163 |
164 |
165 |
166 |
167 | #### Build tags
168 | The build tags used when compiling can be edited with the ` + "`" + `Build tags...` + "`" + ` option. The selected build
169 | tags are persisted when using the ` + "`" + `Share` + "`" + ` feature.
170 |
171 |
172 |
173 |
174 |
175 | #### Download
176 | The ` + "`" + `Download` + "`" + ` option downloads the project. Single file projects are downloaded as a single file, while
177 | multi-file projects download as a zip.
178 |
179 |
180 |
181 |
182 |
183 | #### Upload
184 | Files can be uploaded to the project simply by drag+drop. Zip files generated by the ` + "`" + `Download` + "`" + ` feature
185 | can be uploaded to restore a multi-file project.
186 |
187 |
188 |
189 |
190 |
191 | #### File menu
192 | Change the selected file with the file menu.
193 |
194 |
195 |
196 |
197 |
198 | #### Add file
199 | Add a file to the current package with the ` + "`" + `Add file` + "`" + ` option. Only ` + "`" + `.go` + "`" + `, ` + "`" + `.md` + "`" + ` and ` + "`" + `.inc.js` + "`" + ` files are
200 | supported. If no extension is supplied, ` + "`" + `.go` + "`" + ` is added.
201 |
202 |
203 |
204 |
205 |
206 | #### Delete file
207 | Delete a file from the current package with the ` + "`" + `Delete file` + "`" + ` option.
208 |
209 |
210 |
211 |
212 |
213 | #### Package menu
214 | Change the selected package with the package menu.
215 |
216 |
217 |
218 |
219 |
220 | #### Add package
221 | Add an empty package with the ` + "`" + `Add package` + "`" + ` option.
222 |
223 |
224 |
225 |
226 |
227 | #### Load package
228 | The source for an import or dependency can be loaded with the ` + "`" + `Load package` + "`" + ` option. By default, only
229 | the direct imports of your project are listed. Use the ` + "`" + `Show all dependencies` + "`" + ` option to show the entire
230 | dependency tree.
231 |
232 |
233 |
234 |
235 |
236 | #### Remove package
237 | A package can be removed with the ` + "`" + `Remove package` + "`" + ` option.
238 | `
239 |
--------------------------------------------------------------------------------
/views/load-package.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "sort"
5 |
6 | "github.com/dave/play/actions"
7 | "github.com/dave/play/models"
8 | "github.com/dave/play/stores"
9 | "github.com/gopherjs/vecty"
10 | "github.com/gopherjs/vecty/elem"
11 | "github.com/gopherjs/vecty/event"
12 | "github.com/gopherjs/vecty/prop"
13 | )
14 |
15 | type LoadPackageModal struct {
16 | *Modal
17 | imps *vecty.HTML
18 | }
19 |
20 | func NewLoadPackageModal(app *stores.App) *LoadPackageModal {
21 | v := &LoadPackageModal{}
22 | v.Modal = &Modal{
23 | app: app,
24 | id: models.LoadPackageModal,
25 | title: "Load package",
26 | action: v.action,
27 | hidden: func() {
28 | app.Dispatch(&actions.ShowAllDepsChange{State: false})
29 | },
30 | }
31 | return v
32 | }
33 |
34 | func (v *LoadPackageModal) Render() vecty.ComponentOrHTML {
35 | items := []vecty.MarkupOrChild{
36 | vecty.Markup(
37 | vecty.Class("form-control"),
38 | prop.ID("load-package-imps-select"),
39 | ),
40 | }
41 | var paths []string
42 | if !v.app.Page.ShowAllDeps() {
43 | paths = v.app.Scanner.AllImportsOrdered()
44 | } else {
45 | imps := v.app.Scanner.AllImports()
46 | for p := range imps {
47 | paths = append(paths, p)
48 | }
49 | for p := range v.app.Archive.Cache() {
50 | if !imps[p] {
51 | paths = append(paths, p)
52 | }
53 | }
54 | sort.Strings(paths)
55 | }
56 | for _, path := range paths {
57 | items = append(items,
58 | elem.Option(
59 | vecty.Markup(
60 | prop.Value(path),
61 | ),
62 | vecty.Text(path),
63 | ),
64 | )
65 | }
66 | v.imps = elem.Select(items...)
67 |
68 | infoDisplay := "none"
69 | if v.app.Page.ShowAllDeps() && !v.app.Archive.AllFresh() {
70 | infoDisplay = ""
71 | }
72 |
73 | return v.Body(
74 | elem.Form(
75 | elem.Div(
76 | vecty.Markup(
77 | vecty.Class("form-group"),
78 | ),
79 | elem.Label(
80 | vecty.Markup(
81 | vecty.Property("for", "load-package-imps-select"),
82 | vecty.Class("col-form-label"),
83 | ),
84 | vecty.Text("Imports"),
85 | ),
86 | v.imps,
87 | ),
88 | elem.Div(
89 | vecty.Markup(
90 | vecty.Class("form-check"),
91 | ),
92 | elem.Input(
93 | vecty.Markup(
94 | prop.Type(prop.TypeCheckbox),
95 | vecty.Class("form-check-input"),
96 | prop.ID("load-package-show-all-deps-checkbox"),
97 | prop.Checked(v.app.Page.ShowAllDeps()),
98 | event.Click(func(e *vecty.Event) {
99 | v.app.Dispatch(&actions.ShowAllDepsChange{
100 | State: e.Target.Get("checked").Bool(),
101 | })
102 | }).PreventDefault(),
103 | ),
104 | ),
105 | elem.Label(
106 | vecty.Markup(
107 | vecty.Class("form-check-label"),
108 | prop.For("load-package-show-all-deps-checkbox"),
109 | ),
110 | vecty.Text("Show all dependencies"),
111 | ),
112 | ),
113 | elem.Div(
114 | vecty.Markup(
115 | vecty.Class("form-text"),
116 | vecty.Style("display", infoDisplay),
117 | ),
118 | vecty.Tag(
119 | "svg",
120 | vecty.Markup(
121 | vecty.Namespace("http://www.w3.org/2000/svg"),
122 | vecty.Attribute("width", "14"),
123 | vecty.Attribute("height", "16"),
124 | vecty.Attribute("viewBox", "0 0 14 16"),
125 | vecty.Class("octicon"),
126 | vecty.Style("margin-right", "4px"),
127 | ),
128 | vecty.Tag(
129 | "path",
130 | vecty.Markup(
131 | vecty.Namespace("http://www.w3.org/2000/svg"),
132 | vecty.Attribute("fill-rule", "evenodd"),
133 | vecty.Attribute("d", "M6.3 5.71a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 8.01c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V8v.01zM7 2.32C3.86 2.32 1.3 4.86 1.3 8c0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 1c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"),
134 | ),
135 | ),
136 | ),
137 | vecty.Text("Dependencies are not fully loaded. "),
138 | elem.Anchor(
139 | vecty.Markup(
140 | prop.Href(""),
141 | event.Click(func(e *vecty.Event) {
142 | v.app.Dispatch(&actions.ModalClose{Modal: models.LoadPackageModal})
143 | v.app.Dispatch(&actions.RequestStart{Type: models.UpdateRequest})
144 | }).PreventDefault(),
145 | ),
146 | vecty.Text("Update"),
147 | ),
148 | vecty.Text(" to load."),
149 | ),
150 | ),
151 | ).Build()
152 | }
153 |
154 | func (v *LoadPackageModal) action(*vecty.Event) {
155 | n := v.imps.Node()
156 | i := n.Get("selectedIndex").Int()
157 | value := n.Get("options").Index(i).Get("value").String()
158 | v.app.Dispatch(&actions.ModalClose{Modal: models.LoadPackageModal})
159 | v.app.Dispatch(&actions.RequestStart{Type: models.GetRequest, Path: value})
160 | }
161 |
--------------------------------------------------------------------------------
/views/menu.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/dave/play/actions"
7 | "github.com/dave/play/models"
8 | "github.com/dave/play/stores"
9 | "github.com/gopherjs/vecty"
10 | "github.com/gopherjs/vecty/elem"
11 | "github.com/gopherjs/vecty/event"
12 | "github.com/gopherjs/vecty/prop"
13 | )
14 |
15 | type Menu struct {
16 | vecty.Core
17 | app *stores.App
18 |
19 | compileButton *vecty.HTML
20 | optionsButton *vecty.HTML
21 | }
22 |
23 | func NewMenu(app *stores.App) *Menu {
24 | v := &Menu{
25 | app: app,
26 | }
27 | return v
28 | }
29 |
30 | func (v *Menu) Render() vecty.ComponentOrHTML {
31 |
32 | clashWarningDisplay := "none"
33 | if len(v.app.Scanner.Clashes()) > 0 {
34 | clashWarningDisplay = ""
35 | }
36 |
37 | buildTagsText := "Build tags..."
38 | if len(v.app.Compile.Tags()) > 0 {
39 | buildTagsText = fmt.Sprintf("Build tags (%d)...", len(v.app.Compile.Tags()))
40 | }
41 |
42 | return elem.Navigation(
43 | vecty.Markup(
44 | vecty.Class("menu", "navbar", "navbar-expand", "navbar-light", "bg-light"),
45 | ),
46 | elem.UnorderedList(
47 | vecty.Markup(
48 | vecty.Class("navbar-nav", "mr-auto"),
49 | ),
50 | v.renderPackageDropdown(),
51 | v.renderFileDropdown(),
52 |
53 | elem.ListItem(
54 | vecty.Markup(
55 | vecty.Class("nav-item"),
56 | vecty.Style("display", clashWarningDisplay),
57 | ),
58 | elem.Anchor(
59 | vecty.Markup(
60 | prop.Href(""),
61 | vecty.Class("nav-link", "octicon"),
62 | event.Click(func(e *vecty.Event) {
63 | v.app.Dispatch(&actions.ModalOpen{Modal: models.ClashWarningModal})
64 | }).PreventDefault(),
65 | ),
66 | vecty.Tag(
67 | "svg",
68 | vecty.Markup(
69 | vecty.Namespace("http://www.w3.org/2000/svg"),
70 | vecty.Attribute("width", "14"),
71 | vecty.Attribute("height", "16"),
72 | vecty.Attribute("viewBox", "0 0 14 16"),
73 | ),
74 | vecty.Tag(
75 | "path",
76 | vecty.Markup(
77 | vecty.Namespace("http://www.w3.org/2000/svg"),
78 | vecty.Attribute("fill-rule", "evenodd"),
79 | vecty.Attribute("d", "M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"),
80 | ),
81 | ),
82 | ),
83 | ),
84 | ),
85 | ),
86 | elem.UnorderedList(
87 | vecty.Markup(
88 | vecty.Class("navbar-nav", "ml-auto"),
89 | ),
90 | elem.ListItem(
91 | vecty.Markup(
92 | vecty.Class("nav-item"),
93 | ),
94 | elem.Span(
95 | vecty.Markup(
96 | vecty.Class("navbar-text"),
97 | vecty.Style("margin-right", "10px"),
98 | prop.ID("message"),
99 | ),
100 | vecty.Text(""),
101 | ),
102 | ),
103 | /*
104 | elem.ListItem(
105 | vecty.Markup(
106 | vecty.Class("nav-item"),
107 | ),
108 | elem.Anchor(
109 | vecty.Markup(
110 | prop.Href(""),
111 | vecty.Class("nav-link", "octicon"),
112 | event.Click(func(e *vecty.Event) {
113 | v.app.Dispatch(&actions.ModalOpen{Modal: models.HelpModal})
114 | }).PreventDefault(),
115 | ),
116 | vecty.Tag(
117 | "svg",
118 | vecty.Markup(
119 | vecty.Namespace("http://www.w3.org/2000/svg"),
120 | vecty.Attribute("width", "14"),
121 | vecty.Attribute("height", "16"),
122 | vecty.Attribute("viewBox", "0 0 14 16"),
123 | ),
124 | vecty.Tag(
125 | "path",
126 | vecty.Markup(
127 | vecty.Namespace("http://www.w3.org/2000/svg"),
128 | vecty.Attribute("fill-rule", "evenodd"),
129 | vecty.Attribute("d", "M6 10h2v2H6v-2zm4-3.5C10 8.64 8 9 8 9H6c0-.55.45-1 1-1h.5c.28 0 .5-.22.5-.5v-1c0-.28-.22-.5-.5-.5h-1c-.28 0-.5.22-.5.5V7H4c0-1.5 1.5-3 3-3s3 1 3 2.5zM7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7z"),
130 | ),
131 | ),
132 | ),
133 | ),
134 | ),
135 | */
136 | elem.ListItem(
137 | vecty.Markup(
138 | vecty.Class("nav-item", "btn-group"),
139 | ),
140 | elem.Button(
141 | vecty.Markup(
142 | vecty.Property("type", "button"),
143 | vecty.Class("btn", "btn-primary"),
144 | event.Click(func(e *vecty.Event) {
145 | if v.app.Connection.Open() || v.app.Compile.Compiling() {
146 | return
147 | } else {
148 | v.app.Dispatch(&actions.FormatCode{
149 | Then: &actions.CompileStart{},
150 | })
151 | }
152 | }).PreventDefault(),
153 | ),
154 | vecty.Text("Run"),
155 | ),
156 | elem.Button(
157 | vecty.Markup(
158 | vecty.Property("type", "button"),
159 | vecty.Data("toggle", "dropdown"),
160 | vecty.Property("aria-haspopup", "true"),
161 | vecty.Property("aria-expanded", "false"),
162 | vecty.Class("btn", "btn-primary", "dropdown-toggle", "dropdown-toggle-split"),
163 | ),
164 | elem.Span(vecty.Markup(vecty.Class("sr-only")), vecty.Text("Options")),
165 | ),
166 | elem.Div(
167 | vecty.Markup(
168 | vecty.Class("dropdown-menu", "dropdown-menu-right"),
169 | ),
170 | elem.Anchor(
171 | vecty.Markup(
172 | vecty.Class("dropdown-item"),
173 | prop.Href(""),
174 | event.Click(func(e *vecty.Event) {
175 | v.app.Dispatch(&actions.FormatCode{})
176 | }).PreventDefault(),
177 | ),
178 | vecty.Text("Format code"),
179 | ),
180 | elem.Div(
181 | vecty.Markup(
182 | vecty.Class("dropdown-divider"),
183 | ),
184 | ),
185 | elem.Anchor(
186 | vecty.Markup(
187 | vecty.Class("dropdown-item"),
188 | prop.Href(""),
189 | event.Click(func(e *vecty.Event) {
190 | v.app.Dispatch(&actions.FormatCode{
191 | Then: &actions.RequestStart{Type: models.UpdateRequest},
192 | })
193 | }).PreventDefault(),
194 | ),
195 | vecty.Text("Update"),
196 | ),
197 | elem.Anchor(
198 | vecty.Markup(
199 | vecty.Class("dropdown-item"),
200 | prop.Href(""),
201 | event.Click(func(e *vecty.Event) {
202 | v.app.Dispatch(&actions.FormatCode{
203 | Then: &actions.ShareStart{},
204 | })
205 | }).PreventDefault(),
206 | ),
207 | vecty.Text("Share"),
208 | ),
209 | elem.Anchor(
210 | vecty.Markup(
211 | vecty.Class("dropdown-item"),
212 | prop.Href(""),
213 | event.Click(func(e *vecty.Event) {
214 | v.app.Dispatch(&actions.FormatCode{
215 | Then: &actions.DeployStart{},
216 | })
217 | }).PreventDefault(),
218 | ),
219 | vecty.Text("Deploy"),
220 | ),
221 | elem.Div(
222 | vecty.Markup(
223 | vecty.Class("dropdown-divider"),
224 | ),
225 | ),
226 | elem.Anchor(
227 | vecty.Markup(
228 | vecty.Class("dropdown-item"),
229 | prop.Href("#"),
230 | event.Click(func(e *vecty.Event) {}).StopPropagation(),
231 | ),
232 | elem.Input(
233 | vecty.Markup(
234 | prop.Type(prop.TypeCheckbox),
235 | vecty.Class("form-check-input", "dropdown-item"),
236 | prop.ID("dropdownCheckConsole"),
237 | prop.Checked(v.app.Page.Console()),
238 | event.Change(func(e *vecty.Event) {
239 | v.app.Dispatch(&actions.ConsoleToggleClick{})
240 | }),
241 | vecty.Style("cursor", "pointer"),
242 | ),
243 | ),
244 | elem.Label(
245 | vecty.Markup(
246 | vecty.Class("form-check-label"),
247 | prop.For("dropdownCheckConsole"),
248 | vecty.Style("cursor", "pointer"),
249 | ),
250 | vecty.Text("Show console"),
251 | ),
252 | ),
253 | elem.Anchor(
254 | vecty.Markup(
255 | vecty.Class("dropdown-item"),
256 | prop.Href("#"),
257 | event.Click(func(e *vecty.Event) {}).StopPropagation(),
258 | ),
259 | elem.Input(
260 | vecty.Markup(
261 | prop.Type(prop.TypeCheckbox),
262 | vecty.Class("form-check-input", "dropdown-item"),
263 | prop.ID("dropdownCheckMinify"),
264 | prop.Checked(v.app.Page.Minify()),
265 | event.Change(func(e *vecty.Event) {
266 | v.app.Dispatch(&actions.MinifyToggleClick{})
267 | }),
268 | vecty.Style("cursor", "pointer"),
269 | ),
270 | ),
271 | elem.Label(
272 | vecty.Markup(
273 | vecty.Class("form-check-label"),
274 | prop.For("dropdownCheckMinify"),
275 | vecty.Style("cursor", "pointer"),
276 | ),
277 | vecty.Text("Minify JS"),
278 | ),
279 | ),
280 | elem.Anchor(
281 | vecty.Markup(
282 | vecty.Class("dropdown-item"),
283 | prop.Href(""),
284 | event.Click(func(e *vecty.Event) {
285 | v.app.Dispatch(&actions.ModalOpen{Modal: models.BuildTagsModal})
286 | }).PreventDefault(),
287 | ),
288 | vecty.Text(buildTagsText),
289 | ),
290 | elem.Div(
291 | vecty.Markup(
292 | vecty.Class("dropdown-divider"),
293 | ),
294 | ),
295 | elem.Anchor(
296 | vecty.Markup(
297 | vecty.Class("dropdown-item"),
298 | prop.Href(""),
299 | event.Click(func(e *vecty.Event) {
300 | v.app.Dispatch(&actions.DownloadClick{})
301 | }).PreventDefault(),
302 | ),
303 | vecty.Text("Download"),
304 | ),
305 | elem.Div(
306 | vecty.Markup(
307 | vecty.Class("dropdown-divider"),
308 | ),
309 | ),
310 | elem.Anchor(
311 | vecty.Markup(
312 | vecty.Class("dropdown-item"),
313 | event.Click(func(e *vecty.Event) {
314 | v.app.Dispatch(&actions.ModalOpen{Modal: models.HelpModal})
315 | }).PreventDefault(),
316 | ),
317 | vecty.Text("Help"),
318 | ),
319 | elem.Anchor(
320 | vecty.Markup(
321 | vecty.Class("dropdown-item"),
322 | prop.Href("https://github.com/dave/play"),
323 | vecty.Property("target", "_blank"),
324 | ),
325 | vecty.Text("More info"),
326 | ),
327 | elem.Anchor(
328 | vecty.Markup(
329 | vecty.Class("dropdown-item"),
330 | prop.Href("https://patreon.com/davebrophy"),
331 | vecty.Property("target", "_blank"),
332 | ),
333 | vecty.Text("Help with my hosting bills"),
334 | ),
335 | ),
336 | ),
337 | ),
338 | )
339 | }
340 |
341 | func (v *Menu) renderFileDropdown() *vecty.HTML {
342 | var fileItems []vecty.MarkupOrChild
343 | fileItems = append(fileItems,
344 | vecty.Markup(
345 | vecty.Class("dropdown-menu"),
346 | vecty.Property("aria-labelledby", "fileDropdown"),
347 | ),
348 | )
349 | for _, name := range v.app.Source.Filenames(v.app.Editor.CurrentPackage()) {
350 | name := name
351 | fileItems = append(fileItems,
352 | elem.Anchor(
353 | vecty.Markup(
354 | vecty.Class("dropdown-item"),
355 | vecty.ClassMap{
356 | "disabled": name == v.app.Editor.CurrentFile(),
357 | },
358 | prop.Href(""),
359 | event.Click(func(e *vecty.Event) {
360 | v.app.Dispatch(&actions.UserChangedFile{
361 | Name: name,
362 | })
363 | }).PreventDefault(),
364 | ),
365 | vecty.Text(name),
366 | ),
367 | )
368 | }
369 | fileItems = append(fileItems,
370 | elem.Div(
371 | vecty.Markup(
372 | vecty.Class("dropdown-divider"),
373 | ),
374 | ),
375 | elem.Anchor(
376 | vecty.Markup(
377 | vecty.Class("dropdown-item"),
378 | prop.Href(""),
379 | event.Click(func(e *vecty.Event) {
380 | v.app.Dispatch(&actions.ModalOpen{Modal: models.AddFileModal})
381 | }).PreventDefault(),
382 | ),
383 | vecty.Text("Add file"),
384 | ),
385 | elem.Anchor(
386 | vecty.Markup(
387 | vecty.Class("dropdown-item"),
388 | prop.Href(""),
389 | event.Click(func(e *vecty.Event) {
390 | v.app.Dispatch(&actions.ModalOpen{Modal: models.DeleteFileModal})
391 | }).PreventDefault(),
392 | ),
393 | vecty.Text("Delete file"),
394 | ),
395 | )
396 |
397 | classes := vecty.Class("nav-item", "dropdown", "d-none")
398 | if len(v.app.Source.Files(v.app.Editor.CurrentPackage())) > 0 {
399 | classes = vecty.Class("nav-item", "dropdown")
400 | }
401 |
402 | return elem.ListItem(
403 | vecty.Markup(
404 | classes,
405 | ),
406 | elem.Anchor(
407 | vecty.Markup(
408 | prop.ID("fileDropdown"),
409 | prop.Href(""),
410 | vecty.Class("nav-link", "dropdown-toggle"),
411 | vecty.Property("role", "button"),
412 | vecty.Data("toggle", "dropdown"),
413 | vecty.Property("aria-haspopup", "true"),
414 | vecty.Property("aria-expanded", "false"),
415 | event.Click(func(ev *vecty.Event) {}).PreventDefault(),
416 | ),
417 | vecty.Text(v.app.Editor.CurrentFile()),
418 | ),
419 | elem.Div(
420 | fileItems...,
421 | ),
422 | )
423 | }
424 |
425 | func (v *Menu) renderPackageDropdown() *vecty.HTML {
426 | var packageItems []vecty.MarkupOrChild
427 | packageItems = append(packageItems,
428 | vecty.Markup(
429 | vecty.Class("dropdown-menu"),
430 | vecty.Property("aria-labelledby", "packageDropdown"),
431 | ),
432 | )
433 | for _, path := range v.app.Source.Packages() {
434 | path := path
435 | packageItems = append(packageItems,
436 | elem.Anchor(
437 | vecty.Markup(
438 | vecty.Class("dropdown-item"),
439 | vecty.ClassMap{
440 | "disabled": path == v.app.Editor.CurrentPackage(),
441 | },
442 | prop.Href(""),
443 | event.Click(func(e *vecty.Event) {
444 | v.app.Dispatch(&actions.UserChangedPackage{
445 | Path: path,
446 | })
447 | }).PreventDefault(),
448 | ),
449 | vecty.Text(v.app.Scanner.DisplayPath(path)),
450 | ),
451 | )
452 | }
453 | packageItems = append(packageItems,
454 | elem.Div(
455 | vecty.Markup(
456 | vecty.Class("dropdown-divider"),
457 | ),
458 | ),
459 | elem.Anchor(
460 | vecty.Markup(
461 | vecty.Class("dropdown-item"),
462 | prop.Href(""),
463 | event.Click(func(e *vecty.Event) {
464 | v.app.Dispatch(&actions.ModalOpen{Modal: models.AddPackageModal})
465 | }).PreventDefault(),
466 | ),
467 | vecty.Text("Add package"),
468 | ),
469 | elem.Anchor(
470 | vecty.Markup(
471 | vecty.Class("dropdown-item"),
472 | prop.Href(""),
473 | event.Click(func(e *vecty.Event) {
474 | v.app.Dispatch(&actions.ModalOpen{Modal: models.LoadPackageModal})
475 | }).PreventDefault(),
476 | ),
477 | vecty.Text("Load package"),
478 | ),
479 | elem.Anchor(
480 | vecty.Markup(
481 | vecty.Class("dropdown-item"),
482 | prop.Href(""),
483 | event.Click(func(e *vecty.Event) {
484 | v.app.Dispatch(&actions.ModalOpen{Modal: models.RemovePackageModal})
485 | }).PreventDefault(),
486 | ),
487 | vecty.Text("Remove package"),
488 | ),
489 | )
490 |
491 | classes := vecty.Class("nav-item", "dropdown", "d-none")
492 | if len(v.app.Source.Packages()) > 0 {
493 | classes = vecty.Class("nav-item", "dropdown")
494 | }
495 |
496 | return elem.ListItem(
497 | vecty.Markup(
498 | classes,
499 | ),
500 | elem.Anchor(
501 | vecty.Markup(
502 | prop.ID("packageDropdown"),
503 | prop.Href(""),
504 | vecty.Class("nav-link", "dropdown-toggle"),
505 | vecty.Property("role", "button"),
506 | vecty.Data("toggle", "dropdown"),
507 | vecty.Property("aria-haspopup", "true"),
508 | vecty.Property("aria-expanded", "false"),
509 | event.Click(func(ev *vecty.Event) {}).PreventDefault(),
510 | ),
511 | vecty.Text(v.app.Scanner.DisplayName(v.app.Editor.CurrentPackage())),
512 | ),
513 | elem.Div(
514 | packageItems...,
515 | ),
516 | )
517 | }
518 |
--------------------------------------------------------------------------------
/views/modal.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "github.com/dave/play/actions"
5 | "github.com/dave/play/models"
6 | "github.com/dave/play/stores"
7 | "github.com/gopherjs/gopherjs/js"
8 | "github.com/gopherjs/vecty"
9 | "github.com/gopherjs/vecty/elem"
10 | "github.com/gopherjs/vecty/event"
11 | "github.com/gopherjs/vecty/prop"
12 | )
13 |
14 | type Modal struct {
15 | vecty.Core
16 | app *stores.App
17 | id models.Modal
18 | title string
19 | action func(*vecty.Event)
20 | body []vecty.MarkupOrChild // This should be set on every Render
21 | shown func()
22 | hidden func()
23 | large bool
24 | }
25 |
26 | func (m *Modal) Mount() {
27 | m.app.Watch(m, func(done chan struct{}) {
28 | defer close(done)
29 | modalIsVisible := js.Global.Call("$", "#"+m.id).Call("hasClass", "show").Bool()
30 | shouldBeVisible := m.app.Page.ModalOpen(m.id)
31 | if modalIsVisible != shouldBeVisible {
32 | if shouldBeVisible {
33 | js.Global.Call("$", "#"+m.id).Call("modal", "show")
34 | } else {
35 | js.Global.Call("$", "#"+m.id).Call("modal", "hide")
36 | }
37 | }
38 | })
39 | js.Global.Call("$", "#"+m.id).Call("on", "shown.bs.modal", func() {
40 | if !m.app.Page.ModalOpen(m.id) {
41 | m.app.Dispatch(&actions.ModalOpen{Modal: m.id})
42 | }
43 | if m.shown != nil {
44 | m.shown()
45 | }
46 | })
47 | js.Global.Call("$", "#"+m.id).Call("on", "hidden.bs.modal", func() {
48 | if m.app.Page.ModalOpen(m.id) {
49 | m.app.Dispatch(&actions.ModalClose{Modal: m.id})
50 | }
51 | if m.hidden != nil {
52 | m.hidden()
53 | }
54 | })
55 | }
56 |
57 | func (m *Modal) Unmount() {
58 | m.app.Delete(m)
59 | js.Global.Call("$", "#"+m.id).Call("unbind")
60 | }
61 |
62 | func (m *Modal) Body(body ...vecty.MarkupOrChild) *Modal {
63 | m.body = body
64 | return m
65 | }
66 |
67 | func (m *Modal) Build() vecty.ComponentOrHTML {
68 |
69 | body := []vecty.MarkupOrChild{
70 | vecty.Markup(
71 | vecty.Class("modal-body"),
72 | ),
73 | }
74 | body = append(body, m.body...)
75 |
76 | okDisplay := ""
77 | if m.action == nil {
78 | okDisplay = "none"
79 | }
80 |
81 | modalClass := vecty.Class("modal-dialog")
82 | if m.large {
83 | modalClass = vecty.Class("modal-dialog", "modal-lg")
84 | }
85 |
86 | return elem.Div(
87 | vecty.Markup(
88 | prop.ID(string(m.id)),
89 | vecty.Class("modal"),
90 | vecty.Property("tabindex", "-1"),
91 | vecty.Property("role", "dialog"),
92 | ),
93 | elem.Div(
94 | vecty.Markup(
95 | modalClass,
96 | vecty.Property("role", "dialog"),
97 | ),
98 | elem.Div(
99 | vecty.Markup(
100 | vecty.Class("modal-content"),
101 | ),
102 | elem.Div(
103 | vecty.Markup(
104 | vecty.Class("modal-header"),
105 | ),
106 | elem.Heading5(
107 | vecty.Markup(
108 | vecty.Class("modal-title"),
109 | ),
110 | vecty.Text(m.title),
111 | ),
112 | elem.Button(
113 | vecty.Markup(
114 | prop.Type(prop.TypeButton),
115 | vecty.Class("close"),
116 | vecty.Data("dismiss", "modal"),
117 | vecty.Property("aria-label", "Close"),
118 | ),
119 | elem.Span(
120 | vecty.Markup(
121 | vecty.Property("aria-hidden", "true"),
122 | ),
123 | vecty.Text("×"),
124 | ),
125 | ),
126 | ),
127 | elem.Div(
128 | body...,
129 | ),
130 | elem.Div(
131 | vecty.Markup(
132 | vecty.Class("modal-footer"),
133 | ),
134 | elem.Button(
135 | vecty.Markup(
136 | prop.Type(prop.TypeButton),
137 | vecty.Class("btn", "btn-primary"),
138 | event.Click(m.action).PreventDefault(),
139 | vecty.Style("display", okDisplay),
140 | ),
141 | vecty.Text("OK"),
142 | ),
143 | elem.Button(
144 | vecty.Markup(
145 | prop.Type(prop.TypeButton),
146 | vecty.Class("btn", "btn-secondary"),
147 | vecty.Data("dismiss", "modal"),
148 | ),
149 | vecty.Text("Close"),
150 | ),
151 | ),
152 | ),
153 | ),
154 | )
155 | }
156 |
--------------------------------------------------------------------------------
/views/page.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "github.com/dave/dropper"
5 | "github.com/dave/jsgo/config"
6 | "github.com/dave/play/actions"
7 | "github.com/dave/play/models"
8 | "github.com/dave/play/stores"
9 | "github.com/dave/splitter"
10 | "github.com/gopherjs/gopherjs/js"
11 | "github.com/gopherjs/vecty"
12 | "github.com/gopherjs/vecty/elem"
13 | "github.com/gopherjs/vecty/event"
14 | "github.com/gopherjs/vecty/prop"
15 | "honnef.co/go/js/dom"
16 | )
17 |
18 | type Page struct {
19 | vecty.Core
20 | app *stores.App
21 |
22 | split1, split2 *splitter.Split
23 | editor *Editor
24 | }
25 |
26 | func NewPage(app *stores.App) *Page {
27 | v := &Page{
28 | app: app,
29 | }
30 | return v
31 | }
32 |
33 | func (v *Page) Mount() {
34 | v.app.Watch(v, func(done chan struct{}) {
35 | defer close(done)
36 |
37 | sizes := v.app.Editor.Sizes()
38 | if v.split1.Changed(sizes) {
39 | v.split1.SetSizes(sizes)
40 | }
41 |
42 | if v.app.Page.Console() && !v.split2.Initialised() {
43 | v.split2.Init(
44 | js.S{"#iframe-holder", "#console-holder"},
45 | js.M{
46 | "direction": "vertical",
47 | "sizes": []float64{50.0, 50.0},
48 | },
49 | )
50 | } else if !v.app.Page.Console() && v.split2.Initialised() {
51 | v.split2.Destroy()
52 | }
53 | })
54 |
55 | v.split1 = splitter.New("split")
56 | v.split1.Init(
57 | js.S{"#left", "#right"},
58 | js.M{
59 | "sizes": v.app.Editor.Sizes(),
60 | "onDragEnd": func() {
61 | v.app.Dispatch(&actions.UserChangedSplit{
62 | Sizes: v.split1.GetSizes(),
63 | })
64 | },
65 | },
66 | )
67 |
68 | v.split2 = splitter.New("split")
69 |
70 | events := dropper.Initialise(dom.GetWindow().Document().GetElementByID("left"))
71 | go func() {
72 | for ev := range events {
73 | switch ev := ev.(type) {
74 | case dropper.EnterEvent:
75 | v.app.Dispatch(&actions.DragEnter{})
76 | case dropper.LeaveEvent:
77 | v.app.Dispatch(&actions.DragLeave{})
78 | case dropper.DropEvent:
79 | v.app.Dispatch(&actions.DragDrop{
80 | Files: ev,
81 | })
82 | }
83 | }
84 | }()
85 |
86 | }
87 |
88 | func (v *Page) Unmount() {
89 | v.app.Delete(v)
90 | }
91 |
92 | const Styles = `
93 | html, body {
94 | height: 100%;
95 | }
96 | #left {
97 | display: flex;
98 | flex-flow: column;
99 | height: 100%;
100 | }
101 | .menu {
102 | min-height: 56px;
103 | }
104 | .editor, .empty-panel {
105 | flex: 1;
106 | width: 100%;
107 | }
108 | .empty-panel {
109 | display: flex;
110 | align-items: center;
111 | justify-content: center;
112 | }
113 | .split {
114 | height: 100%;
115 | width: 100%;
116 | }
117 | .gutter {
118 | height: 100%;
119 | background-color: #eee;
120 | background-repeat: no-repeat;
121 | background-position: 50%;
122 | }
123 | .gutter.gutter-horizontal {
124 | cursor: col-resize;
125 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==')
126 | }
127 | .gutter.gutter-vertical {
128 | cursor: row-resize;
129 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=')
130 | }
131 | .split {
132 | -webkit-box-sizing: border-box;
133 | -moz-box-sizing: border-box;
134 | box-sizing: border-box;
135 | }
136 | .split, .gutter.gutter-horizontal {
137 | float: left;
138 | }
139 | .preview {
140 | border: 0;
141 | height: 100%;
142 | width: 100%;
143 | }
144 | #console-holder {
145 | overflow: auto;
146 | }
147 | #console {
148 | padding:5px;
149 | }
150 | .octicon {
151 | display: inline-block;
152 | vertical-align: text-top;
153 | fill: currentColor;
154 | }
155 | #help-modal table {
156 | clear: both;
157 | }
158 | #help-modal img {
159 | margin-left: 20px;
160 | margin-bottom: 30px;
161 | }
162 | #help-modal h2 {
163 | padding-bottom: 0.3em;
164 | font-size: 1.5em;
165 | border-bottom: 1px solid #eaecef;
166 |
167 | margin-top: 24px;
168 | margin-bottom: 16px;
169 | font-weight: 600;
170 | line-height: 1.25;
171 | }
172 | #help-modal h4 {
173 | font-size: 1em;
174 |
175 | margin-top: 24px;
176 | margin-bottom: 16px;
177 | font-weight: 600;
178 | line-height: 1.25;
179 | }
180 | #help-modal .modal-lg {
181 | max-width: 700px;
182 | }
183 | #help-modal a {
184 | color: #0366d6;
185 | }
186 | #help-modal code {
187 | padding: 0.2em 0.4em;
188 | margin: 0;
189 | font-size: 85%;
190 | background-color: rgba(27,31,35,0.05);
191 | border-radius: 3px;
192 | color: #24292e;
193 | }
194 | `
195 |
196 | func (v *Page) Render() vecty.ComponentOrHTML {
197 | githubBannerDisplay := ""
198 | if v.app.Compile.Compiled() {
199 | githubBannerDisplay = "none"
200 | }
201 | forkMe := "https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png"
202 | if config.LOCAL {
203 | forkMe = "/_local/forkme_right_gray_6d6d6d.png"
204 | }
205 | return elem.Body(
206 | elem.Div(
207 | vecty.Markup(
208 | vecty.Class("container-fluid", "p-0", "split", "split-horizontal"),
209 | ),
210 | v.renderLeft(),
211 | v.renderRight(),
212 | ),
213 | NewAddFileModal(v.app),
214 | NewDeleteFileModal(v.app),
215 | NewAddPackageModal(v.app),
216 | NewRemovePackageModal(v.app),
217 | NewDeployDoneModal(v.app),
218 | NewLoadPackageModal(v.app),
219 | NewClashWarningModal(v.app),
220 | NewBuildTagsModal(v.app),
221 | NewHelpModal(v.app),
222 | elem.Anchor(
223 | vecty.Markup(
224 | prop.Href("https://github.com/dave/play"),
225 | vecty.Style("display", githubBannerDisplay),
226 | vecty.Property("target", "_blank"),
227 | ),
228 | elem.Image(
229 | vecty.Markup(
230 | vecty.Style("position", "absolute"),
231 | vecty.Style("top", "0"),
232 | vecty.Style("right", "0"),
233 | vecty.Style("border", "0"),
234 | prop.Src(forkMe),
235 | vecty.Property("alt", "Fork me on GitHub"),
236 | ),
237 | ),
238 | ),
239 | )
240 | }
241 |
242 | func (v *Page) renderLeft() *vecty.HTML {
243 |
244 | v.editor = NewEditor(v.app)
245 |
246 | emptyDisplay := "none"
247 | addFileDisplay := "none"
248 | addPackageDisplay := "none"
249 | loadingDisplay := "none"
250 | if !v.app.Editor.Loaded() {
251 | emptyDisplay = ""
252 | loadingDisplay = ""
253 | } else if len(v.app.Source.Packages()) == 0 {
254 | emptyDisplay = ""
255 | addPackageDisplay = ""
256 | } else if len(v.app.Source.Files(v.app.Editor.CurrentPackage())) == 0 {
257 | emptyDisplay = ""
258 | addFileDisplay = ""
259 | }
260 |
261 | return elem.Div(
262 | vecty.Markup(
263 | prop.ID("left"),
264 | vecty.Class("split"),
265 | ),
266 | NewMenu(v.app),
267 | v.editor,
268 | elem.Div(
269 | vecty.Markup(
270 | vecty.Class("empty-panel"),
271 | vecty.Style("display", emptyDisplay),
272 | ),
273 | elem.Span(
274 | vecty.Markup(
275 | vecty.Style("display", loadingDisplay),
276 | ),
277 | vecty.Text("Loading..."),
278 | ),
279 | elem.Button(
280 | vecty.Markup(
281 | vecty.Property("type", "button"),
282 | vecty.Class("btn", "btn-primary"),
283 | event.Click(func(e *vecty.Event) {
284 | v.app.Dispatch(&actions.ModalOpen{Modal: models.AddFileModal})
285 | }).PreventDefault(),
286 | vecty.Style("display", addFileDisplay),
287 | ),
288 | vecty.Text("Add file"),
289 | ),
290 | elem.Button(
291 | vecty.Markup(
292 | vecty.Property("type", "button"),
293 | vecty.Class("btn", "btn-primary"),
294 | event.Click(func(e *vecty.Event) {
295 | v.app.Dispatch(&actions.ModalOpen{Modal: models.AddPackageModal})
296 | }).PreventDefault(),
297 | vecty.Style("display", addPackageDisplay),
298 | ),
299 | vecty.Text("Add package"),
300 | ),
301 | ),
302 | )
303 | }
304 |
305 | func (v *Page) renderRight() *vecty.HTML {
306 | consoleDisplay := ""
307 | if !v.app.Page.Console() {
308 | consoleDisplay = "none"
309 | }
310 | return elem.Div(
311 | vecty.Markup(
312 | prop.ID("right"),
313 | vecty.Class("split", "split-vertical"),
314 | ),
315 | elem.Div(
316 | vecty.Markup(
317 | prop.ID("iframe-holder"),
318 | ),
319 | ),
320 | elem.Div(
321 | vecty.Markup(
322 | prop.ID("console-holder"),
323 | vecty.Style("display", consoleDisplay),
324 | ),
325 | elem.Preformatted(
326 | vecty.Markup(
327 | prop.ID("console"),
328 | ),
329 | ),
330 | ),
331 | )
332 | }
333 |
--------------------------------------------------------------------------------
/views/remove-package.go:
--------------------------------------------------------------------------------
1 | package views
2 |
3 | import (
4 | "github.com/dave/play/actions"
5 | "github.com/dave/play/models"
6 | "github.com/dave/play/stores"
7 | "github.com/gopherjs/vecty"
8 | "github.com/gopherjs/vecty/elem"
9 | "github.com/gopherjs/vecty/prop"
10 | )
11 |
12 | type RemovePackageModal struct {
13 | *Modal
14 | sel *vecty.HTML
15 | }
16 |
17 | func NewRemovePackageModal(app *stores.App) *RemovePackageModal {
18 | v := &RemovePackageModal{}
19 | v.Modal = &Modal{
20 | app: app,
21 | id: models.RemovePackageModal,
22 | title: "Remove package",
23 | action: v.action,
24 | }
25 | return v
26 | }
27 |
28 | func (v *RemovePackageModal) Render() vecty.ComponentOrHTML {
29 | items := []vecty.MarkupOrChild{
30 | vecty.Markup(
31 | vecty.Class("form-control"),
32 | prop.ID("remove-package-select"),
33 | ),
34 | }
35 | for _, path := range v.app.Source.Packages() {
36 | items = append(items,
37 | elem.Option(
38 | vecty.Markup(
39 | prop.Value(path),
40 | vecty.Property("selected", v.app.Editor.CurrentPackage() == path),
41 | ),
42 | vecty.Text(path),
43 | ),
44 | )
45 | }
46 | v.sel = elem.Select(items...)
47 |
48 | return v.Body(
49 | elem.Form(
50 | elem.Div(
51 | vecty.Markup(
52 | vecty.Class("form-group"),
53 | ),
54 | elem.Label(
55 | vecty.Markup(
56 | vecty.Property("for", "remove-package-select"),
57 | vecty.Class("col-form-label"),
58 | ),
59 | vecty.Text("Package path"),
60 | ),
61 | v.sel,
62 | ),
63 | ),
64 | ).Build()
65 | }
66 |
67 | func (v *RemovePackageModal) action(*vecty.Event) {
68 | n := v.sel.Node()
69 | i := n.Get("selectedIndex").Int()
70 | value := n.Get("options").Index(i).Get("value").String()
71 | v.app.Dispatch(&actions.ModalClose{Modal: models.RemovePackageModal})
72 | v.app.Dispatch(&actions.RemovePackage{Path: value})
73 | }
74 |
--------------------------------------------------------------------------------