// // Copyright (c) 2015, Brian Frank and Andy Frank // Licensed under the Academic Free License version 3.0 // // History: // 19 Mar 2015 Andy Frank Creation // using dom using graphics ************************************************************************** ** TreeNode ************************************************************************** ** ** TreeNode models a node in a Tree. ** ** See also: [docDomkit]`docDomkit::Controls#tree` ** @Js abstract class TreeNode { ** Parent node of this node, or 'null' if this node is a root. TreeNode? parent { internal set } ** Is this node expanded? Bool isExpanded() { expanded } ** Return true if this has or might have children. This ** is an optimization to display an expansion control ** without actually loading all the children. The ** default returns '!children.isEmpty'. virtual Bool hasChildren() { !children.isEmpty } ** Get the children of this node. If no children return ** an empty list. Default behavior is no children. This ** method must return the same instances when called. virtual TreeNode[] children() { List.defVal } ** Callback to customize Elem for this node. abstract Void onElem(Elem elem, TreeFlags flags) internal Int? depth internal Elem? elem internal Bool expanded := false } ************************************************************************** ** TreeFlags ************************************************************************** ** Tree specific flags for eventing @Js const class TreeFlags { new make(|This| f) { f(this) } ** Tree has focus. const Bool focused ** Node is selected. const Bool selected override Str toStr() { "TreeFlags { focused=$focused; selected=$selected }" } } ************************************************************************** ** TreeEvent ************************************************************************** ** ** TreeEvent are generated by `TreeNode` nodes. ** @Js class TreeEvent { internal new make(Tree t, TreeNode n, |This| f) { this.tree = t this.node = n f(this) } ** Parent `Tree` instance. Tree tree { private set } ** `TreeNode` this event was trigged on. TreeNode node { private set } ** Event type. const Str type ** Mouse position relative to page. const Point pagePos ** Mouse position relative to node. const Point nodePos ** Size of node for this event. const Size size override Str toStr() { "TreeNode { node=$node type=$type pagePos=$pagePos nodePos=$nodePos size=$size }" } } ************************************************************************** ** Tree ************************************************************************** ** ** Tree visualizes [TreeNodes]`TreeNode` as a series of expandable nodes. ** ** See also: [docDomkit]`docDomkit::Controls#tree` ** @Js class Tree : Box { ** Constructor. new make() : super() { this.sel = TreeSelection(this) this->tabIndex = 0 this.style.addClass("domkit-Tree domkit-border") this.onEvent("mousedown", false) |e| { onMouseEvent(e) } this.onEvent("mouseup", false) |e| { onMouseEvent(e) } this.onEvent("dblclick", false) |e| { onMouseEvent(e) } // manually track focus so we can detect when // the browser window becomes unactive while // maintaining focus internally in document this.onEvent("focus", false) |e| { manFocus=true; refresh } this.onEvent("blur", false) |e| { manFocus=false; refresh } } ** Root nodes for this tree. TreeNode[] roots := [,] ** Rebuild tree layout. Void rebuild() { if (this.size.w > 0f) doRebuild else Win.cur.setTimeout(16ms) |->| { rebuild } } ** Refresh tree content. Void refresh() { roots.each |r| { refreshNode(r) } } ** Refresh given node. Void refreshNode(TreeNode node) { doRefreshNode(node) } ** Set expanded state for given node. Void expand(TreeNode node, Bool expanded) { // short-cirucit if no-op if (node.expanded == expanded) return node.expanded = expanded refreshNode(node) } ** Experimental hook to modify the node display state. @NoDoc Void displayState(TreeNode node, Str? state) { // remove existing state content := node.elem.querySelector(".domkit-Tree-node") content.style.removeClass("down") // add new state if (state == "down") content.style.addClass("down") } ** Selection for tree. Index based selection is not supported for Tree. Selection sel { private set } ** Callback when selection changes. Void onSelect(|This| f) { cbSelect = f } ** Callback when a node has been double clicked. Void onAction(|Tree, Event| f) { cbAction = f } ** Callback when a event occurs inside a tree node. Void onTreeEvent(Str type, |TreeEvent| f) { cbTreeEvent[type] = f } ////////////////////////////////////////////////////////////////////////// // Layout ////////////////////////////////////////////////////////////////////////// private Void doRebuild() { removeAll roots.each |r| { add(toElem(null, r)) } } private Void doRefreshNode(TreeNode node) { // TODO: how does this work? if (node.elem == null) return // update css node.elem.style.toggleClass("expanded", node.expanded) // set expander icon expander := node.elem.querySelector(".domkit-Tree-node-expander") expander.style->left = "${node.depth * depthIndent}px" expander.html = node.hasChildren ? "\u25ba" : " " // remove existing children while (node.elem.children.size > 1) node.elem.remove(node.elem.lastChild) // update selection selected := sel.items.contains(node) content := node.elem.querySelector(".domkit-Tree-node") content.style.toggleClass("domkit-sel", selected) // update content flags := TreeFlags { it.focused = manFocus it.selected = selected } content.style->paddingLeft = "${(node.depth+1) * depthIndent}px" node.onElem(content.lastChild, flags) // add children if expanded if (node.expanded) { node.children.each |k| { k.parent = node node.elem.add(toElem(node, k)) doRefreshNode(k) } } } ** Map TreeNode to DOM element. private Elem toElem(TreeNode? parent, TreeNode node) { if (node.elem == null) { node.depth = parent==null ? 0 : parent.depth+1 node.elem = Elem { it.style.addClass("domkit-Tree-node-block") Elem { it.style.addClass("domkit-Tree-node") Elem { it.style.addClass("domkit-Tree-node-expander") }, Elem {}, }, } refreshNode(node) } return node.elem } ** Map DOM element to TreeNode. private TreeNode toNode(Elem elem) { // bubble to block elem while (!elem.style.hasClass("domkit-Tree-node-block")) elem = elem.parent // find dom path elemPath := Elem[elem] while (!elemPath.first.parent.style.hasClass("domkit-Tree")) elemPath.insert(0, elemPath.first.parent) // walk path from roots TreeNode? node elemPath.each |p| { i := p.parent.children.findIndex |k| { p == k } node = node==null ? roots[i] : node.children[i-1] } return node } ////////////////////////////////////////////////////////////////////////// // Eventing ////////////////////////////////////////////////////////////////////////// private Void onMouseEvent(Event e) { elem := e.target if (elem == this) return node := toNode(elem) // check sel/expand if (e.type == "mousedown") { // update selection if (!elem.style.hasClass("domkit-Tree-node-expander") && !sel.items.contains(node)) { sel.item = node cbSelect?.call(this) } } else if (e.type == "mouseup") { // expand node if (elem.style.hasClass("domkit-Tree-node-expander")) expand(node, !node.expanded) } // check action if (e.type == "dblclick" && !elem.style.hasClass("domkit-Tree-node-expander")) cbAction?.call(this, e) // delegate to cell handlers cb := cbTreeEvent[e.type] if (cb != null) { blockElem := node.elem nodeElem := blockElem.firstChild indent := (node.depth + 1) * depthIndent npos := nodeElem.relPos(e.pagePos) // outside of content if (npos.x.toInt - indent < 0) return cb.call(TreeEvent(this, node) { it.type = e.type it.pagePos = e.pagePos it.nodePos = Point(npos.x-indent, npos.y) it.size = Size(nodeElem.size.w-indent, nodeElem.size.h) }) } } ////////////////////////////////////////////////////////////////////////// // Selection ////////////////////////////////////////////////////////////////////////// internal Void onUpdateSel(TreeNode[] oldNodes, TreeNode[] newNodes) { oldNodes.each |n| { refreshNode(n) } newNodes.each |n| { refreshNode(n) } } ////////////////////////////////////////////////////////////////////////// // Fields ////////////////////////////////////////////////////////////////////////// private static const Int depthIndent := 16 private TreeNode[] nodes := [,] private Func? cbSelect private Func? cbAction private Str:Func cbTreeEvent := [:] // focus/blur private Bool manFocus := false } ************************************************************************** ** TreeSelection ************************************************************************** @Js internal class TreeSelection : Selection { new make(Tree tree) { this.tree = tree } override Bool isEmpty() { items.isEmpty } override Int size() { items.size } override Obj? item { get { items.first } set { items = (it == null) ? Obj[,] : [it] } } override Obj[] items := [,] { set { if (!enabled) return oldItems := &items newItems := (multi ? it : (it.size > 0 ? [it.first] : Obj[,])).ro &items = newItems tree.onUpdateSel(oldItems, newItems) } } // TODO: unless we can make index meaningful/useful and performant // its probably better to fail fast so its not used override Int? index { get { throw Err("Not implemented for Tree") } set { throw Err("Not implemented for Tree") } } override Int[] indexes { get { throw Err("Not implemented for Tree") } set { throw Err("Not implemented for Tree") } } private Tree tree }