<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>AngularJS UI Tree demo</title>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" />
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/angular-ui-tree/2.22.6/angular-ui-tree.min.css" />
<link rel="stylesheet" href="demo.css" />
</head>
<body ng-app="demoApp">
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="row" ng-controller="MainCtrl">
<div class="col-lg-6">
<h1>Hierarchy Selector</h1>
<hr>
<hierarchy-search dataset="list"></hierarchy-search>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript -->
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-tree/2.22.6/angular-ui-tree.min.js"></script>
<script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.11.0.js"></script>
<script src="demolist.js"></script>
<script src="HierarchyNodeService.js"></script>
<script src="hierarchySearch.js"></script>
<script src="main.js"></script>
</body>
</html>
* {
-webkit-font-smoothing: antialiased;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.past-users-filter {
cursor:pointer;
margin-left:20px;
}
.no-results {
padding-left:20px;
padding-bottom:10px;
}
/*matched result from search*/
.matched {
font-weight:700 !important;
}
/*wraps entire selector*/
.hierarchy-wrap {
background: #FFFFFF;
border:1px solid #ccc;
border-radius:5px;
}
/* past users + search */
.hierarchy-header {
padding:10px;
}
.hierarchy-header .search-wrap {
margin-bottom:5px;
}
.angular-ui-tree-handle {
color: black;
padding: 5px;
font-weight:normal;
cursor:pointer;
}
.angular-ui-tree-nodes .angular-ui-tree-nodes { padding-left:0; }
.angular-ui-tree-nodes .angular-ui-tree-nodes .node-embed {
padding-left:10px;
}
.angular-ui-tree-nodes .angular-ui-tree-nodes .angular-ui-tree-nodes .node-embed {
padding-left:20px;
}
.angular-ui-tree-nodes .angular-ui-tree-nodes .angular-ui-tree-nodes .angular-ui-tree-nodes .node-embed {
padding-left:30px;
}
.angular-ui-tree-handle:hover {
color: black;
background: #f5f5f5;
}
.node-header {
padding-right:5px;
}
.nodeActive {
background:#deeaf9;
}
.nodeActive:hover {
background: #428bca;
color:#FEFEFA;
}
.angular-ui-tree-handle i {
color:black;
}
<div class="node-embed" ui-tree-handle ng-click="itemSelect(item,this)" ng-class="{nodeActive:item.isSelected,matched:item.match}">
<a class="btn btn-clear btn-xs" data-ng-show="item.items.length" ng-click="expandNode(this,$event)">
<i class="fa" ng-class="{'fa-chevron-right': collapsed, 'fa-chevron-down': !collapsed}"></i>
</a>
<span class="node-header">
<input type="checkbox" ng-checked="item.isSelected" indeterminate-checkbox node="item" />
</span>
{{item.title}}
</div>
<ol ui-tree-nodes="options" ng-model="item.items" ng-class="{hidden: collapsed,displayed:!collapsed}">
<li ng-repeat="item in item.items" ui-tree-node collapsed="true" ng-include="'items_renderer.html'">
</li>
</ol>
(function() {
angular.module('demoApp.list',[])
.value('MyList',[
{
"id": 1,
"title": "ASD Headquarters",
"items": [
{
"id": 11,
"title": "San Jose",
"items": [
{
"id":13,
"title":"Jensen Chapman's Team",
"items": [
{
"id":14,
"title":"Jimmy John"
},
{
"id":15,
"title":"Daniel Mills"
}
,
{
"id":16,
"title":"Chris Boden"
}
]
}
],
},
{
"id": 12,
"title": "Irvine",
"items": [
{
"id":23,
"title":"Tracey Chapman's Team",
"items": [
{
"id":24,
"title":"San Jesus"
},
{
"id":25,
"title":"Fat Albert"
}
,
{
"id":26,
"title":"Connor McDavid"
}
]
}
]
},
{
"id":30,
"title":"San Diego",
"items": [{
"id":31,
"title":"Duran Duran's Team",
"items":[
{
"id":32,
"title":"Amberlynn Pinkerton"
},
{
"id":33,
"title":"Tony Mejia"
}
,
{
"id":34,
"title":"Richard Partridge"
}
,
{
"id":35,
"title":"Elliot Stabler"
}
]
},
{
"id":40,
"title":"Steely Dan's Team",
"items":[
{
"id":36,
"title":"Tony Stark"
},
{
"id":37,
"title":"Totally Rad"
}
,
{
"id":38,
"title":"Matt Murdock"
}
,
{
"id":39,
"title":"Stan Lee"
}
]
}
]
}
]
}
]);
})()
<div>
<div class="hierarchy-wrap">
<div class="hierarchy-header">
<div class="noselect">
<label for="pastUsersFilter" class=" past-users-filter">
<input type="checkbox" ng-model="pastUsersFilter" id="pastUsersFilter"/>
<span style="margin-left:5px">Include Past Users</span>
</label>
<div class="pull-right">
<strong>{{numSelected}} selected</strong>
</div>
</div>
<div class="input-group search-wrap">
<div class="input-group-addon"><i class="fa fa-search"></i></div>
<input type="text" class="form-control" ng-model="searchValue" placeholder="Search..." />
</div>
</div>
<div class="no-results" ng-show="emptyData"><strong>No Results Found</strong></div>
<div ng-show="!emptyData" ui-tree="options" data-drag-enabled="false">
<ol id="demoTree" ui-tree-nodes="" ng-model="list">
<li ng-repeat="item in list" ui-tree-node collapsed="true" ng-include="'items_renderer.html'"></li>
</ol>
</div>
</div>
<hr>
<h2>Selected Ids</h2>
<pre class="code">{{ selected }}</pre>
<h2>Search Results</h2>
<pre class="code">{{ list | json }}</pre>
</div>
(function(){
angular.module('demoApp.directives',[])
.directive('indeterminateCheckbox',function(HierarchyNodeService) {
return {
restrict:'A',
scope: {
node:'='
},
link: function(scope, element, attr) {
scope.$watch('node',function(nv) {
var flattenedTree = HierarchyNodeService.getAllChildren(scope.node,[]);
flattenedTree = flattenedTree.map(function(n){ return n.isSelected });
var initalLength = flattenedTree.length;
var compactedTree = _.compact(flattenedTree);
var r = compactedTree.length > 0 && compactedTree.length < flattenedTree.length;
element.prop('indeterminate', r);
},true);
}
}
})
.directive('hierarchySearch',function(HierarchyNodeService,$timeout) {
return {
restrict:'E',
templateUrl:'hierarchySearch.tpl.html',
scope: {
dataset:'='
},
controller:function($scope) {
$scope.numSelected = 0;
//$scope.list is used by ng-tree, dataset should never be modified
$scope.list = angular.copy($scope.dataset);
$scope.options = {};
$scope.expandNode = function(n,$event) {
$event.stopPropagation();
n.toggle();
}
$scope.itemSelect = function(item) {
var rootVal = !item.isSelected;
HierarchyNodeService.selectChildren(item,rootVal)
HierarchyNodeService.findParent($scope.list[0],null,item,selectParent);
var s = _.compact(HierarchyNodeService.getAllChildren($scope.list[0],[]).map(function(c){ return c.isSelected && !c.items;}));
$scope.numSelected = s.length;
}
function selectParent(parent) {
var children = HierarchyNodeService.getAllChildren(parent,[]);
if(!children) return;
children = children.slice(1).map(function(c){ return c.isSelected;});
parent.isSelected = children.length === _.compact(children).length;
HierarchyNodeService.findParent($scope.list[0],null,parent,selectParent)
}
$scope.nodeStatus = function(node) {
var flattenedTree = getAllChildren(node,[]);
flattenedTree = flattenedTree.map(function(n){ return n.isSelected });
return flattenedTree.length === _.compact(flattenedTree);
}
},
link:function(scope,el,attr) {
scope.$watch('pastUsersFilter',function(nv){
if(_.isUndefined(nv)) return;
if(nv) {
HierarchyNodeService.trimLeafs(scope.list[0]);
} else {
scope.list = angular.copy(scope.dataset);
}
});
var inputTimeout;
var time = 300;
scope.$watch('searchValue',function(nv) {
if(!nv && nv !== '') {
return;
}
var previousDataset = angular.copy(scope.list);
var newData = (scope.searchValue === '') ? angular.copy(scope.dataset) : [HierarchyNodeService.treeSearch(angular.copy(scope.dataset[0]),scope.searchValue)];
if(newData.length === 1 && _.isEmpty(newData[0]) ) {
scope.emptyData = true;
return;
}
scope.emptyData = false;
if(_.isEqual(previousDataset,newData)) {
clearTimeout(inputTimeout);
return;
}
scope.list = newData;
$timeout.cancel(inputTimeout);
inputTimeout = $timeout(function() {
var els = document.querySelectorAll('[ui-tree-node]');
Array.prototype.forEach.call(els,function(el) {
el = angular.element(el);
var elScope = el.scope();
if(elScope.$modelValue.match) {
elScope.expand();
//loop through all parents and keep expanding until no more parents are found
var p = elScope.$parentNodeScope;
while(p) {
p.expand();
p = p.$parentNodeScope;
}
}
});
},500);
});
scope.$watch('list',function(nv,ov) {
if(!nv) return;
if(nv && !ov) { scope.$apply();}
//UPDATE SELECTED IDs FOR QUERY
//get the root node
var rootNode = nv[0];
//get all elements where isSelected == true
var a = HierarchyNodeService.getSelected(rootNode,[]);
//get the ids of each element
a = _.pluck(a,'id');
scope.selected = a;
},true);
}
}
})
}).call(this);
(function(){
angular.module('demoApp.services',[])
.service('HierarchyNodeService',function() {
function lowerCase(str) {
return str.split(' ').map(function(e){
return e.toString().toLowerCase();
}).join(' ');
}
function treeSearch(tree, query) {
if (!tree) {
return {};
}
if (lowerCase(tree.title).indexOf(lowerCase(query)) > -1) {
tree.match = true;
return tree;
}
var branches = _.reduce(tree.items, function(acc, leaf) {
var newLeaf = treeSearch(leaf, query);
if (!_.isEmpty(newLeaf)) {
acc.push(newLeaf);
}
return acc;
}, []);
if (_.size(branches) > 0) {
var trunk = _.omit(tree, 'items');
trunk.items = branches;
return trunk;
}
return {};
}
function getAllChildren(node,arr) {
if(!node) return;
arr.push(node);
if(node.items) {
//if the node has children call getSelected for each and concat to array
node.items.forEach(function(childNode) {
arr = arr.concat(getAllChildren(childNode,[]))
})
}
return arr;
}
function findParent(node,parent,targetNode,cb) {
if(_.isEqual(node,targetNode)) {
cb(parent);
return;
}
if(node.items) {
node.items.forEach(function(item){
findParent(item,node,targetNode,cb);
});
}
}
function getSelected(node,arr) {
//if(!node) return [];
//if this node is selected add to array
if(node.isSelected) {
arr.push(node);
return arr;
}
if(node.items) {
//if the node has children call getSelected for each and concat to array
node.items.forEach(function(childNode) {
arr = arr.concat(getSelected(childNode,[]))
})
}
return arr;
}
function selectChildren(children,val) {
//set as selected
children.isSelected = val;
if(children.items) {
//recursve to set all children as selected
children.items.forEach(function(el) {
selectChildren(el,val);
})
}
}
function trimLeafs(node,parent) {
if(!node.items) {
//da end of the road
delete parent.items;
} else {
node.items.forEach(function(item){
trimLeafs(item,node);
})
}
}
return {
getAllChildren:getAllChildren,
getSelected:getSelected,
selectChildren:selectChildren,
trimLeafs:trimLeafs,
treeSearch:treeSearch,
findParent:findParent
};
})
}).call(this);
(function() {
console.clear();
'use strict';
angular.module('demoApp', ['ui.tree', 'ui.bootstrap','demoApp.list','demoApp.directives','demoApp.services'])
.controller('MainCtrl', function($scope,$timeout,MyList,HierarchyNodeService) {
$scope.list = MyList;
$scope.getParentId = function(item) {
alert(JSON.stringify(item));
};
})
})();