Provides a statically served and embedded version of Quercus.js, a lightweight and customizable JavaScript treeview library with no dependencies.
The embedded third-party files are vendored Quercus.js tree assets from
https://github.com/stefaneichert/quercus.js. The assets/jawstree.* files are
local adapter source in this repository and are covered by normal code review.
| File | Source |
|---|---|
assets/treeview.js |
Quercus.js from https://github.com/stefaneichert/quercus.js |
assets/treeview.css |
Quercus.js styles from https://github.com/stefaneichert/quercus.js |
When bumping the vendored Quercus files, update this table in the same change.
package main
import (
"embed"
"log/slog"
"net/http"
"sync"
"github.com/linkdata/jaws"
"github.com/linkdata/jaws/jawsboot"
"github.com/linkdata/jawstree"
"github.com/linkdata/jaws/lib/templatereloader"
"github.com/linkdata/jaws/lib/ui"
"github.com/linkdata/staticserve"
)
// This example assumes an 'assets' directory:
//
//. assets/
//. static/
//. images/
//. favicon.png
//. ui/
//. index.html
//go:embed assets
var assetsFS embed.FS
func setupJaws(jw *jaws.Jaws, mux *http.ServeMux) (err error) {
mux.Handle("GET /jaws/", jw) // Ensure the JaWS routes are handled
var tmpl jaws.TemplateLookuper
if tmpl, err = templatereloader.New(assetsFS, "assets/ui/*.html", ""); err == nil {
jw.AddTemplateLookuper(tmpl)
// Initialize jawsboot; we will serve the JavaScript and CSS from /static/*.[js|css].
// All files under assets/static will be available under /static. Any favicon loaded
// this way will have its URL available using jw.FaviconURL().
if err = jw.Setup(mux.Handle, "/static",
jawsboot.Setup,
jawstree.Setup,
staticserve.MustNewFS(assetsFS, "assets/static", "images/favicon.png"),
); err == nil {
var mu sync.RWMutex
root := &jawstree.Node{Children: []*jawstree.Node{
{Name: "Documents", Children: []*jawstree.Node{{Name: "report.pdf"}}},
{Name: "Pictures"},
}}
tree, terr := jawstree.New(&mu, root, jawstree.InitiallyExpanded)
if terr != nil {
return terr
}
mux.Handle("GET /", ui.Handler(jw, "index.html", tree))
}
}
return
}
func main() {
jw, err := jaws.New()
if err == nil {
jw.Logger = slog.Default()
if err = setupJaws(jw, http.DefaultServeMux); err == nil {
// start the JaWS processing loop and the HTTP server
go jw.Serve()
slog.Error(http.ListenAndServe("localhost:8080", nil).Error())
}
}
if err != nil {
panic(err)
}
}The example expects an assets directory in the source tree:
assets
├── static
│ └── images
│ └── favicon.png
└── ui
└── index.html
The examples use {{$.HeadHTML}} inside <head> to emit the configured
resources and Request key metadata. Applications that provide equivalent markup
may omit it. {{$.TailHTML}} is optional; placing it before the closing
</body> tag applies updates queued during initial rendering before the
WebSocket connects.
A Tree is shared, server-authoritative UI state and the jaws.UI that renders
it. Build it once before serving, then render that same *Tree for every request
that should show the same collaborative tree; it holds no per-request state, so
sharing it is safe.
New takes the lock guarding the tree (which may be shared with other application
state) and the root *Node. It returns ErrInvalidTree for an invalid graph
(nil, cyclic, shared node, unknown option bit, more than MaxTreeNodes nodes,
nesting deeper than MaxTreeDepth, or depth-weighted serialized node data
exceeding MaxTreeRenderBytes) and ErrInvalidSelection when the initial
Selected flags violate the mode's policy. It assigns each node's positional-path
ID, a preorder wire index, and the parent back-pointers the name-path API needs, so
it must run before rendering.
Once New returns, only the selection may change, through Tree.SetSelected or
browser events. Each node's Name, Disabled, assigned ID, and the topology
(Children) are fixed; changing any of them afterward is unsupported, with a different
consequence per field: altering the topology or an ID breaks the ID-to-wire-position
mapping used by Quercus.js; enlarging a Name defeats the size bounds New enforced
(rendering re-serializes the live tree); toggling Disabled can desync the selection
policy.
Build a Node tree (by hand, or from a directory with Root) and pass it with a
lock to New. Browser container IDs are managed internally:
var mu sync.RWMutex
root := &jawstree.Node{Children: []*jawstree.Node{
{Name: "Documents", Children: []*jawstree.Node{{Name: "report.pdf"}}},
{Name: "Pictures"},
}}
tree, err := jawstree.New(&mu, root, jawstree.InitiallyExpanded)
if err != nil {
// handle an invalid tree or initial selection
}
mux.Handle("GET /", ui.Handler(jw, "index.html", tree))In the page template, render the tree directly. Its JaWS-managed Jid.N element
is also the Quercus.js container; the browser initializer unhides it after the
deferred page assets are ready. The same initialization works when a JaWS
container or template inserts the Tree through a live DOM update:
<!DOCTYPE html>
<html>
<head>{{$.HeadHTML}}</head>
<body>
{{$.NewUI .Dot}}
{{$.TailHTML}}
</body>
</html>Selections made in the browser are validated and applied to the Node tree under
the Tree's lock; read them with Tree.GetSelected or change them with
Tree.SetSelected. After mutating the tree server-side, push the new state to all
rendered clients with Tree.Dirty:
if err := tree.SetSelected([][]string{{"Documents", "report.pdf"}}); err == nil {
tree.Dirty(jw)
}The selection policy follows the configured mode. With neither
MultiSelectEnabled nor CascadeSelectChildren, at most one node may be selected.
With cascade but not multi-select, the selection is empty or one connected rooted
subtree: descendant branches may be pruned, but every selected descendant must have
its selectable ancestors selected back to the subtree root. Disabled ancestors are
transparent. Tree.SetSelected returns ErrInvalidSelection for a selection the
browser cannot represent, and a new browser cascade replaces the previous subtree.
MultiSelectEnabled permits independent selected nodes.