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.
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.
| <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" /> |