Skip to content

Instantly share code, notes, and snippets.

@avishnyak
Created December 16, 2015 07:25
Show Gist options
  • Save avishnyak/d44ddd890a0df79e335c to your computer and use it in GitHub Desktop.
Save avishnyak/d44ddd890a0df79e335c to your computer and use it in GitHub Desktop.
Angular Drill-down Selector

Angular Drill-down Selector

Drill down with folders and selectable items in Angular.

Includes: Dynamic drill-down, select-all functionality, and breadcrumbs.

A Pen by Anton Vishnyak on CodePen.

License.

<body ng-app="app">
<div ng-controller="test">
<div class="panel panel-success" style="width: 400px;height:500px">
<drill-down src="tree" select-all selected="selectedItems" get-path="getPath" get-display-name="getDisplayName"></drill-down>
</div>
</div>
</body>
angular.module('app', []).controller('test', ['$scope', function($scope) {
$scope.tree = [{
name: "Admin",
scope: ""
}, {
name: "Manager",
scope: "California"
}, {
name: "Anton",
scope: "California|north"
}, {
name: "Mickey",
scope: "California|north"
}, {
name: "Laura",
scope: "California|south"
}, {
name: "Laura",
scope: "California|really long name|southern territory|Sietch Tabr"
}];
$scope.selectedItems = [];
$scope.getPath = (i) => i.scope;
$scope.getDisplayName = (i) => i.name;
}]);
angular.module('app').directive('drillDown', drillDownDirective);
drillDownDirective.$inject = [];
function drillDownDirective() {
let scope = {
delimeter: '@',
src: '=',
selected: '=',
getPath: '&',
getDisplayName: '&'
};
return {
restrict: 'E',
scope,
bindToController: true,
link,
template: `
<div class="dd-wrapper">
<div class="dd-breadcrumb-wrapper">
<ul class="breadcrumb">
<li><a href="#" ng-click="selectPath(0)"><span class="fa fa-folder-open"></span></a></li>
<li ng-repeat="p in selectedPathParts" ng-class="{ 'active': $last }">
<a href="#" ng-click="selectPath($index + 1)">{{:: p }}</a>
</li>
</ul>
<div class="list-group-item" ng-if="selectedItems.length > 0"><i>{{ selectedItems.length }} selected</i></div>
<div class="list-group-item" ng-if="selectedItems.length == 0">
<a href="#" ng-click="selectAllNodes()">Select All</a>
</div>
</div>
<div class="dd-menu-wrapper">
<ul class="dd-menu nav">
<li ng-repeat="item in items" ng-class="{ 'dd-parent': isParent(item) }">
<a href="#" ng-click="selectItem(item)">
{{:: item.name }}
<i ng-if="!isParent(item) && item.selected" class="fa fa-check pull-right"></i>
<i ng-if="isParent(item)" class="fa fa-chevron-right pull-right"></i>
</a>
</li>
</ul>
</div>
</div>
`
};
function link(scope, elem, attrs) {
const delimeter = scope.delimeter || '|';
scope.tree = {};
scope.selectedPath = '';
scope.selectedPathParts = [];
scope.items = [];
scope.selectedItems = [];
scope.selectAll = _.has(attrs, 'selectAll');
// Exposed functions
scope.isParent = isParent;
scope.selectItem = selectItem;
scope.selectPath = selectPath;
scope.selectAllNodes = selectAllNodes;
// Handle data
scope.$watch(() => scope.src, (n) => {
scope.tree = buildTree(scope.src, delimeter);
});
scope.$watch(() => scope.selectedPath, () => {
scope.items.splice(0, scope.items.length);
scope.selectedPathParts = scope.selectedPath.split(delimeter);
_.forEach(getItems(scope.selectedPath), (item) => {
scope.items.push(item);
});
});
function selectPath(index) {
scope.selectedPathParts.splice(index, scope.selectedPathParts.length);
scope.selectedPath = index === 0 ? '' : scope.selectedPathParts.join(delimeter);
scope.selectedItems.splice(0, scope.selectedItems.length);
}
function getItems(path) {
debugger;
let nodes = _(scope.tree[path] || [])
.map((item, i) => {
return {
path: path,
name: scope.getDisplayName()(item),
hasChildren: false,
index: i,
selected: false
};
})
.sortBy((i) => i.name)
.value(),
edges = _(scope.tree)
.keys()
.filter((i) => {
return path.length == 0 && i.length > 0 || i.startsWith(path + delimeter);
})
.map((i) => {
let nextSegment = i.indexOf(delimeter, path.length + 1);
return i.substring(path.length === 0 ? 0 : path.length + 1, nextSegment === -1 ? undefined : nextSegment);
})
.sortBy()
.uniq()
.map((e) => {
return {
path: path,
name: e,
hasChildren: true
}
})
.value();
return _.union(edges, nodes);
}
function selectAllNodes() {
let nodes = _.filter(scope.items, (i) => i.hasChildren === false);
if (nodes.length > 0) {
let treeItem = scope.tree[scope.selectedPath];
scope.selectedItems.splice(0, scope.selectedItems.length);
_.forEach(treeItem, (i) => {
scope.selectedItems.push(i);
});
_.forEach(nodes, (n) => {
n.selected = true;
});
}
}
function selectItem(item) {
if (item.hasChildren) {
scope.selectedPath = item.path === '' ? item.name : item.path + delimeter + item.name;
scope.selectedItems.splice(0, scope.selectedItems.length);
} else {
// Toggle selection
let i = _.findIndex(scope.selectedItems, (p) => {
return _.eq(p, scope.tree[item.path][item.index]);
});
if (i >= 0) {
scope.selectedItems.splice(i, 1);
item.selected = false;
} else {
scope.selectedItems.push(scope.tree[item.path][item.index]);
item.selected = true;
}
}
}
function isParent(item) {
return item.hasChildren;
}
// Helper functions
function buildTree(src, delimeter) {
return _.reduce(src, (acc, nxt) => {
let path = scope.getPath()(nxt),
node = acc[path];
if (_.isUndefined(node)) {
node = acc[path] = [];
}
node.push(nxt);
return acc;
}, {});
}
}
}
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-animate.js"></script>
@use cssnext;
@use postcss-nested;
.dd-wrapper {
position: relative;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
& ul, & li {
list-style: none;
}
& .breadcrumb {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0;
border-radius: 0;
& li {
display: inline;
&:nth-child(n+2) > a {
display: none;
}
&:nth-child(n+2):after {
position: relative;
left: -5px;
content: "\2026";
}
&:nth-last-child(-n+2) a {
display: inline;
}
&:nth-last-child(-n+2):after {
display: none;
}
}
}
& .dd-menu-wrapper {
overflow: scroll;
}
& .dd-menu {
& ul {
margin: 0;
position: absolute;
top: 0;
right: 0;
}
& a {
display: block;
}
}
}
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment