jawstree

package
v0.500.0 Latest Latest
Warning

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

Go to latest
Published: Jun 1, 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.

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/bind"
	"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 {
			// Add a route to our index template with a bound variable accessible as '.Dot' in the template
			var mu sync.Mutex
			var f float64
			mux.Handle("GET /", ui.Handler(jw, "index.html", bind.New(&mu, &f)))
		}
	}
	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.

Documentation

Overview

Package jawstree provides a JaWS widget and embedded assets for the Quercus.js treeview library.

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/bind"
	"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 {
			// Add a route to our index template with a bound variable accessible as '.Dot' in the template
			var mu sync.Mutex
			var f float64
			mux.Handle("GET /", ui.Handler(jw, "index.html", bind.New(&mu, &f)))
		}
	}
	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

This section is empty.

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.

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, 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) 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, so the struct json tags below are not actually used for encoding and must be kept in sync with marshalJSON by hand.

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. If filterFn is not nil, it must return true for a directory entry to be included in the tree.

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 subdirectory that fails to read is omitted from its parent, but its readable siblings are kept.

The returned nodes have a nil Tree and filesystem-relative path IDs; New overwrites both with the owning Tree pointer and the canonical JSON path IDs. The node tree must therefore be passed to New (as the JsVar value) before rendering or any path operation, which otherwise dereference the nil Tree and panic.

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.

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.

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 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 path is resolved by navigating the Children slice ourselves with strict in-range index bounds rather than delegating to the generic JsVar path-setter (github.com/linkdata/jq.Set), which sets arbitrary json-tagged fields and grows a slice by one when asked to set index == len.

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 (delegating to the canonical marshalJSON encoder).

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.

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
)

type Tree

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

Tree renders and updates a Quercus.js tree bound to a ui.JsVar.

func New

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

New returns a tree widget with id, jsvar and options.

The id is used both as a JavaScript variable name and as a URL path segment for the init script, so it must be non-empty and contain only the characters [A-Za-z0-9_$]; otherwise New panics. Validating here turns what would otherwise be a confusing render-time "illegal jsvar name" error and a 400 on the init-script route into an immediate, clear failure.

New initializes node IDs and tree back-pointers in jsvar.Ptr. It panics if jsvar or jsvar.Ptr is nil, or if id is not a valid name.

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.

Jump to

Keyboard shortcuts

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