jawstree

package
v0.600.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 23, 2026 License: MIT Imports: 17 Imported by: 0

README

jawstree

Provides a statically served and embedded version of Quercus.js, a lightweight and customizable JavaScript treeview library with no dependencies.

Asset provenance

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/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 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 := jawstree.New("mytree", ui.NewJsVar(&mu, root), jawstree.InitiallyExpanded)
			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

Page templates rendered through ui.Handler should include {{$.HeadHTML}} inside <head> and {{$.TailHTML}} before the closing </body> tag.

Using the tree widget

A Tree is shared UI state. Build it once before serving or rendering it, then reuse that *Tree for every request that should show the same tree. The embedded ui.JsVar is the backing store, lock, and browser communication channel for the Node tree.

New fixes the tree structure by assigning node IDs from each node's position. After a tree has been rendered, mutate selection state through Tree.SetSelected or browser selection events, but do not add, remove or reorder Children; that breaks the ID-to-wire-position mapping used by Quercus.js.

Build a Node tree (by hand, or from a directory with Root), wrap its root in a ui.JsVar, and pass it to New. New initializes node IDs plus the tree and parent back-pointers, so it must run before rendering or using the name-path selection API:

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))

In the page template, render the tree (it emits a hidden data element and the init script) and provide a container element whose HTML id equals the tree id; Quercus.js renders the tree into that container, and without it the tree silently fails to appear:

<!DOCTYPE html>
<html>
<head>{{$.HeadHTML}}</head>
<body>
  {{$.NewUI .Dot}}
  <div id="mytree"></div>
  {{$.TailHTML}}
</body>
</html>

Selections made in the browser are applied to the Node tree under the ui.JsVar 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 by dirtying the JsVar's bound pointer:

tree.SetSelected([][]string{{"Documents", "report.pdf"}})
jw.Dirty(root)

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)
	}
}

Index

Examples

Constants

This section is empty.

Variables

View Source
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

func Setup(jw *jaws.Jaws, handleFn jaws.HandleFunc, prefix string) (urls []*url.URL, err error)

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) GetNames

func (node *Node) GetNames() (names []string)

GetNames returns the path of names from the root to node.

func (*Node) GetSelected

func (node *Node) GetSelected() (nameLists [][]string)

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

func (node *Node) HasNames(names []string) (yes bool)

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

func (node *Node) JawsPathSet(elem *jaws.Element, jsPath string, value any)

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

func (node *Node) JawsSetPath(elem *jaws.Element, jsPath string, value any) (err error)

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

func (node Node) MarshalJSON() (b []byte, err error)

MarshalJSON writes the Quercus.js JSON shape for node.

func (*Node) SetSelected

func (node *Node) SetSelected(nameLists [][]string) (changed []*Node)

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

func (node *Node) Walk(jsPath string, fn func(jsPath string, node *Node))

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

type Tree struct {
	*ui.JsVar[Node]
	// contains filtered or unexported fields
}

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

func New(id string, jsvar *ui.JsVar[Node], options ...Option) (t *Tree)

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

func (tree *Tree) GetSelected() (nameLists [][]string)

GetSelected returns the selected name-paths.

It reads under the tree read lock.

func (*Tree) JawsRender

func (tree *Tree) JawsRender(elem *jaws.Element, w io.Writer, params []any) (err error)

JawsRender renders the hidden root data element and tree initialization script.

func (*Tree) JawsUpdate added in v0.300.0

func (tree *Tree) JawsUpdate(elem *jaws.Element)

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

func (tree *Tree) SetSelected(nameLists [][]string) (changed []*Node)

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.

func (*Tree) Walk added in v0.503.0

func (tree *Tree) Walk(fn func(jsPath string, node *Node))

Walk calls fn for the tree root and all descendants.

It is called with the tree read lock held, so the callback must not call methods that acquire the same tree lock.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL