Documentation
¶
Overview ¶
Package jawstree provides a JaWS widget and embedded assets for the Quercus.js treeview library.
The package embeds a small JaWS adapter plus vendored Quercus.js assets from https://github.com/stefaneichert/quercus.js. When updating the vendored Quercus files, update README.md's provenance table in the same change.
Example ¶
Example wires jawstree (and jawsboot) into an HTTP server. It is a compile-checked illustration only: it starts a blocking server, so it has no testable Output and is not executed by "go test".
package main
import (
"embed"
"log/slog"
"net/http"
"sync"
"github.com/linkdata/jaws"
"github.com/linkdata/jaws/jawsboot"
"github.com/linkdata/jaws/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 jaws.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 := jawstree.New("mytree", ui.NewJsVar(&mu, root), jawstree.InitiallyExpanded)
mux.Handle("GET /", ui.Handler(jw, "index.html", tree))
}
}
return
}
// Example wires jawstree (and jawsboot) into an HTTP server. It is a
// compile-checked illustration only: it starts a blocking server, so it has no
// testable Output and is not executed by "go test".
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)
}
}
Output:
Index ¶
- Variables
- func Setup(jw *jaws.Jaws, handleFn jaws.HandleFunc, prefix string) (urls []*url.URL, err error)
- type Node
- func (node *Node) GetNames() (names []string)
- func (node *Node) GetSelected() (nameLists [][]string)
- func (node *Node) HasNames(names []string) (yes bool)
- func (node *Node) JawsPathSet(elem *jaws.Element, jsPath string, value any)
- func (node *Node) JawsSetPath(elem *jaws.Element, jsPath string, value any) (err error)
- func (node Node) MarshalJSON() (b []byte, err error)
- func (node *Node) SetSelected(nameLists [][]string) (changed []*Node)
- func (node *Node) Walk(jsPath string, fn func(jsPath string, node *Node))
- type Option
- type Tree
- func (tree *Tree) GetSelected() (nameLists [][]string)
- func (tree *Tree) JawsRender(elem *jaws.Element, w io.Writer, params []any) (err error)
- func (tree *Tree) JawsUpdate(elem *jaws.Element)
- func (tree *Tree) SetSelected(nameLists [][]string) (changed []*Node)
- func (tree *Tree) Walk(fn func(jsPath string, node *Node))
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrPathRejected = errors.New("jawstree: refusing client path-set")
ErrPathRejected is returned by Node.JawsSetPath when a client-initiated path write is refused: a path that does not address a per-node ".selected" flag, a non-bool value, or a malformed or out-of-range child index. The error text carries the specific reason; match the class with errors.Is.
Functions ¶
func Setup ¶
Setup registers embedded jawstree static assets under prefix.
The prefix may be absolute ("/static"), relative ("static") or empty; the registered handler paths and the returned URLs are kept identical in all cases.
It is intended to be passed to jaws.Jaws.Setup. Returned URLs should be included in the page head through jaws.Jaws.GenerateHeadHTML.
Types ¶
type Node ¶
type Node struct {
Tree *Tree `json:"-"` // owning tree, set by New
Parent *Node `json:"-"` // parent node, set by New, nil for root
Name string `json:"name"` // display name
ID string `json:"id,omitzero"` // JSON path ID, set by New
Selected bool `json:"selected,omitzero"` // selected state
Disabled bool `json:"disabled,omitzero"` // emitted as "selectable":false (inverted) on the wire
Children []*Node `json:"children,omitzero"` // child nodes
}
Node is one tree node rendered by Tree.
Concurrency: once the owning Tree has been rendered, its Node tree is shared with the JaWS event goroutines, which access it under the Tree's lock (the embedded ui.JsVar is an RWLocker). The exported Node accessors below are not internally synchronized, so callers must hold that lock when using them on a rendered Tree: the Tree's read lock (RLock) for the read-only helpers (Node.Walk, Node.HasNames, Node.GetNames, Node.GetSelected, Node.MarshalJSON) and its write lock (Lock) for the mutating Node.SetSelected. No locking is needed before the Tree is rendered (for example while building it in New).
marshalJSON is the single source of truth for the wire shape sent to Quercus.js; MarshalJSON delegates to it and there is no UnmarshalJSON, so the struct json tags below are documentation only and unused for both encoding and decoding. They cannot all mirror the wire shape: Disabled is tagged "disabled" but is emitted inverted as "selectable":false, so treat marshalJSON as authoritative rather than the tags.
func Root ¶
func Root(r *os.Root, filterFn func(dirpath string, ent fs.DirEntry) (include bool)) (rootnode *Node, err error)
Root builds a root node from an os.Root. It panics if r is nil.
If filterFn is not nil, a directory entry is included only when filterFn returns true for it. Entries that are neither regular files nor directories (such as symbolic links) are always excluded, regardless of filterFn.
Building the tree is best-effort: if one or more directories cannot be read, Root returns the tree built from the readable entries together with a non-nil error joining every read failure (see errors.Join). A directory whose own listing fails to read is omitted from its parent, but its readable siblings — and the readable entries of any directory with a deeper failure — are kept.
The returned nodes have a nil Tree and filesystem-relative IDs; pass the tree to New before rendering or any path operation. New overwrites both fields with the owning Tree pointer and the canonical JSON path IDs. The name-path helpers work on the returned nodes as-is, but rendering or serving the tree reaches Node.JawsPathSet, which dereferences Tree and panics until New has set it.
func (*Node) GetSelected ¶
GetSelected returns the name-paths (root-to-node name lists) of all selected nodes.
Selection is reported and matched by name-path, not by the unique node identity used on the wire. If sibling nodes share the same name their name-paths are identical, so the round-trip is lossy: Node.SetSelected cannot tell them apart and will select every sibling sharing a selected name-path. Give siblings distinct names if they must be addressed independently through this API.
func (*Node) HasNames ¶
HasNames reports whether node matches names as a path from the root.
The root (nil Parent) matches only an empty names slice. The match walks the parent chain, comparing each name against the corresponding ancestor, so a call is O(len(names)); resolving large selections over deep trees is therefore O(nodes x depth x paths).
func (*Node) JawsPathSet ¶
JawsPathSet runs after a node's selected flag has been set on the server-side tree; it broadcasts a jawstreeSetPath JsCall so the change is reflected in the rendered tree of every client sharing this Tree.
It requires node.Tree to be set — that is, the node must have been passed to New — and panics otherwise, since a bare Root node has a nil Tree.
func (*Node) JawsSetPath ¶ added in v0.500.0
JawsSetPath restricts browser-initiated mutations to the per-node "selected" flag.
Any other path, a non-bool value, or an out-of-range child index is rejected with an error matching ErrPathRejected without mutating the tree, so a WebSocket client cannot change node names, ids, the children slice, or any other Node field by path. This is the server-side enforcement of the "server holds the truth" contract for Tree.
The root's Selected flag is effectively server-only: the standard client cannot produce the path that addresses the root itself, so avoid rendering the root selected, since clients cannot change it back through the protocol.
func (Node) MarshalJSON ¶ added in v0.300.0
MarshalJSON writes the Quercus.js JSON shape for node.
func (*Node) SetSelected ¶
SetSelected applies the given selected name-paths and returns the nodes that changed.
Nodes are matched by name-path (see Node.GetSelected); when sibling nodes share a name they are selected or deselected together, since their name-paths are indistinguishable.
It mutates the shared Node tree; on a rendered Tree, hold the Tree's write lock while calling it (see the Node concurrency note).
func (*Node) Walk ¶
Walk calls fn for node and all descendants with their JSON paths.
node is visited with the supplied jsPath; callers pass "" for the root. Each descendant is visited with "children.<i>" appended to its parent's path, where i is the child's index in node.Children. A nil child is skipped while i keeps the raw slice index, so on a Tree built by New — where [Node.stripNilChildren] has already removed nil entries — these indices stay dense and match the wire positions emitted by [Node.marshalJSON].
type Option ¶ added in v0.300.0
type Option int
Option configures a Tree.
const ( // SearchEnabled enables tree search controls. SearchEnabled Option = (1 << iota) // InitiallyExpanded renders nodes expanded initially. InitiallyExpanded // MultiSelectEnabled allows multiple selected nodes. MultiSelectEnabled // ShowSelectAllButton shows a select-all control. ShowSelectAllButton // ShowInvertSelectionButton shows an invert-selection control. ShowInvertSelectionButton // ShowExpandCollapseAllButtons shows expand/collapse-all controls. ShowExpandCollapseAllButtons // NodeSelectionDisabled disables node selection. NodeSelectionDisabled // CascadeSelectChildren cascades selection to child nodes. CascadeSelectChildren // CheckboxSelectionEnabled renders checkbox selection controls. CheckboxSelectionEnabled )
The bit positions below are wired one-to-one to the literal bit tests in jawstreeNew (assets/jawstree.js); do not reorder or insert constants mid-block without updating that script.
type Tree ¶
Tree renders and updates a shared Quercus.js tree bound to a ui.JsVar.
A Tree is shared UI state that may be rendered by multiple requests. It embeds a ui.JsVar, which provides the lock and browser communication for the backing Node tree. Read or mutate that Node tree through Tree methods, or while holding the Tree lock.
The tree is structurally fixed once New returns: it assigns each node's ID from its position, which must match the node's wire position. Mutating node fields (e.g. via Tree.SetSelected) under the lock is safe, but adding, removing, or reordering Children afterward breaks that mapping and is unsupported on a rendered Tree.
func New ¶
New returns a tree widget for jsvar, identified by id.
id must be non-empty and contain only the characters [A-Za-z0-9_$], both jsvar and jsvar.Ptr must be non-nil, and the combined options must be non-negative; New panics otherwise. Call New before serving or rendering the Tree.
The rendered page must contain an element whose HTML id equals id (for example <div id="mytree"></div>): Quercus.js renders the tree into that container. If it is missing, the tree silently fails to appear; the only signal is a browser console error, with nothing reported server-side.
func (*Tree) GetSelected ¶ added in v0.503.0
GetSelected returns the selected name-paths.
It reads under the tree read lock.
func (*Tree) JawsRender ¶
JawsRender renders the hidden root data element and tree initialization script.
func (*Tree) JawsUpdate ¶ added in v0.300.0
JawsUpdate sends the latest tree JSON to the browser.
It reads the shared Node tree under the Tree read lock, so it is safe to call concurrently with the JaWS event goroutines that mutate the tree under the write lock.
func (*Tree) SetSelected ¶ added in v0.503.0
SetSelected applies the selected name-paths and returns the changed Node values.
It runs under the tree write lock. The returned Node pointers reference the lock-protected shared tree and the write lock is released on return, so on a rendered Tree they must only be read under the tree read lock (RLock) and mutated under the write lock (Lock), per the Node concurrency note. Dereferencing them without re-taking the lock races the JaWS event goroutines.