coffeescript-ui
Version:
Coffeescript User Interface System
879 lines (708 loc) • 22.2 kB
text/coffeescript
###
* coffeescript-ui - Coffeescript User Interface System (CUI)
* Copyright (c) 2013 - 2016 Programmfabrik GmbH
* MIT Licence
* https://github.com/programmfabrik/coffeescript-ui, http://www.coffeescript-ui.org
###
class CUI.ListViewTreeNode extends CUI.ListViewRow
initOpts: ->
super()
@addOpts
children:
check: Array
open:
check: Boolean
html: {}
colspan:
check: (v) ->
v > 0
getChildren:
check: Function
readOpts: ->
super()
@setColspan(@_colspan)
if @_children
@children = @opts.children
@initChildren()
if @_open
@do_open = true
else
@do_open = false
@is_open = false
@html = @_html
@__loadingDeferred = null
setColspan: (@colspan) ->
getColspan: ->
@colspan
# overwrite with Method
getChildren: null
# overwrite with Method
hasChildren: null
isLeaf: ->
leaf = (if @children
false
else if @opts.getChildren
false
else if @getChildren
if @opts.leaf or (@hasChildren and not @hasChildren())
true
else
false
else
true)
# console.debug CUI.util.getObjectClass(@), "isLeaf?",leaf, @hasChildren?(), @opts.leaf, @
leaf
getClass: ->
cls = super()
cls = cls + " cui-lv-tree-node"
if not @isLeaf()
cls = cls + " cui-lv-tree-node--is-branch"
cls
isSelectable: ->
@getTree?().isSelectable() and @__selectable and not @isRoot()
getFather: ->
@father
setFather: (new_father) ->
CUI.util.assert(new_father == null or new_father instanceof CUI.ListViewTreeNode, "#{CUI.util.getObjectClass(@)}.setFather", "father can only be null or instanceof CUI.ListViewTreeNode", father: new_father)
CUI.util.assert(new_father != @, "#{CUI.util.getObjectClass(@)}.setFather", "father cannot be self", node: @, father: new_father)
# console.debug @, new_father
if new_father
CUI.util.assert(@ not in new_father.getPath(true), "#{CUI.util.getObjectClass(@)}.setFather", "father cannot any of the node's children", node: @, father: new_father)
if not new_father and @selected
@setSelectedNode(null)
@selected = false
if @father and not new_father
# copy tree, since we are a new root node
tree = @getTree()
@father = new_father
if tree
@setTree(tree)
else
@father = new_father
# if @children
# for c in @children
# c.setFather(@)
@
isRoot: ->
not @father
setTree: (@tree) ->
CUI.util.assert(@isRoot(), "#{CUI.util.getObjectClass(@)}.setTree", "node must be root node to set tree", tree: @tree, opts: @opts)
CUI.util.assert(@tree instanceof CUI.ListViewTree, "#{CUI.util.getObjectClass(@)}.setTree", "tree must be instance of ListViewTree", tree: @tree, opts: @opts)
getRoot: (call=0) ->
CUI.util.assert(call < 100, "ListViewTreeNode.getRoot", "Recursion detected.")
if @father
@father.getRoot(call+1)
else
@
dump: (lines=[], depth=0) ->
padding = []
for pad in [0...depth]
padding.push(" ")
lines.push(padding.join("")+@dumpString())
if @children
for c in @children
c.dump(lines, depth+1)
if depth==0
return "\n"+lines.join("\n")+"\n"
dumpString: ->
@getNodeId()
getTree: (call=0) ->
CUI.util.assert(call < 100, "ListViewTreeNode.getTree", "Recursion detected.")
if @isRoot()
@tree
else
@getRoot().getTree(call+1)
# find tree nodes, using the provided compare
# function
find: (eq_func=null, nodes=[]) ->
if not eq_func or eq_func.call(@, @)
nodes.push(@)
if @children
for c in @children
c.find(eq_func, nodes)
nodes
# filters the children by function
filter: (filter_func, filtered_nodes = []) ->
save_children = @children?.slice(0)
if @father and filter_func.call(@, @)
# this means we have to be removed and our children
# must be attached in our place to our father
our_idx = @getChildIdx()
filtered_nodes.push(@)
father = @getFather()
CUI.ListViewTreeNode::remove.call(@, true, false) # keep children array, no de-select
for c, idx in save_children
father.children.splice(our_idx+idx, 0, c)
c.setFather(father)
# console.debug "removed ", @, our_idx, save_children
if save_children
for c in save_children
c.filter(filter_func, filtered_nodes)
filtered_nodes
getPath: (include_self=false, path=[], call=0) ->
CUI.util.assert(call < 100, "ListViewTreeNode.getPath", "Recursion detected.")
if @father
@father.getPath(true, path, call+1)
if include_self
path.push(@)
return path
getChildIdx: ->
if @isRoot()
"root"
else
ci = @father.children.indexOf(@)
CUI.util.assert(ci > -1, "#{CUI.util.getObjectClass(@)}.getChildIdx()", "Node not found in fathers children Array", node: @, father: @father, "father.children": @father.children)
ci
getNodeId: (include_self=true) ->
path = @getPath(include_self)
(p.getChildIdx() for p in path).join(".")
getOpenChildNodes: (nodes=[]) ->
if @children and @is_open
for node in @children
nodes.push(node)
node.getOpenChildNodes(nodes)
nodes
getRowsToMove: ->
@getOpenChildNodes()
isRendered: ->
if (@isRoot() and @getTree()?.getGrid()) or @__is_rendered
true
else
false
sort: (func, level=0) ->
if not @children?.length
return
@children.sort(func)
for node in @children
node.sort(func, level+1)
if level == 0 and @isRendered()
@reload()
@
close: ->
CUI.util.assert(@father, "ListViewTreeNode.close()", "Cannot close root node", node: @)
CUI.util.assert(not @isLoading(), "ListViewTreeNode.close", "Cannot close node, during opening...", node: @, tree: @getTree())
@do_open = false
if @father.is_open
# console.debug "closing node", @node_element
@removeFromDOM(false)
@replaceSelf()
# @getTree().layout()
# @element.trigger("tree", type: "close")
CUI.resolvedPromise()
# remove_self == false only removs children
removeFromDOM: (remove_self=true) ->
@abortLoading()
# console.debug "remove from DOM", @getNodeId(), @is_open, @children?.length, remove_self
if @is_open
@do_open = true
if @children
for c in @children
c.removeFromDOM()
else
@do_open = false
if remove_self
# console.debug "removing ", @getUniqueId(), @getDOMNodes()?[0], @__is_rendered, @getRowIdx()
if @__is_rendered
tree = @getTree()
if tree and not tree.isDestroyed()
if @getRowIdx() == null
if not @isRoot()
tree.removeDeferredRow(@)
else
tree.removeRow(@getRowIdx())
@__is_rendered = false
@is_open = false
@
# getElement: ->
# @element
# replaces node_element with a new render of ourself
replaceSelf: ->
if @father
if tree = @getTree()
# layout_stopped = tree.stopLayout()
tree.replaceRow(@getRowIdx(), @render())
if @selected
tree.rowAddClass(@getRowIdx(), CUI.ListViewRow.defaults.selected_class)
# if layout_stopped
# tree.startLayout()
return CUI.resolvedPromise()
else if @is_open
# root node
@removeFromDOM(false) # tree.removeAllRows()
return @open()
else
return CUI.resolvedPromise()
# opens all children and grandchilden
openRecursively: ->
@__actionRecursively("open")
closeRecursively: ->
@__actionRecursively("close")
__actionRecursively: (action) ->
dfr = new CUI.Deferred()
if @isLeaf()
return dfr.resolve().promise()
if action == "open"
ret = @getLoading()
if not ret
ret = @open()
else
ret = @close()
ret.done =>
promises = []
for child in @children
promises.push(child["#{action}Recursively"]())
CUI.when(promises)
.done(dfr.resolve)
.fail(dfr.reject)
.fail(dfr.reject)
dfr.promise()
isOpen: ->
!!@is_open
isLoading: ->
!!@__loadingDeferred
getLoading: ->
@__loadingDeferred
abortLoading: ->
if not @__loadingDeferred
return
# console.error("ListViewTreeNode.abortLoading: Aborting chunk loading.")
if @__loadingDeferred.state == 'pending'
@__loadingDeferred.reject()
@__loadingDeferred = null
return
# resolves with the opened node
open: ->
# we could return loading_deferred here
CUI.util.assert(not @isLoading(), "ListViewTreeNode.open", "Cannot open node #{@getUniqueId()}, during opening. This can happen if the same node exists multiple times in the same tree.", node: @)
if @is_open or @isLeaf()
return CUI.resolvedPromise()
# console.error @getUniqueId(), "opening...", "is open:", @is_open, open_counter
@is_open = true
@do_open = false
dfr = @__loadingDeferred = new CUI.Deferred()
# console.warn("Start opening", @getUniqueId(), dfr.getUniqueId())
do_resolve = =>
if @__loadingDeferred.state() == "pending"
@__loadingDeferred.resolve(@)
@__loadingDeferred = null
do_reject = =>
if @__loadingDeferred.state() == "pending"
@__loadingDeferred.reject(@)
@__loadingDeferred = null
load_children = =>
CUI.util.assert(CUI.util.isArray(@children), "ListViewTreeNode.open", "children to be loaded must be an Array", children: @children, listViewTreeNode: @)
# console.debug @._key, @getUniqueId(), "children loaded", @children.length
if @children.length == 0
if not @isRoot()
@replaceSelf()
do_resolve()
return
@initChildren()
CUI.chunkWork.call @,
items: @children
chunk_size: 5
timeout: 1
call: (items) =>
CUI.chunkWork.call @,
items: items
chunk_size: 1
timeout: -1
call: (_items) =>
# console.error @getUniqueId(), open_counter, @__open_counter, "chunking work"
if dfr != @__loadingDeferred
# we are already in a new run, exit
return false
@__appendNode(_items[0], true) # , false, true))
.done =>
# console.error @getUniqueId(), open_counter, @__open_counter, "chunking work DONE"
if dfr != @__loadingDeferred
return
if not @isRoot()
@replaceSelf()
do_resolve()
.fail =>
# console.error @getUniqueId(), open_counter, @__open_counter, "chunking work FAIL"
if dfr != @__loadingDeferred
return
for c in @children
c.removeFromDOM()
do_reject()
return
if @children
load_children()
else
func = @opts.getChildren or @getChildren
if func
ret = func.call(@)
if CUI.util.isArray(ret)
@children = ret
load_children()
else
CUI.util.assert(CUI.util.isPromise(ret), "#{CUI.util.getObjectClass(@)}.open", "returned children are not of type Promise or Array", children: ret)
ret
.done (@children) =>
if dfr != @__loadingDeferred
return
load_children()
return
.fail(do_reject)
else
if not @isRoot()
@replaceSelf()
do_resolve()
dfr.promise()
prependChild: (node) ->
@addNode(node, false)
addChild: (node) ->
@addNode(node, true)
prependSibling: (node) ->
idx = @getChildIdx()
@father.addNode(node, idx)
appendSibling: (node) ->
idx = @getChildIdx()+1
if idx > @father.children.length-1
@father.addNode(node)
else
@father.addNode(node, idx)
setChildren: (@children) ->
@initChildren()
return
initChildren: ->
for node, idx in @children
for _node, _idx in @children
if idx == _idx
continue
CUI.util.assert(_node != node, "ListViewTreeNode.initChildren", "Must have every child only once.", node: @, child: node)
node.setFather(@)
return
# add node adds the node to our children array and
# actually visually appends it to the ListView
# however, it does not visually appends it, if the root node
# is not yet "open".
addNode: (node, append=true) ->
CUI.util.assert(not @isLoading(), "ListViewTreeNode.addNode", "Cannot add node, during loading.", node: @)
if not @children
@children = []
CUI.util.assert(CUI.util.isArray(@children), "Tree.addNode","Cannot add node, children needs to be an Array in node", node: @, new_node: node)
for _node in @children
CUI.util.assert(_node != node, "ListViewTreeNode.addNode", "Must have every child only once.", node: @, child: _node)
if append == true
@children.push(node)
else
@children.splice(append,0,node)
node.setFather(@)
# if @is_open
# # we are already open, append the node
# return @__appendNode(node, append)
# else
# return CUI.resolvedPromise(node)
if not @is_open
if @isRoot() or not @isRendered()
return CUI.resolvedPromise(node)
# open us, since we have just added a child node
# we need to open us
# make sure to return the addedNode
dfr = new CUI.Deferred()
@open()
.done =>
dfr.resolve(node)
.fail =>
dfr.reject(node)
promise = dfr.promise()
else
# we are already open, so simply append the node
promise = @__appendNode(node, append)
promise
# resolves with the appended node
__appendNode: (node, append=true) -> # , assume_open=false) ->
CUI.util.assert(node instanceof CUI.ListViewTreeNode, "ListViewTreeNode.__appendNode", "node must be instance of ListViewTreeNode", node: @, new_node: node)
CUI.util.assert(node.getFather() == @, "ListViewTreeNode.__appendNode", "node added must be child of current node", node: @, new_node: node)
# console.debug ".__appendNode: father: ", @getUniqueId()+"["+@getNodeId()+"]", "child:", node.getUniqueId()+"["+node.getNodeId()+"]"
if append == false
append = 0
tree = @getTree()
if tree?.isDestroyed()
return CUI.rejectedPromise(node)
if not @isRendered()
return CUI.resolvedPromise(node)
# console.warn "appendNode", @getUniqueId(), "new:", node.getUniqueId(), "father open:", @getFather()?.isOpen()
child_idx = node.getChildIdx()
if @isRoot()
if append == true or @children.length == 1 or append+1 == @children.length
tree.appendRow(node.render())
else
CUI.util.assert(@children[append+1], @__cls+".__addNode", "Node not found", children: @children, node: @, append: append)
tree.insertRowBefore(@children[append+1].getRowIdx(), node.render())
else if child_idx == 0
# this means the added node is the first child, we
# always add this after
tree.insertRowAfter(@getRowIdx(), node.render())
else if append != true
tree.insertRowBefore(@children[append+1].getRowIdx(), node.render())
else
# this last node is the node before us, we need to see
# if is is opened and find the last node opened
last_node = @children[child_idx-1]
child_nodes = last_node.getOpenChildNodes()
if child_nodes.length
last_node = child_nodes[child_nodes.length-1]
# if last_node.getRowIdx() == null
# console.error "row index is not there", tree, @is_open, @getNodeId(), child_idx, @children
# _last_node = @children[child_idx-1]
# console.debug "last node", _last_node, _last_node.isRendered()
# console.debug "child nodes", child_nodes, last_node.isRendered()
tree.insertRowAfter(last_node.getRowIdx(), node.render())
if node.selected
tree.rowAddClass(node.getRowIdx(), CUI.ListViewRow.defaults.selected_class)
if node.do_open
node.open()
else
CUI.resolvedPromise(node)
remove: (keep_children_array=false, select_after=true) ->
dfr = new CUI.Deferred()
select_after_node = null
remove_node = =>
# console.debug "remove", @getNodeId(), @father.getNodeId(), @element
@removeFromDOM()
@father?.removeChild(@, keep_children_array)
if tree = @getTree()
CUI.Events.trigger
node: tree
type: "row_removed"
if select_after_node
select_after_node.select()
.done(dfr.resolve).fail(dfr.reject)
else
dfr.resolve()
else
dfr.resolve()
return
if select_after and not @isRoot()
children = @getFather().children
if children.length > 1
child_idx = @getChildIdx()
if child_idx == 0
select_after = 1
else
select_after = Math.min(children.length-2, child_idx-1)
if select_after != null
select_after_node = children[select_after]
if @isSelected()
@deselect().fail(dfr.reject).done(remove_node)
else
remove_node()
dfr.promise()
removeChild: (child, keep_children_array=false) ->
CUI.util.removeFromArray(child, @children)
if @children.length == 0 and not @isRoot()
@is_open = false
if not keep_children_array
# this hides the "open" icon
@children = null
# console.error "removeChild...", @children?.length, keep_children_array, @isRoot()
@update()
child.setFather(null)
deselect: (ev, new_node) ->
if not @getTree().isSelectable()
return CUI.resolvedPromise()
@check_deselect(ev, new_node)
.done =>
@setSelectedNode()
@removeSelectedClass()
@selected = false
@getTree().triggerNodeDeselect(ev, @)
allowRowMove: ->
true
check_deselect: (ev, new_node) ->
CUI.resolvedPromise()
isSelected: ->
!!@selected
addSelectedClass: ->
@getTree().rowAddClass(@getRowIdx(), CUI.ListViewRow.defaults.selected_class)
removeSelectedClass: ->
@getTree().rowRemoveClass(@getRowIdx(), CUI.ListViewRow.defaults.selected_class)
setSelectedNode: (node = null, key = @getSelectedNodeKey()) ->
@getRoot()[@getSelectedNodeKey()] = node
getSelectedNode: (key = @getSelectedNodeKey()) ->
@getRoot()?[key] or null
getSelectedNodeKey: ->
"selectedNode"
select: (event) ->
deferred = new CUI.Deferred()
if event and @getTree?().isSelectable()
event.stopPropagation?()
deferred.done =>
@getTree().triggerNodeSelect(event, @)
if not @isSelectable()
# console.debug "row is not selectable", "row:", @, "tree:", @getTree()
return deferred.reject().promise()
if @isSelected()
return deferred.resolve().promise()
# console.debug "selecting node", sel_node
do_select = =>
@setSelectedNode(@)
# console.error "openUpwards", @getNodeId(), @is_open
@openUpwards()
.done =>
@addSelectedClass()
@selected = true
deferred.resolve()
.fail(deferred.reject)
# the selected node is not us, so we ask the other
# node
selectedNode = @getSelectedNode()
# If selectableRows is 'true' means that only one row can be selected at the same time, then it is deselected.
if selectedNode and @getTree()?.__selectableRows == true
selectedNode.check_deselect(event, @).done( =>
# don't pass event, so no check is performed
#console.debug "selected node:", sel_node
selectedNode.deselect(null, @).done( =>
do_select()
).fail(deferred.reject)
).fail(deferred.reject)
else
do_select()
# console.debug "selecting node",
deferred.promise()
# resolves with the innermost node
openUpwards: (level=0) ->
dfr = new CUI.Deferred()
if @isRoot()
if @isLoading()
@getLoading()
.done =>
dfr.resolve(@)
.fail =>
dfr.reject(@)
else if @is_open
dfr.resolve(@)
else
# if root is not open, we reject all promises
# an tell our callers to set "do_open"
dfr.reject(@)
# not opening root
else
promise = @father.openUpwards(level+1)
promise.done =>
if not @is_open and level > 0
if @isLoading()
_promise = @getLoading()
else
_promise = @open()
_promise
.done =>
dfr.resolve(@)
.fail =>
dfr.reject(@)
else
dfr.resolve(@)
promise.fail =>
# remember to open
@do_open = true
if level == 0
# on the last level we resolve fine
dfr.resolve(@)
else
dfr.reject(@)
dfr.promise()
level: ->
if @isRoot()
0
else
@father.level()+1
renderContent: ->
if CUI.util.isFunction(@html)
@html.call(@opts, @)
else if @html
@html
else
new CUI.EmptyLabel(text: "<empty>").DOM
update: (update_root=false) =>
# console.debug "updating ", @element?[0], @children, @getFather(), update_root, @isRoot(), @getTree()
if not update_root and (not @__is_rendered or @isRoot())
# dont update root
return CUI.resolvedPromise()
tree = @getTree()
layout_stopped = tree?.stopLayout()
@replaceSelf()
.done =>
if layout_stopped
tree.startLayout()
reload: ->
# console.debug "ListViewTreeNode.reload:", @isRoot(), @is_open
CUI.util.assert(not @isLoading(), "ListViewTreeNode.reload", "Cannot reload node, during opening...", node: @, tree: @getTree())
if @isRoot()
@replaceSelf()
else if @is_open
@close()
@do_open = true
@open()
else
if @opts.children
@children = null
@update()
showSpinner: ->
if @__is_rendered
CUI.dom.empty(@__handleDiv)
CUI.dom.append(@__handleDiv, new CUI.Icon(icon: "spinner").DOM)
@
hideSpinner: ->
if @__is_rendered
CUI.dom.empty(@__handleDiv)
if @__handleIcon
CUI.dom.append(@__handleDiv, new CUI.Icon(icon: @__handleIcon).DOM)
else
@
render: ->
CUI.util.assert(not @isRoot(), "ListViewTreeNode.render", "Unable to render root node.")
@removeColumns()
element = CUI.dom.div("cui-tree-node level-#{@level()}")
@__is_rendered = true
# Space for the left side
for i in [1...@level()] by 1
CUI.dom.append(element, CUI.dom.div("cui-tree-node-spacer"))
# Handle before content
cls = ["cui-tree-node-handle"]
if @is_open
@__handleIcon = "tree_close"
cls.push("cui-tree-node-is-open")
else if @isLeaf()
@__handleIcon = null
cls.push("cui-tree-node-is-leaf")
else
@__handleIcon = "tree_open"
cls.push("cui-tree-node-is-closed")
if @children?.length == 0
cls.push("cui-tree-node-no-children")
@__handleDiv = CUI.dom.div(cls.join(" "))
if @__handleIcon
CUI.dom.append(@__handleDiv, new CUI.Icon(icon: @__handleIcon).DOM)
CUI.dom.append(element, @__handleDiv)
# push the tree element as the first column
@prependColumn new CUI.ListViewColumn
element: element
class: "cui-tree-node-column cui-tree-node-level-#{@level()}"
colspan: @getColspan()
# nodes can re-arrange the order of the columns
# so we call them last
# append Content
contentDiv = CUI.dom.div("cui-tree-node-content")
content = @renderContent()
if CUI.util.isArray(content)
for con in content
CUI.dom.append(contentDiv, con)
else
CUI.dom.append(contentDiv, content)
CUI.dom.append(element, contentDiv)
@
moveToNewFather: (new_father, new_child_idx) ->
old_father = @father
old_father.removeChild(@)
new_father.children.splice(new_child_idx, 0, @)
@setFather(new_father)
old_father.reload()
new_father.reload()
# needs to return a promise
moveNodeBefore: (to_node, new_father, after) ->
CUI.resolvedPromise()
moveNodeAfter: (to_node, new_father, after) ->