| (function ( angular ) { |
| 'use strict'; |
| |
| angular.module( 'treeControl', [] ) |
| .directive( 'treecontrol', ['$compile', function( $compile ) { |
| /** |
| * @param cssClass - the css class |
| * @param addClassProperty - should we wrap the class name with class="" |
| */ |
| function classIfDefined(cssClass, addClassProperty) { |
| if (cssClass) { |
| if (addClassProperty) |
| return 'class="' + cssClass + '"'; |
| else |
| return cssClass; |
| } |
| else |
| return ""; |
| } |
| |
| function ensureDefault(obj, prop, value) { |
| if (!obj.hasOwnProperty(prop)) |
| obj[prop] = value; |
| } |
| |
| return { |
| restrict: 'EA', |
| require: "treecontrol", |
| transclude: true, |
| scope: { |
| treeModel: "=", |
| selectedNode: "=?", |
| onSelection: "&", |
| options: "=?", |
| orderBy: "@", |
| reverseOrder: "@" |
| }, |
| controller: ['$scope', function( $scope ) { |
| |
| function defaultIsLeaf(node) { |
| return !node[$scope.options.nodeChildren] || node[$scope.options.nodeChildren].length === 0; |
| } |
| |
| function defaultEquality(a, b) { |
| if (a === undefined || b === undefined) |
| return false; |
| a = angular.copy(a); |
| a[$scope.options.nodeChildren] = []; |
| b = angular.copy(b); |
| b[$scope.options.nodeChildren] = []; |
| return angular.equals(a, b); |
| } |
| |
| $scope.options = $scope.options || {}; |
| ensureDefault($scope.options, "nodeChildren", "children"); |
| ensureDefault($scope.options, "dirSelectable", "true"); |
| ensureDefault($scope.options, "injectClasses", {}); |
| ensureDefault($scope.options.injectClasses, "ul", ""); |
| ensureDefault($scope.options.injectClasses, "li", ""); |
| ensureDefault($scope.options.injectClasses, "liSelected", ""); |
| ensureDefault($scope.options.injectClasses, "iExpanded", ""); |
| ensureDefault($scope.options.injectClasses, "iCollapsed", ""); |
| ensureDefault($scope.options.injectClasses, "iLeaf", ""); |
| ensureDefault($scope.options.injectClasses, "label", ""); |
| ensureDefault($scope.options.injectClasses, "labelSelected", ""); |
| ensureDefault($scope.options, "equality", defaultEquality); |
| ensureDefault($scope.options, "isLeaf", defaultIsLeaf); |
| |
| $scope.expandedNodes = {}; |
| if ($scope.options.defaultExpanded && angular.isArray($scope.options.defaultExpanded)) { |
| for (var i=0; i < $scope.options.defaultExpanded.length; i++) { |
| $scope.expandedNodes[""+i] = $scope.options.defaultExpanded[i]; |
| } |
| } |
| $scope.parentScopeOfTree = $scope.$parent; |
| |
| |
| $scope.headClass = function(node) { |
| var liSelectionClass = classIfDefined($scope.options.injectClasses.liSelected, false); |
| var injectSelectionClass = ""; |
| if (liSelectionClass && (this.$id == $scope.selectedScope)) |
| injectSelectionClass = " " + liSelectionClass; |
| if ($scope.options.isLeaf(node)) |
| return "tree-leaf" + injectSelectionClass; |
| if ($scope.expandedNodes[this.$id]) |
| return "tree-expanded" + injectSelectionClass; |
| else |
| return "tree-collapsed" + injectSelectionClass; |
| }; |
| |
| $scope.iBranchClass = function() { |
| if ($scope.expandedNodes[this.$id]) |
| return classIfDefined($scope.options.injectClasses.iExpanded); |
| else |
| return classIfDefined($scope.options.injectClasses.iCollapsed); |
| }; |
| |
| $scope.nodeExpanded = function() { |
| return !!$scope.expandedNodes[this.$id]; |
| }; |
| |
| $scope.selectNodeHead = function() { |
| $scope.expandedNodes[this.$id] = ($scope.expandedNodes[this.$id] === undefined ? this.node : undefined); |
| }; |
| |
| $scope.selectNodeLabel = function( selectedNode ){ |
| if (selectedNode[$scope.options.nodeChildren] && selectedNode[$scope.options.nodeChildren].length > 0 && |
| !$scope.options.dirSelectable) { |
| this.selectNodeHead(); |
| } |
| else { |
| $scope.selectedScope = this.$id; |
| $scope.selectedNode = selectedNode; |
| if ($scope.onSelection) |
| $scope.onSelection({node: selectedNode}); |
| } |
| }; |
| |
| $scope.selectedClass = function() { |
| var labelSelectionClass = classIfDefined($scope.options.injectClasses.labelSelected, false); |
| var injectSelectionClass = ""; |
| if (labelSelectionClass && (this.$id == $scope.selectedScope)) |
| injectSelectionClass = " " + labelSelectionClass; |
| |
| return (this.$id == $scope.selectedScope)?"tree-selected" + injectSelectionClass:""; |
| }; |
| |
| //tree template |
| var template = |
| '<ul '+classIfDefined($scope.options.injectClasses.ul, true)+'>' + |
| '<li ng-repeat="node in node.' + $scope.options.nodeChildren + ' | orderBy:orderBy:reverseOrder" ng-class="headClass(node)" '+classIfDefined($scope.options.injectClasses.li, true)+'>' + |
| '<i class="tree-branch-head" ng-class="iBranchClass()" ng-click="selectNodeHead(node)"></i>' + |
| '<i class="tree-leaf-head '+classIfDefined($scope.options.injectClasses.iLeaf, false)+'"></i>' + |
| '<div class="tree-label '+classIfDefined($scope.options.injectClasses.label, false)+'" ng-class="selectedClass()" ng-click="selectNodeLabel(node)" tree-transclude></div>' + |
| '<treeitem ng-if="nodeExpanded()"></treeitem>' + |
| '</li>' + |
| '</ul>'; |
| |
| return { |
| template: $compile(template) |
| } |
| }], |
| compile: function(element, attrs, childTranscludeFn) { |
| return function ( scope, element, attrs, treemodelCntr ) { |
| |
| scope.$watch("treeModel", function updateNodeOnRootScope(newValue) { |
| if (angular.isArray(newValue)) { |
| if (angular.isDefined(scope.node) && angular.equals(scope.node[scope.options.nodeChildren], newValue)) |
| return; |
| scope.node = {}; |
| scope.node[scope.options.nodeChildren] = newValue; |
| } |
| else { |
| if (angular.equals(scope.node, newValue)) |
| return; |
| scope.node = newValue; |
| } |
| }); |
| |
| //Rendering template for a root node |
| treemodelCntr.template( scope, function(clone) { |
| element.html('').append( clone ); |
| }); |
| // save the transclude function from compile (which is not bound to a scope as apposed to the one from link) |
| // we can fix this to work with the link transclude function with angular 1.2.6. as for angular 1.2.0 we need |
| // to keep using the compile function |
| scope.$treeTransclude = childTranscludeFn; |
| } |
| } |
| }; |
| }]) |
| .directive("treeitem", function() { |
| return { |
| restrict: 'E', |
| require: "^treecontrol", |
| link: function( scope, element, attrs, treemodelCntr) { |
| // Rendering template for the current node |
| treemodelCntr.template(scope, function(clone) { |
| element.html('').append(clone); |
| }); |
| } |
| } |
| }) |
| .directive("treeTransclude", function() { |
| return { |
| link: function(scope, element, attrs, controller) { |
| angular.forEach(scope.expandedNodes, function (node, id) { |
| if (scope.options.equality(node, scope.node)) { |
| scope.expandedNodes[scope.$id] = scope.node; |
| scope.expandedNodes[id] = undefined; |
| } |
| }); |
| if (scope.options.equality(scope.node, scope.selectedNode)) { |
| scope.selectNodeLabel(scope.node); |
| } |
| |
| // create a scope for the transclusion, whos parent is the parent of the tree control |
| scope.transcludeScope = scope.parentScopeOfTree.$new(); |
| scope.transcludeScope.node = scope.node; |
| scope.$on('$destroy', function() { |
| scope.transcludeScope.$destroy(); |
| }); |
| |
| scope.$treeTransclude(scope.transcludeScope, function(clone) { |
| element.empty(); |
| element.append(clone); |
| }); |
| } |
| } |
| }); |
| })( angular ); |