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/lib/templatereloader"
"github.com/linkdata/jaws/lib/ui"
"github.com/linkdata/jawstree"
"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, terr := jawstree.New(&mu, root, jawstree.InitiallyExpanded)
if terr != nil {
return terr
}
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 ¶
- Constants
- Variables
- func Setup(jw *jaws.Jaws, handleFn jaws.HandleFunc, prefix string) (urls []*url.URL, err error)
- type Node
- type Option
- type Tree
- func (t *Tree) Dirty(jw *jaws.Jaws)
- func (t *Tree) GetSelected() (nameLists [][]string)
- func (t *Tree) JawsInput(elem *jaws.Element, value string) error
- func (t *Tree) JawsRender(elem *jaws.Element, w io.Writer, params []any) (err error)
- func (t *Tree) JawsUpdate(elem *jaws.Element)
- func (t *Tree) SetSelected(nameLists [][]string) (err error)
- func (t *Tree) Walk(fn func(jsPath string, node *Node))
Examples ¶
Constants ¶
const MaxTreeDepth = 128
MaxTreeDepth is the deepest nesting New accepts, measured in edges from the root (the root is depth 0, its children depth 1).
The browser renderer (the vendored Quercus.js treeview) recurses once per level, so its stack use grows with the tree depth. A tree deeper than a browser can render is rejected here with ErrInvalidTree so it never reaches the client. The bound is far above any realistic UI nesting yet well below where the renderer's recursion overflows.
const MaxTreeNodes = 180000
MaxTreeNodes is the largest node count New accepts.
It bounds the largest possible selection message, whose size grows with the node count, to stay within the WebSocket size limit. New rejects a larger tree with ErrInvalidTree; TestSelectionFrameFitsInboundLimit pins the guarantee.
const MaxTreeRenderBytes = 64 << 20 // 64 MiB
MaxTreeRenderBytes is the largest depth-weighted serialized node size New accepts: the sum over all nodes of (depth+1) times the node's own wire size (its JSON-escaped name, positional-path ID, and fixed structural bytes).
The browser holds each node's whole serialized data — its potentially large Node.Name included — in the init payload and in the data-node-data of every element whose subtree contains it. The root is not rendered, so its data is held once (the payload). A non-root node at depth d is held d+1 times: once in the payload, once on its own element, and once on each of its d-1 rendered ancestors. Weighting each node by depth+1 covers both cases. This depth-weighted sum is what the independent MaxTreeNodes and MaxTreeDepth bounds do not cap: a shallow spine with a wide, deep fan-out, or a deep chain of large names, passes them yet duplicates its data across every level. New rejects a tree whose depth-weighted serialized size exceeds this limit with ErrInvalidTree.
The bound is on the retained serialized node data (the per-element data plus the init payload), which is a proxy for, not the exact size of, client memory: the renderer also builds DOM from it, and cascade mode copies descendant data, so actual client memory is a bounded multiple of this ceiling.
Variables ¶
var ErrInvalidSelection = errors.New("jawstree: invalid selection")
ErrInvalidSelection identifies a selection rejected by a Tree policy.
New and Tree.SetSelected return it for more than one selected node in ordinary single-select mode, a disconnected selection in cascade-only mode, selection on a tree configured with NodeSelectionDisabled, or a selected node that is disabled or the root. Match with errors.Is.
var ErrInvalidTree = errors.New("jawstree: invalid tree")
ErrInvalidTree is returned by New when the supplied node graph cannot back a Tree: a nil root, a cyclic or shared-node graph (a node reachable more than once), a negative or unknown Option bit, more nodes than MaxTreeNodes, nesting deeper than MaxTreeDepth, or depth-weighted serialized node data exceeding MaxTreeRenderBytes. The error text carries the specific reason; match the class with errors.Is.
var ErrPathRejected = errors.New("jawstree: refusing client selection write")
ErrPathRejected identifies structurally invalid browser selection input.
Tree.JawsInput uses it for a malformed payload, an out-of-range or root node index, a disabled node, or an absolute selection that the configured mode cannot represent. Other selection-policy failures may match ErrInvalidSelection instead. 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 {
Parent *Node `json:"-"` // parent node, set by New, nil for root
Name string `json:"name"` // display name; do not change after New (see [Tree])
ID string `json:"id,omitzero"` // positional JSON path, set by New; also the DOM data-id
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 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). No locking is needed before the Tree is rendered (for example while building it in New).
The struct json tags are documentation only: the wire encoding sent to Quercus.js is produced by Node.MarshalJSON, and Disabled is emitted inverted (as "selectable":false), so the tags do not all mirror the wire shape.
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 no parent back-pointers and are not yet prepared for rendering; pass the tree to New, which assigns node identities and the parent back-pointers the name-path helpers need.
func (*Node) GetSelected ¶
GetSelected returns the name-paths (root-to-node name lists) of all selected nodes.
Selection is reported 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: Tree.SetSelected cannot tell them apart and selects every sibling sharing a selected name-path. Give siblings distinct names if they must be addressed independently through this API, or use the index-based wire selection.
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) MarshalJSON ¶
MarshalJSON writes the Quercus.js JSON shape for node.
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 ¶
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 independent 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. // // Without [MultiSelectEnabled], the selection is empty or one connected rooted // subtree with disabled nodes treated as transparent, and selecting a new node // replaces the previous subtree. CascadeSelectChildren // CheckboxSelectionEnabled renders checkbox selection controls. CheckboxSelectionEnabled )
The bit positions below are wired one-to-one to the literal bit tests in jawstreeDecodeOptions (assets/jawstree.js); do not reorder or insert constants mid-block without updating that script.
type Tree ¶
type Tree struct {
bind.RWLocker // guards root selection state and is the concurrency contract for Node accessors
// contains filtered or unexported fields
}
Tree is the shared, server-authoritative model behind a Quercus.js tree, and the jaws.UI that renders it.
A Tree holds the node tree and the master selection state, guarded by the lock passed to New (which the application may share with other state). A single Tree is built once and rendered by every request that should show the same collaborative tree; it holds no per-request state, so sharing it is safe.
The tree is fixed once New returns: it derives each node's identity from its position and validates the wire-shaping state against the MaxTreeNodes, MaxTreeDepth, and MaxTreeRenderBytes bounds. Only the selection may change afterward, through Tree.SetSelected or browser events (safe under the lock); each node's Name, Disabled, assigned ID, and the topology (Children) are fixed. Changing any of them is unsupported, with a different consequence per field: altering the topology or an ID breaks the wire-index-to-node identity mapping; enlarging a Name defeats the size bounds New enforced (rendering re-serializes the live tree); toggling Disabled can desync the selection policy.
func New ¶
New returns a shared tree model for root, guarded by l.
It returns ErrInvalidTree for a nil root, a cyclic or shared-node graph, a negative or 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 selection policy (see Tree.SetSelected).
New must run before rendering the Tree or using the name-path selection API. The Tree renders its own container, so no caller-provided HTML id is required.
func (*Tree) Dirty ¶
Dirty marks the Tree changed so every rendered view resynchronizes its client on the next update pass. Call it after mutating selection server-side.
func (*Tree) GetSelected ¶
GetSelected returns the name-paths of all selected nodes.
It reads under the read lock. Selection is reported by name-path, which is lossy for duplicate sibling names; see Node.GetSelected.
func (*Tree) JawsInput ¶
JawsInput applies a browser selection change to the shared Tree.
On a real change it dirties the Tree so every rendered client reconverges. On a rejected, malformed, or no-op change it resynchronizes only the originating client, whose optimistic view may be ahead of the server, leaving peers untouched. It always returns nil: a routine client rejection is logged, not surfaced as an alert.
func (*Tree) JawsRender ¶
JawsRender writes the hidden tree container and schedules its browser initialization.
The current selection is included in the initializer, so a freshly rendered client is correct before any update arrives. A Tree is shared UI state: one *Tree may be rendered by many requests, each getting its own element.
func (*Tree) JawsUpdate ¶
JawsUpdate pushes the current selection to this element's client.
It is safe to call concurrently with the browser event handling that mutates selection, and never rebuilds the tree, so the client's local expansion state is preserved.
func (*Tree) SetSelected ¶
SetSelected sets the selection to the nodes matching the given name-paths.
It runs under the write lock and enforces the selection policy, returning ErrInvalidSelection when the match violates it (for example matching more than one node in a single-select tree, a disconnected selection with CascadeSelectChildren but not MultiSelectEnabled, or a disabled node). Matching is by name-path and lossy for duplicate sibling names; see Node.GetSelected.