UNPKG

coffeescript-ui

Version:
879 lines (708 loc) 22.2 kB
### * 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) ->