/* commonjs package manager support (eg componentjs) */
if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){
module.exports = 'treeControl';
}
(function ( angular ) {
'use strict';
function createPath(startScope) {
return function path() {
var _path = [];
var scope = startScope;
var prevNode;
while (scope && scope.node !== startScope.synteticRoot) {
if (prevNode !== scope.node)
_path.push(scope.node);
prevNode = scope.node;
scope = scope.$parent;
}
return _path;
}
}
angular.module( 'treeControl', [] )
.constant('treeConfig', {
templateUrl: null
})
.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: "=?",
selectedNodes: "=?",
expandedNodes: "=?",
onSelection: "&",
onNodeToggle: "&",
options: "=?",
orderBy: "=?",
reverseOrder: "@",
filterExpression: "=?",
filterComparator: "=?"
},
controller: ['$scope', '$templateCache', '$interpolate', 'treeConfig', function( $scope, $templateCache, $interpolate, treeConfig ) {
function defaultIsLeaf(node) {
return !node[$scope.options.nodeChildren] || node[$scope.options.nodeChildren].length === 0;
}
function shallowCopy(src, dst) {
if (angular.isArray(src)) {
dst = dst || [];
for ( var i = 0; i < src.length; i++) {
dst[i] = src[i];
}
} else if (angular.isObject(src)) {
dst = dst || {};
for (var key in src) {
if (hasOwnProperty.call(src, key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) {
dst[key] = src[key];
}
}
}
return dst || src;
}
function defaultEquality(a, b) {
if (!a || !b )
return false;
a = shallowCopy(a);
a[$scope.options.nodeChildren] = [];
b = shallowCopy(b);
b[$scope.options.nodeChildren] = [];
return angular.equals(a, b);
}
function defaultIsSelectable() {
return true;
}
$scope.options = $scope.options || {};
ensureDefault($scope.options, "multiSelection", false);
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);
ensureDefault($scope.options, "allowDeselect", true);
ensureDefault($scope.options, "isSelectable", defaultIsSelectable);
$scope.selectedNodes = $scope.selectedNodes || [];
$scope.expandedNodes = $scope.expandedNodes || [];
$scope.expandedNodesMap = {};
for (var i=0; i < $scope.expandedNodes.length; i++) {
$scope.expandedNodesMap[""+i] = $scope.expandedNodes[i];
}
$scope.parentScopeOfTree = $scope.$parent;
function isSelectedNode(node) {
if (!$scope.options.multiSelection && ($scope.options.equality(node, $scope.selectedNode)))
return true;
else if ($scope.options.multiSelection && $scope.selectedNodes) {
for (var i = 0; (i < $scope.selectedNodes.length); i++) {
if ($scope.options.equality(node, $scope.selectedNodes[i])) {
return true;
}
}
return false;
}
}
$scope.headClass = function(node) {
var liSelectionClass = classIfDefined($scope.options.injectClasses.liSelected, false);
var injectSelectionClass = "";
if (liSelectionClass && isSelectedNode(node))
injectSelectionClass = " " + liSelectionClass;
if ($scope.options.isLeaf(node))
return "tree-leaf" + injectSelectionClass;
if ($scope.expandedNodesMap[this.$id])
return "tree-expanded" + injectSelectionClass;
else
return "tree-collapsed" + injectSelectionClass;
};
$scope.iBranchClass = function() {
if ($scope.expandedNodesMap[this.$id])
return classIfDefined($scope.options.injectClasses.iExpanded);
else
return classIfDefined($scope.options.injectClasses.iCollapsed);
};
$scope.nodeExpanded = function() {
return !!$scope.expandedNodesMap[this.$id];
};
$scope.selectNodeHead = function() {
var transcludedScope = this;
var expanding = $scope.expandedNodesMap[transcludedScope.$id] === undefined;
$scope.expandedNodesMap[transcludedScope.$id] = (expanding ? transcludedScope.node : undefined);
if (expanding) {
$scope.expandedNodes.push(transcludedScope.node);
}
else {
var index;
for (var i=0; (i < $scope.expandedNodes.length) && !index; i++) {
if ($scope.options.equality($scope.expandedNodes[i], transcludedScope.node)) {
index = i;
}
}
if (index !== undefined)
$scope.expandedNodes.splice(index, 1);
}
if ($scope.onNodeToggle) {
var parentNode = (transcludedScope.$parent.node === transcludedScope.synteticRoot)?null:transcludedScope.$parent.node;
var path = createPath(transcludedScope);
$scope.onNodeToggle({node: transcludedScope.node, $parentNode: parentNode, $path: path,
$index: transcludedScope.$index, $first: transcludedScope.$first, $middle: transcludedScope.$middle,
$last: transcludedScope.$last, $odd: transcludedScope.$odd, $even: transcludedScope.$even, expanded: expanding});
}
};
$scope.selectNodeLabel = function( selectedNode){
var transcludedScope = this;
if(!$scope.options.isLeaf(selectedNode) && (!$scope.options.dirSelectable || !$scope.options.isSelectable(selectedNode))) {
// Branch node is not selectable, expand
this.selectNodeHead();
}
else if($scope.options.isLeaf(selectedNode) && (!$scope.options.isSelectable(selectedNode))) {
// Leaf node is not selectable
return;
}
else {
var selected = false;
if ($scope.options.multiSelection) {
var pos = -1;
for (var i=0; i < $scope.selectedNodes.length; i++) {
if($scope.options.equality(selectedNode, $scope.selectedNodes[i])) {
pos = i;
break;
}
}
if (pos === -1) {
$scope.selectedNodes.push(selectedNode);
selected = true;
} else {
$scope.selectedNodes.splice(pos, 1);
}
} else {
if (!$scope.options.equality(selectedNode, $scope.selectedNode)) {
$scope.selectedNode = selectedNode;
selected = true;
}
else {
if ($scope.options.allowDeselect) {
$scope.selectedNode = undefined;
} else {
$scope.selectedNode = selectedNode;
selected = true;
}
}
}
if ($scope.onSelection) {
var parentNode = (transcludedScope.$parent.node === transcludedScope.synteticRoot)?null:transcludedScope.$parent.node;
var path = createPath(transcludedScope)
$scope.onSelection({node: selectedNode, selected: selected, $parentNode: parentNode, $path: path,
$index: transcludedScope.$index, $first: transcludedScope.$first, $middle: transcludedScope.$middle,
$last: transcludedScope.$last, $odd: transcludedScope.$odd, $even: transcludedScope.$even});
}
}
};
$scope.selectedClass = function() {
var isThisNodeSelected = isSelectedNode(this.node);
var labelSelectionClass = classIfDefined($scope.options.injectClasses.labelSelected, false);
var injectSelectionClass = "";
if (labelSelectionClass && isThisNodeSelected)
injectSelectionClass = " " + labelSelectionClass;
return isThisNodeSelected ? "tree-selected" + injectSelectionClass : "";
};
$scope.unselectableClass = function() {
var isThisNodeUnselectable = !$scope.options.isSelectable(this.node);
var labelUnselectableClass = classIfDefined($scope.options.injectClasses.labelUnselectable, false);
return isThisNodeUnselectable ? "tree-unselectable " + labelUnselectableClass : "";
};
//tree template
$scope.isReverse = function() {
return !($scope.reverseOrder === 'false' || $scope.reverseOrder === 'False' || $scope.reverseOrder === '' || $scope.reverseOrder === false);
};
$scope.orderByFunc = function() {
return $scope.orderBy;
};
// return "" + $scope.orderBy;
var templateOptions = {
orderBy: $scope.orderBy ? " | orderBy:orderByFunc():isReverse()" : '',
ulClass: classIfDefined($scope.options.injectClasses.ul, true),
nodeChildren: $scope.options.nodeChildren,
liClass: classIfDefined($scope.options.injectClasses.li, true),
iLeafClass: classIfDefined($scope.options.injectClasses.iLeaf, false),
labelClass: classIfDefined($scope.options.injectClasses.label, false)
};
var template;
var templateUrl = $scope.options.templateUrl || treeConfig.templateUrl;
if(templateUrl) {
template = $templateCache.get(templateUrl);
}
if(!template) {
template =
'<ul {{options.ulClass}} >' +
'<li ng-repeat="node in node.{{options.nodeChildren}} | filter:filterExpression:filterComparator {{options.orderBy}}" ng-class="headClass(node)" {{options.liClass}}' +
'set-node-to-data>' +
'<i class="tree-branch-head" ng-class="iBranchClass()" ng-click="selectNodeHead(node)"></i>' +
'<i class="tree-leaf-head {{options.iLeafClass}}"></i>' +
'<div class="tree-label {{options.labelClass}}" ng-class="[selectedClass(), unselectableClass()]" ng-click="selectNodeLabel(node)" tree-transclude></div>' +
'<treeitem ng-if="nodeExpanded()"></treeitem>' +
'</li>' +
'</ul>';
}
this.template = $compile($interpolate(template)({options: templateOptions}));
}],
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.synteticRoot = scope.node;
scope.node[scope.options.nodeChildren] = newValue;
}
else {
if (angular.equals(scope.node, newValue))
return;
scope.node = newValue;
}
});
scope.$watchCollection('expandedNodes', function(newValue, oldValue) {
var notFoundIds = 0;
var newExpandedNodesMap = {};
var $liElements = element.find('li');
var existingScopes = [];
// find all nodes visible on the tree and the scope $id of the scopes including them
angular.forEach($liElements, function(liElement) {
var $liElement = angular.element(liElement);
var liScope = {
$id: $liElement.data('scope-id'),
node: $liElement.data('node')
};
existingScopes.push(liScope);
});
// iterate over the newValue, the new expanded nodes, and for each find it in the existingNodesAndScopes
// if found, add the mapping $id -> node into newExpandedNodesMap
// if not found, add the mapping num -> node into newExpandedNodesMap
angular.forEach(newValue, function(newExNode) {
var found = false;
for (var i=0; (i < existingScopes.length) && !found; i++) {
var existingScope = existingScopes[i];
if (scope.options.equality(newExNode, existingScope.node)) {
newExpandedNodesMap[existingScope.$id] = existingScope.node;
found = true;
}
}
if (!found)
newExpandedNodesMap[notFoundIds++] = newExNode;
});
scope.expandedNodesMap = newExpandedNodesMap;
});
// scope.$watch('expandedNodesMap', function(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("setNodeToData", ['$parse', function($parse) {
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
$element.data('node', $scope.node);
$element.data('scope-id', $scope.$id);
}
};
}])
.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) {
if (!scope.options.isLeaf(scope.node)) {
angular.forEach(scope.expandedNodesMap, function (node, id) {
if (scope.options.equality(node, scope.node)) {
scope.expandedNodesMap[scope.$id] = scope.node;
scope.expandedNodesMap[id] = undefined;
}
});
}
if (!scope.options.multiSelection && scope.options.equality(scope.node, scope.selectedNode)) {
scope.selectedNode = scope.node;
} else if (scope.options.multiSelection) {
var newSelectedNodes = [];
for (var i = 0; (i < scope.selectedNodes.length); i++) {
if (scope.options.equality(scope.node, scope.selectedNodes[i])) {
newSelectedNodes.push(scope.node);
}
}
scope.selectedNodes = newSelectedNodes;
}
// 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.transcludeScope.$path = createPath(scope);
scope.transcludeScope.$parentNode = (scope.$parent.node === scope.synteticRoot)?null:scope.$parent.node;
scope.transcludeScope.$index = scope.$index;
scope.transcludeScope.$first = scope.$first;
scope.transcludeScope.$middle = scope.$middle;
scope.transcludeScope.$last = scope.$last;
scope.transcludeScope.$odd = scope.$odd;
scope.transcludeScope.$even = scope.$even;
scope.$on('$destroy', function() {
scope.transcludeScope.$destroy();
});
scope.$treeTransclude(scope.transcludeScope, function(clone) {
element.empty();
element.append(clone);
});
}
};
});
})( angular );
|