2 * @license Angular UI Tree v2.17.0
3 * (c) 2010-2016. https://github.com/angular-ui-tree/angular-ui-tree
9 angular.module('ui.tree', [])
10 .constant('treeConfig', {
11 treeClass: 'angular-ui-tree',
12 emptyTreeClass: 'angular-ui-tree-empty',
13 hiddenClass: 'angular-ui-tree-hidden',
14 nodesClass: 'angular-ui-tree-nodes',
15 nodeClass: 'angular-ui-tree-node',
16 handleClass: 'angular-ui-tree-handle',
17 placeholderClass: 'angular-ui-tree-placeholder',
18 dragClass: 'angular-ui-tree-drag',
21 defaultCollapsed: false
29 angular.module('ui.tree')
31 .controller('TreeHandleController', ['$scope', '$element',
32 function ($scope, $element) {
35 $scope.$element = $element;
36 $scope.$nodeScope = null;
37 $scope.$type = 'uiTreeHandle';
46 angular.module('ui.tree')
47 .controller('TreeNodeController', ['$scope', '$element',
48 function ($scope, $element) {
51 $scope.$element = $element;
52 $scope.$modelValue = null; // Model value for node;
53 $scope.$parentNodeScope = null; // uiTreeNode Scope of parent node;
54 $scope.$childNodesScope = null; // uiTreeNodes Scope of child nodes.
55 $scope.$parentNodesScope = null; // uiTreeNodes Scope of parent nodes.
56 $scope.$treeScope = null; // uiTree scope
57 $scope.$handleScope = null; // it's handle scope
58 $scope.$type = 'uiTreeNode';
59 $scope.$$allowNodeDrop = false;
60 $scope.collapsed = false;
61 $scope.expandOnHover = false;
63 $scope.init = function (controllersArr) {
64 var treeNodesCtrl = controllersArr[0];
65 $scope.$treeScope = controllersArr[1] ? controllersArr[1].scope : null;
67 // find the scope of it's parent node
68 $scope.$parentNodeScope = treeNodesCtrl.scope.$nodeScope;
69 // modelValue for current node
70 $scope.$modelValue = treeNodesCtrl.scope.$modelValue[$scope.$index];
71 $scope.$parentNodesScope = treeNodesCtrl.scope;
72 treeNodesCtrl.scope.initSubNode($scope); // init sub nodes
74 $element.on('$destroy', function () {
75 treeNodesCtrl.scope.destroySubNode($scope); // destroy sub nodes
79 $scope.index = function () {
80 return $scope.$parentNodesScope.$modelValue.indexOf($scope.$modelValue);
83 $scope.dragEnabled = function () {
84 return !($scope.$treeScope && !$scope.$treeScope.dragEnabled);
87 $scope.isSibling = function (targetNode) {
88 return $scope.$parentNodesScope == targetNode.$parentNodesScope;
91 $scope.isChild = function (targetNode) {
92 var nodes = $scope.childNodes();
93 return nodes && nodes.indexOf(targetNode) > -1;
96 $scope.prev = function () {
97 var index = $scope.index();
99 return $scope.siblings()[index - 1];
104 $scope.siblings = function () {
105 return $scope.$parentNodesScope.childNodes();
108 $scope.childNodesCount = function () {
109 return $scope.childNodes() ? $scope.childNodes().length : 0;
112 $scope.hasChild = function () {
113 return $scope.childNodesCount() > 0;
116 $scope.childNodes = function () {
117 return $scope.$childNodesScope && $scope.$childNodesScope.$modelValue ?
118 $scope.$childNodesScope.childNodes() :
122 $scope.accept = function (sourceNode, destIndex) {
123 return $scope.$childNodesScope &&
124 $scope.$childNodesScope.$modelValue &&
125 $scope.$childNodesScope.accept(sourceNode, destIndex);
128 $scope.remove = function () {
129 return $scope.$parentNodesScope.removeNode($scope);
132 $scope.toggle = function () {
133 $scope.collapsed = !$scope.collapsed;
134 $scope.$treeScope.$callbacks.toggle($scope.collapsed, $scope);
137 $scope.collapse = function () {
138 $scope.collapsed = true;
141 $scope.expand = function () {
142 $scope.collapsed = false;
145 $scope.depth = function () {
146 var parentNode = $scope.$parentNodeScope;
148 return parentNode.depth() + 1;
154 * Returns the depth of the deepest subtree under this node
155 * @param scope a TreeNodesController scope object
156 * @returns Depth of all nodes *beneath* this node. If scope belongs to a leaf node, the
157 * result is 0 (it has no subtree).
159 function countSubTreeDepth(scope) {
160 var thisLevelDepth = 0,
161 childNodes = scope.childNodes(),
165 if (!childNodes || childNodes.length === 0) {
168 for (i = childNodes.length - 1; i >= 0 ; i--) {
169 childNode = childNodes[i],
170 childDepth = 1 + countSubTreeDepth(childNode);
171 thisLevelDepth = Math.max(thisLevelDepth, childDepth);
173 return thisLevelDepth;
176 $scope.maxSubDepth = function () {
177 return $scope.$childNodesScope ? countSubTreeDepth($scope.$childNodesScope) : 0;
186 angular.module('ui.tree')
188 .controller('TreeNodesController', ['$scope', '$element',
189 function ($scope, $element) {
192 $scope.$element = $element;
193 $scope.$modelValue = null;
194 $scope.$nodeScope = null; // the scope of node which the nodes belongs to
195 $scope.$treeScope = null;
196 $scope.$type = 'uiTreeNodes';
197 $scope.$nodesMap = {};
199 $scope.nodropEnabled = false;
201 $scope.cloneEnabled = false;
203 $scope.initSubNode = function (subNode) {
204 if (!subNode.$modelValue) {
207 $scope.$nodesMap[subNode.$modelValue.$$hashKey] = subNode;
210 $scope.destroySubNode = function (subNode) {
211 if (!subNode.$modelValue) {
214 $scope.$nodesMap[subNode.$modelValue.$$hashKey] = null;
217 $scope.accept = function (sourceNode, destIndex) {
218 return $scope.$treeScope.$callbacks.accept(sourceNode, $scope, destIndex);
221 $scope.beforeDrag = function (sourceNode) {
222 return $scope.$treeScope.$callbacks.beforeDrag(sourceNode);
225 $scope.isParent = function (node) {
226 return node.$parentNodesScope == $scope;
229 $scope.hasChild = function () {
230 return $scope.$modelValue.length > 0;
233 $scope.safeApply = function (fn) {
234 var phase = this.$root.$$phase;
235 if (phase == '$apply' || phase == '$digest') {
236 if (fn && (typeof (fn) === 'function')) {
244 $scope.removeNode = function (node) {
245 var index = $scope.$modelValue.indexOf(node.$modelValue);
247 $scope.safeApply(function () {
248 $scope.$modelValue.splice(index, 1)[0];
250 return $scope.$treeScope.$callbacks.removed(node);
255 $scope.insertNode = function (index, nodeData) {
256 $scope.safeApply(function () {
257 $scope.$modelValue.splice(index, 0, nodeData);
261 $scope.childNodes = function () {
263 if ($scope.$modelValue) {
264 for (i = 0; i < $scope.$modelValue.length; i++) {
265 nodes.push($scope.$nodesMap[$scope.$modelValue[i].$$hashKey]);
271 $scope.depth = function () {
272 if ($scope.$nodeScope) {
273 return $scope.$nodeScope.depth();
275 return 0; // if it has no $nodeScope, it's root
278 // check if depth limit has reached
279 $scope.outOfDepth = function (sourceNode) {
280 var maxDepth = $scope.maxDepth || $scope.$treeScope.maxDepth;
282 return $scope.depth() + sourceNode.maxSubDepth() + 1 > maxDepth;
294 angular.module('ui.tree')
296 .controller('TreeController', ['$scope', '$element',
297 function ($scope, $element) {
300 $scope.$element = $element;
301 $scope.$nodesScope = null; // root nodes
302 $scope.$type = 'uiTree';
303 $scope.$emptyElm = null;
304 $scope.$callbacks = null;
306 $scope.dragEnabled = true;
307 $scope.emptyPlaceholderEnabled = true;
309 $scope.dragDelay = 0;
310 $scope.cloneEnabled = false;
311 $scope.nodropEnabled = false;
313 // Check if it's a empty tree
314 $scope.isEmpty = function () {
315 return ($scope.$nodesScope && $scope.$nodesScope.$modelValue
316 && $scope.$nodesScope.$modelValue.length === 0);
319 // add placeholder to empty tree
320 $scope.place = function (placeElm) {
321 $scope.$nodesScope.$element.append(placeElm);
322 $scope.$emptyElm.remove();
325 this.resetEmptyElement = function () {
326 if ((!$scope.$nodesScope.$modelValue || $scope.$nodesScope.$modelValue.length === 0) &&
327 $scope.emptyPlaceholderEnabled) {
328 $element.append($scope.$emptyElm);
330 $scope.$emptyElm.remove();
334 $scope.resetEmptyElement = this.resetEmptyElement;
342 angular.module('ui.tree')
343 .directive('uiTree', ['treeConfig', '$window',
344 function (treeConfig, $window) {
348 controller: 'TreeController',
349 link: function (scope, element, attrs, ctrl) {
359 angular.extend(config, treeConfig);
360 if (config.treeClass) {
361 element.addClass(config.treeClass);
364 if (element.prop('tagName').toLowerCase() === 'table') {
365 scope.$emptyElm = angular.element($window.document.createElement('tr'));
366 $trElm = element.find('tr');
367 // If we can find a tr, then we can use its td children as the empty element colspan.
368 if ($trElm.length > 0) {
369 emptyElmColspan = angular.element($trElm).children().length;
371 // If not, by setting a huge colspan we make sure it takes full width.
372 emptyElmColspan = 1000000;
374 tdElm = angular.element($window.document.createElement('td'))
375 .attr('colspan', emptyElmColspan);
376 scope.$emptyElm.append(tdElm);
378 scope.$emptyElm = angular.element($window.document.createElement('div'));
381 if (config.emptyTreeClass) {
382 scope.$emptyElm.addClass(config.emptyTreeClass);
385 scope.$watch('$nodesScope.$modelValue.length', function (val) {
386 if (!angular.isNumber(val)) {
390 ctrl.resetEmptyElement();
393 scope.$watch(attrs.dragEnabled, function (val) {
394 if ((typeof val) == 'boolean') {
395 scope.dragEnabled = val;
399 scope.$watch(attrs.emptyPlaceholderEnabled, function (val) {
400 if ((typeof val) == 'boolean') {
401 scope.emptyPlaceholderEnabled = val;
402 ctrl.resetEmptyElement();
406 scope.$watch(attrs.nodropEnabled, function (val) {
407 if ((typeof val) == 'boolean') {
408 scope.nodropEnabled = val;
412 scope.$watch(attrs.cloneEnabled, function (val) {
413 if ((typeof val) == 'boolean') {
414 scope.cloneEnabled = val;
418 scope.$watch(attrs.maxDepth, function (val) {
419 if ((typeof val) == 'number') {
420 scope.maxDepth = val;
424 scope.$watch(attrs.dragDelay, function (val) {
425 if ((typeof val) == 'number') {
426 scope.dragDelay = val;
431 * Callback checks if the destination node can accept the dragged node.
432 * By default, ui-tree will check that 'data-nodrop-enabled' is not set for the
433 * destination ui-tree-nodes, and that the 'max-depth' attribute will not be exceeded
434 * if it is set on the ui-tree or ui-tree-nodes.
435 * This callback can be overridden, but callers must manually enforce nodrop and max-depth
436 * themselves if they need those to be enforced.
437 * @param sourceNodeScope Scope of the ui-tree-node being dragged
438 * @param destNodesScope Scope of the ui-tree-nodes where the node is hovering
439 * @param destIndex Index in the destination nodes array where the source node will drop
440 * @returns {boolean} True if the node is permitted to be dropped here
442 callbacks.accept = function (sourceNodeScope, destNodesScope, destIndex) {
443 return !(destNodesScope.nodropEnabled || destNodesScope.$treeScope.nodropEnabled || destNodesScope.outOfDepth(sourceNodeScope));
446 callbacks.beforeDrag = function (sourceNodeScope) {
450 callbacks.expandTimeoutStart = function()
455 callbacks.expandTimeoutCancel = function()
460 callbacks.expandTimeoutEnd = function()
465 callbacks.removed = function (node) {
470 * Callback is fired when a node is successfully dropped in a new location
473 callbacks.dropped = function (event) {
478 * Callback is fired each time the user starts dragging a node
481 callbacks.dragStart = function (event) {
486 * Callback is fired each time a dragged node is moved with the mouse/touch.
489 callbacks.dragMove = function (event) {
494 * Callback is fired when the tree exits drag mode. If the user dropped a node, the drop may have been
495 * accepted or reverted.
498 callbacks.dragStop = function (event) {
503 * Callback is fired when a user drops a node (but prior to processing the drop action)
504 * beforeDrop can return a Promise, truthy, or falsy (returning nothing is falsy).
505 * If it returns falsy, or a resolve Promise, the node move is accepted
506 * If it returns truthy, or a rejected Promise, the node move is reverted
508 * @returns {Boolean|Promise} Truthy (or rejected Promise) to cancel node move; falsy (or resolved promise)
510 callbacks.beforeDrop = function (event) {
515 * Callback is fired when a user toggles node (but after processing the toggle action)
516 * @param sourceNodeScope
519 callbacks.toggle = function (collapsed, sourceNodeScope) {
523 scope.$watch(attrs.uiTree, function (newVal, oldVal) {
524 angular.forEach(newVal, function (value, key) {
525 if (callbacks[key]) {
526 if (typeof value === 'function') {
527 callbacks[key] = value;
532 scope.$callbacks = callbacks;
545 angular.module('ui.tree')
546 .directive('uiTreeHandle', ['treeConfig',
547 function (treeConfig) {
549 require: '^uiTreeNode',
552 controller: 'TreeHandleController',
553 link: function (scope, element, attrs, treeNodeCtrl) {
555 angular.extend(config, treeConfig);
556 if (config.handleClass) {
557 element.addClass(config.handleClass);
559 // connect with the tree node.
560 if (scope != treeNodeCtrl.scope) {
561 scope.$nodeScope = treeNodeCtrl.scope;
562 treeNodeCtrl.scope.$handleScope = scope;
573 angular.module('ui.tree')
575 .directive('uiTreeNode', ['treeConfig', 'UiTreeHelper', '$window', '$document', '$timeout', '$q',
576 function (treeConfig, UiTreeHelper, $window, $document, $timeout, $q) {
578 require: ['^uiTreeNodes', '^uiTree'],
580 controller: 'TreeNodeController',
581 link: function (scope, element, attrs, controllersArr) {
582 // todo startPos is unused
584 hasTouch = 'ontouchstart' in window,
585 startPos, firstMoving, dragInfo, pos,
586 placeElm, hiddenPlaceElm, dragElm,
588 elements, // As a parameter for callbacks
592 body = document.body,
593 html = document.documentElement,
607 unbindDragMoveEvents,
613 angular.extend(config, treeConfig);
614 if (config.nodeClass) {
615 element.addClass(config.nodeClass);
617 scope.init(controllersArr);
619 scope.collapsed = !!UiTreeHelper.getNodeAttribute(scope, 'collapsed') || treeConfig.defaultCollapsed;
620 scope.expandOnHover = !!UiTreeHelper.getNodeAttribute(scope, 'expandOnHover');
621 scope.sourceOnly = scope.nodropEnabled || scope.$treeScope.nodropEnabled;
623 scope.$watch(attrs.collapsed, function (val) {
624 if ((typeof val) == 'boolean') {
625 scope.collapsed = val;
629 scope.$watch('collapsed', function (val) {
630 UiTreeHelper.setNodeAttribute(scope, 'collapsed', val);
631 attrs.$set('collapsed', val);
634 scope.$watch(attrs.expandOnHover, function(val) {
635 if ((typeof val) == 'boolean') {
636 scope.expandOnHover = val;
640 scope.$watch('expandOnHover', function (val) {
641 UiTreeHelper.setNodeAttribute(scope, 'expandOnHover', val);
642 attrs.$set('expandOnHover', val);
645 scope.$on('angular-ui-tree:collapse-all', function () {
646 scope.collapsed = true;
649 scope.$on('angular-ui-tree:expand-all', function () {
650 scope.collapsed = false;
654 * Called when the user has grabbed a node and started dragging it
657 dragStart = function (e) {
658 // disable right click
659 if (!hasTouch && (e.button === 2 || e.which === 3)) {
663 // event has already fired in other scope
664 if (e.uiTreeDragging || (e.originalEvent && e.originalEvent.uiTreeDragging)) {
668 // the node being dragged
669 var eventElm = angular.element(e.target),
670 isHandleChild, cloneElm, eventElmTagName, tagName,
671 eventObj, tdElm, hStyle,
675 // if the target element is a child element of a ui-tree-handle,
676 // use the containing handle element as target element
677 isHandleChild = UiTreeHelper.treeNodeHandlerContainerOfElement(eventElm);
679 eventElm = angular.element(isHandleChild);
682 cloneElm = element.clone();
683 isTreeNode = UiTreeHelper.elementIsTreeNode(eventElm);
684 isTreeNodeHandle = UiTreeHelper.elementIsTreeNodeHandle(eventElm);
686 if (!isTreeNode && !isTreeNodeHandle) {
690 if (isTreeNode && UiTreeHelper.elementContainsTreeNodeHandler(eventElm)) {
694 eventElmTagName = eventElm.prop('tagName').toLowerCase();
695 if (eventElmTagName == 'input' ||
696 eventElmTagName == 'textarea' ||
697 eventElmTagName == 'button' ||
698 eventElmTagName == 'select') { // if it's a input or button, ignore it
702 // check if it or it's parents has a 'data-nodrag' attribute
703 el = angular.element(e.target);
704 while (el && el[0] && el[0] !== element) {
705 if (UiTreeHelper.nodrag(el)) { // if the node mark as `nodrag`, DONOT drag it.
711 if (!scope.beforeDrag(scope)) {
715 e.uiTreeDragging = true; // stop event bubbling
716 if (e.originalEvent) {
717 e.originalEvent.uiTreeDragging = true;
720 eventObj = UiTreeHelper.eventObj(e);
723 dragInfo = UiTreeHelper.dragInfo(scope);
725 tagName = element.prop('tagName');
727 if (tagName.toLowerCase() === 'tr') {
728 placeElm = angular.element($window.document.createElement(tagName));
729 tdElm = angular.element($window.document.createElement('td'))
730 .addClass(config.placeholderClass)
731 .attr('colspan', element[0].children.length);
732 placeElm.append(tdElm);
734 placeElm = angular.element($window.document.createElement(tagName))
735 .addClass(config.placeholderClass);
737 hiddenPlaceElm = angular.element($window.document.createElement(tagName));
738 if (config.hiddenClass) {
739 hiddenPlaceElm.addClass(config.hiddenClass);
742 pos = UiTreeHelper.positionStarted(eventObj, element);
743 placeElm.css('height', UiTreeHelper.height(element) + 'px');
745 dragElm = angular.element($window.document.createElement(scope.$parentNodesScope.$element.prop('tagName')))
746 .addClass(scope.$parentNodesScope.$element.attr('class')).addClass(config.dragClass);
747 dragElm.css('width', UiTreeHelper.width(element) + 'px');
748 dragElm.css('z-index', 9999);
750 // Prevents cursor to change rapidly in Opera 12.16 and IE when dragging an element
751 hStyle = (element[0].querySelector('.angular-ui-tree-handle') || element[0]).currentStyle;
753 document.body.setAttribute('ui-tree-cursor', $document.find('body').css('cursor') || '');
754 $document.find('body').css({'cursor': hStyle.cursor + '!important'});
757 if (scope.sourceOnly) {
758 placeElm.css('display', 'none');
760 element.after(placeElm);
761 element.after(hiddenPlaceElm);
762 if (dragInfo.isClone() && scope.sourceOnly) {
763 dragElm.append(cloneElm);
765 dragElm.append(element);
768 $document.find('body').append(dragElm);
771 'left': eventObj.pageX - pos.offsetX + 'px',
772 'top': eventObj.pageY - pos.offsetY + 'px'
775 placeholder: placeElm,
779 bindDragMoveEvents();
780 // Fire dragStart callback
781 scope.$apply(function () {
782 scope.$treeScope.$callbacks.dragStart(dragInfo.eventArgs(elements, pos));
785 document_height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
786 document_width = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth);
789 dragMove = function (e) {
790 var eventObj = UiTreeHelper.eventObj(e),
812 if ($window.getSelection) {
813 $window.getSelection().removeAllRanges();
814 } else if ($window.document.selection) {
815 $window.document.selection.empty();
818 leftElmPos = eventObj.pageX - pos.offsetX;
819 topElmPos = eventObj.pageY - pos.offsetY;
821 //dragElm can't leave the screen on the left
822 if (leftElmPos < 0) {
826 //dragElm can't leave the screen on the top
831 //dragElm can't leave the screen on the bottom
832 if ((topElmPos + 10) > document_height) {
833 topElmPos = document_height - 10;
836 //dragElm can't leave the screen on the right
837 if ((leftElmPos + 10) > document_width) {
838 leftElmPos = document_width - 10;
842 'left': leftElmPos + 'px',
843 'top': topElmPos + 'px'
846 top_scroll = window.pageYOffset || $window.document.documentElement.scrollTop;
847 bottom_scroll = top_scroll + (window.innerHeight || $window.document.clientHeight || $window.document.clientHeight);
849 // to scroll down if cursor y-position is greater than the bottom position the vertical scroll
850 if (bottom_scroll < eventObj.pageY && bottom_scroll < document_height) {
851 scrollDownBy = Math.min(document_height - bottom_scroll, 10);
852 window.scrollBy(0, scrollDownBy);
855 // to scroll top if cursor y-position is less than the top position the vertical scroll
856 if (top_scroll > eventObj.pageY) {
857 window.scrollBy(0, -10);
860 UiTreeHelper.positionMoved(e, pos, firstMoving);
866 // check if add it as a child node first
867 // todo decrease is unused
868 decrease = (UiTreeHelper.offset(dragElm).left - UiTreeHelper.offset(placeElm).left) >= config.threshold;
870 targetX = eventObj.pageX - ($window.pageXOffset ||
871 $window.document.body.scrollLeft ||
872 $window.document.documentElement.scrollLeft) -
873 ($window.document.documentElement.clientLeft || 0);
875 targetY = eventObj.pageY - ($window.pageYOffset ||
876 $window.document.body.scrollTop ||
877 $window.document.documentElement.scrollTop) -
878 ($window.document.documentElement.clientTop || 0);
880 // Select the drag target. Because IE does not support CSS 'pointer-events: none', it will always
881 // pick the drag element itself as the target. To prevent this, we hide the drag element while
882 // selecting the target.
883 if (angular.isFunction(dragElm.hide)) {
886 displayElm = dragElm[0].style.display;
887 dragElm[0].style.display = 'none';
890 // when using elementFromPoint() inside an iframe, you have to call
891 // elementFromPoint() twice to make sure IE8 returns the correct value
892 $window.document.elementFromPoint(targetX, targetY);
894 targetElm = angular.element($window.document.elementFromPoint(targetX, targetY));
896 // if the target element is a child element of a ui-tree-handle,
897 // use the containing handle element as target element
898 isHandleChild = UiTreeHelper.treeNodeHandlerContainerOfElement(targetElm);
900 targetElm = angular.element(isHandleChild);
903 if (angular.isFunction(dragElm.show)) {
906 dragElm[0].style.display = displayElm;
909 outOfBounds = !UiTreeHelper.elementIsTreeNodeHandle(targetElm) &&
910 !UiTreeHelper.elementIsTreeNode(targetElm) &&
911 !UiTreeHelper.elementIsTreeNodes(targetElm) &&
912 !UiTreeHelper.elementIsTree(targetElm) &&
913 !UiTreeHelper.elementIsPlaceholder(targetElm);
915 // Detect out of bounds condition, update drop target display, and prevent drop
918 // Remove the placeholder
921 // If the target was an empty tree, replace the empty element placeholder
923 treeScope.resetEmptyElement();
929 if (pos.dirAx && pos.distAxX >= config.levelThreshold) {
932 // increase horizontal level if previous sibling exists and is not collapsed
934 prev = dragInfo.prev();
935 if (prev && !prev.collapsed
936 && prev.accept(scope, prev.childNodesCount())) {
937 prev.$childNodesScope.$element.append(placeElm);
938 dragInfo.moveTo(prev.$childNodesScope, prev.childNodes(), prev.childNodesCount());
942 // decrease horizontal level
944 // we can't decrease a level if an item preceeds the current one
945 next = dragInfo.next();
947 target = dragInfo.parentNode(); // As a sibling of it's parent node
949 && target.$parentNodesScope.accept(scope, target.index() + 1)) {
950 target.$element.after(placeElm);
951 dragInfo.moveTo(target.$parentNodesScope, target.siblings(), target.index() + 1);
959 if (UiTreeHelper.elementIsTree(targetElm)) {
960 targetNode = targetElm.controller('uiTree').scope;
961 } else if (UiTreeHelper.elementIsTreeNodeHandle(targetElm)) {
962 targetNode = targetElm.controller('uiTreeHandle').scope;
963 } else if (UiTreeHelper.elementIsTreeNode(targetElm)) {
964 targetNode = targetElm.controller('uiTreeNode').scope;
965 } else if (UiTreeHelper.elementIsTreeNodes(targetElm)) {
966 targetNode = targetElm.controller('uiTreeNodes').scope;
967 } else if (UiTreeHelper.elementIsPlaceholder(targetElm)) {
968 targetNode = targetElm.controller('uiTreeNodes').scope;
969 } else if (targetElm.controller('uiTreeNode')) {
970 // is a child element of a node
971 targetNode = targetElm.controller('uiTreeNode').scope;
974 // check it's new position
980 // Show the placeholder if it was hidden for nodrop-enabled and this is a new tree
981 if (targetNode.$treeScope && !targetNode.$parent.nodropEnabled && !targetNode.$treeScope.nodropEnabled) {
982 placeElm.css('display', '');
985 if (targetNode.$type == 'uiTree' && targetNode.dragEnabled) {
986 isEmpty = targetNode.isEmpty(); // Check if it's empty tree
989 if (targetNode.$type == 'uiTreeHandle') {
990 targetNode = targetNode.$nodeScope;
993 if (targetNode.$type != 'uiTreeNode'
994 && !isEmpty) { // Check if it is a uiTreeNode or it's an empty tree
998 // if placeholder move from empty tree, reset it.
999 if (treeScope && placeElm.parent()[0] != treeScope.$element[0]) {
1000 treeScope.resetEmptyElement();
1004 if (isEmpty) { // it's an empty tree
1005 treeScope = targetNode;
1006 if (targetNode.$nodesScope.accept(scope, 0)) {
1007 targetNode.place(placeElm);
1008 dragInfo.moveTo(targetNode.$nodesScope, targetNode.$nodesScope.childNodes(), 0);
1010 } else if (targetNode.dragEnabled()) { // drag enabled
1011 if (angular.isDefined(scope.expandTimeoutOn) && scope.expandTimeoutOn !== targetNode.id) {
1012 $timeout.cancel(scope.expandTimeout);
1013 delete scope.expandTimeout;
1014 delete scope.expandTimeoutOn;
1016 scope.$callbacks.expandTimeoutCancel();
1019 if (targetNode.collapsed) {
1020 if (scope.expandOnHover === true || (angular.isNumber(scope.expandOnHover) && scope.expandOnHover === 0)) {
1021 targetNode.collapsed = false;
1022 } else if (scope.expandOnHover !== false && angular.isNumber(scope.expandOnHover) && scope.expandOnHover > 0) {
1023 if (angular.isUndefined(scope.expandTimeoutOn)) {
1024 scope.expandTimeoutOn = targetNode.$id;
1026 scope.$callbacks.expandTimeoutStart();
1027 scope.expandTimeout = $timeout(function()
1029 scope.$callbacks.expandTimeoutEnd();
1030 targetNode.collapsed = false;
1031 }, scope.expandOnHover);
1036 targetElm = targetNode.$element; // Get the element of ui-tree-node
1037 targetOffset = UiTreeHelper.offset(targetElm);
1038 targetBefore = targetNode.horizontal ? eventObj.pageX < (targetOffset.left + UiTreeHelper.width(targetElm) / 2)
1039 : eventObj.pageY < (targetOffset.top + UiTreeHelper.height(targetElm) / 2);
1041 if (targetNode.$parentNodesScope.accept(scope, targetNode.index())) {
1043 targetElm[0].parentNode.insertBefore(placeElm[0], targetElm[0]);
1044 dragInfo.moveTo(targetNode.$parentNodesScope, targetNode.siblings(), targetNode.index());
1046 targetElm.after(placeElm);
1047 dragInfo.moveTo(targetNode.$parentNodesScope, targetNode.siblings(), targetNode.index() + 1);
1049 } else if (!targetBefore && targetNode.accept(scope, targetNode.childNodesCount())) { // we have to check if it can add the dragging node as a child
1050 targetNode.$childNodesScope.$element.append(placeElm);
1051 dragInfo.moveTo(targetNode.$childNodesScope, targetNode.childNodes(), targetNode.childNodesCount());
1058 scope.$apply(function () {
1059 scope.$treeScope.$callbacks.dragMove(dragInfo.eventArgs(elements, pos));
1064 dragEnd = function (e) {
1065 var dragEventArgs = dragInfo.eventArgs(elements, pos);
1067 unbindDragMoveEvents();
1069 $timeout.cancel(scope.expandTimeout);
1071 scope.$treeScope.$apply(function () {
1072 $q.when(scope.$treeScope.$callbacks.beforeDrop(dragEventArgs))
1073 // promise resolved (or callback didn't return false)
1074 .then(function (allowDrop) {
1075 if (allowDrop !== false && scope.$$allowNodeDrop && !outOfBounds) { // node drop accepted)
1077 // fire the dropped callback only if the move was successful
1078 scope.$treeScope.$callbacks.dropped(dragEventArgs);
1079 } else { // drop canceled - revert the node to its original position
1080 bindDragStartEvents();
1083 // promise rejected - revert the node to its original position
1084 .catch(function () {
1085 bindDragStartEvents();
1087 .finally(function () {
1088 hiddenPlaceElm.replaceWith(scope.$element);
1091 if (dragElm) { // drag element is attached to the mouse pointer
1095 scope.$treeScope.$callbacks.dragStop(dragEventArgs);
1096 scope.$$allowNodeDrop = false;
1099 // Restore cursor in Opera 12.16 and IE
1100 var oldCur = document.body.getAttribute('ui-tree-cursor');
1101 if (oldCur !== null) {
1102 $document.find('body').css({'cursor': oldCur});
1103 document.body.removeAttribute('ui-tree-cursor');
1109 dragStartEvent = function (e) {
1110 if (scope.dragEnabled()) {
1115 dragMoveEvent = function (e) {
1119 dragEndEvent = function (e) {
1120 scope.$$allowNodeDrop = true;
1124 dragCancelEvent = function (e) {
1128 dragDelay = (function () {
1132 exec: function (fn, ms) {
1137 to = $timeout(fn, ms);
1139 cancel: function () {
1140 $timeout.cancel(to);
1146 * Binds the mouse/touch events to enable drag start for this node
1148 bindDragStartEvents = function () {
1149 element.bind('touchstart mousedown', function (e) {
1150 dragDelay.exec(function () {
1152 }, scope.dragDelay || 0);
1154 element.bind('touchend touchcancel mouseup', function () {
1158 bindDragStartEvents();
1161 * Binds mouse/touch events that handle moving/dropping this dragged node
1163 bindDragMoveEvents = function () {
1164 angular.element($document).bind('touchend', dragEndEvent);
1165 angular.element($document).bind('touchcancel', dragEndEvent);
1166 angular.element($document).bind('touchmove', dragMoveEvent);
1167 angular.element($document).bind('mouseup', dragEndEvent);
1168 angular.element($document).bind('mousemove', dragMoveEvent);
1169 angular.element($document).bind('mouseleave', dragCancelEvent);
1173 * Unbinds mouse/touch events that handle moving/dropping this dragged node
1175 unbindDragMoveEvents = function () {
1176 angular.element($document).unbind('touchend', dragEndEvent);
1177 angular.element($document).unbind('touchcancel', dragEndEvent);
1178 angular.element($document).unbind('touchmove', dragMoveEvent);
1179 angular.element($document).unbind('mouseup', dragEndEvent);
1180 angular.element($document).unbind('mousemove', dragMoveEvent);
1181 angular.element($document).unbind('mouseleave', dragCancelEvent);
1184 keydownHandler = function (e) {
1185 if (e.keyCode == 27) {
1186 scope.$$allowNodeDrop = false;
1191 angular.element($window.document).bind('keydown', keydownHandler);
1193 //unbind handler that retains scope
1194 scope.$on('$destroy', function () {
1195 angular.element($window.document).unbind('keydown', keydownHandler);
1207 angular.module('ui.tree')
1208 .directive('uiTreeNodes', ['treeConfig', '$window',
1209 function (treeConfig) {
1211 require: ['ngModel', '?^uiTreeNode', '^uiTree'],
1214 controller: 'TreeNodesController',
1215 link: function (scope, element, attrs, controllersArr) {
1218 ngModel = controllersArr[0],
1219 treeNodeCtrl = controllersArr[1],
1220 treeCtrl = controllersArr[2];
1222 angular.extend(config, treeConfig);
1223 if (config.nodesClass) {
1224 element.addClass(config.nodesClass);
1228 treeNodeCtrl.scope.$childNodesScope = scope;
1229 scope.$nodeScope = treeNodeCtrl.scope;
1231 // find the root nodes if there is no parent node and have a parent ui-tree
1232 treeCtrl.scope.$nodesScope = scope;
1234 scope.$treeScope = treeCtrl.scope;
1237 ngModel.$render = function () {
1238 scope.$modelValue = ngModel.$modelValue;
1242 scope.$watch(function () {
1243 return attrs.maxDepth;
1245 if ((typeof val) == 'number') {
1246 scope.maxDepth = val;
1250 scope.$watch(function () {
1251 return attrs.nodropEnabled;
1252 }, function (newVal) {
1253 if ((typeof newVal) != 'undefined') {
1254 scope.nodropEnabled = true;
1258 attrs.$observe('horizontal', function (val) {
1259 scope.horizontal = ((typeof val) != 'undefined');
1271 angular.module('ui.tree')
1275 * @name ui.tree.service:UiTreeHelper
1276 * @requires ng.$document
1277 * @requires ng.$window
1282 .factory('UiTreeHelper', ['$document', '$window', 'treeConfig',
1283 function ($document, $window, treeConfig) {
1287 * A hashtable used to storage data of nodes
1292 setNodeAttribute: function (scope, attrName, val) {
1293 if (!scope.$modelValue) {
1296 var data = this.nodesData[scope.$modelValue.$$hashKey];
1299 this.nodesData[scope.$modelValue.$$hashKey] = data;
1301 data[attrName] = val;
1304 getNodeAttribute: function (scope, attrName) {
1305 if (!scope.$modelValue) {
1308 var data = this.nodesData[scope.$modelValue.$$hashKey];
1310 return data[attrName];
1317 * @methodOf ui.tree.service:$nodrag
1318 * @param {Object} targetElm angular element
1319 * @return {Bool} check if the node can be dragged.
1321 nodrag: function (targetElm) {
1322 if (typeof targetElm.attr('data-nodrag') != 'undefined') {
1323 return targetElm.attr('data-nodrag') !== 'false';
1329 * get the event object for touches
1330 * @param {[type]} e [description]
1331 * @return {[type]} [description]
1333 eventObj: function (e) {
1335 if (e.targetTouches !== undefined) {
1336 obj = e.targetTouches.item(0);
1337 } else if (e.originalEvent !== undefined && e.originalEvent.targetTouches !== undefined) {
1338 obj = e.originalEvent.targetTouches.item(0);
1343 dragInfo: function (node) {
1347 cloneModel: node.$treeScope.cloneEnabled === true ? angular.copy(node.$modelValue) : undefined,
1349 index: node.index(),
1350 nodesScope: node.$parentNodesScope
1352 index: node.index(),
1353 siblings: node.siblings().slice(0),
1354 parent: node.$parentNodesScope,
1356 // Move the node to a new position
1357 moveTo: function (parent, siblings, index) {
1358 this.parent = parent;
1359 this.siblings = siblings.slice(0);
1361 // If source node is in the target nodes
1362 var i = this.siblings.indexOf(this.source);
1364 this.siblings.splice(i, 1);
1365 if (this.source.index() < index) {
1370 this.siblings.splice(index, 0, this.source);
1374 parentNode: function () {
1375 return this.parent.$nodeScope;
1379 if (this.index > 0) {
1380 return this.siblings[this.index - 1];
1387 if (this.index < this.siblings.length - 1) {
1388 return this.siblings[this.index + 1];
1394 isClone: function () {
1395 return this.source.$treeScope.cloneEnabled === true;
1398 clonedNode: function (node) {
1399 return angular.copy(node);
1402 isDirty: function () {
1403 return this.source.$parentNodesScope != this.parent ||
1404 this.source.index() != this.index;
1407 isForeign: function () {
1408 return this.source.$treeScope !== this.parent.$treeScope;
1411 eventArgs: function (elements, pos) {
1413 source: this.sourceInfo,
1416 nodesScope: this.parent
1423 apply: function () {
1425 var nodeData = this.source.$modelValue;
1427 // nodrop enabled on tree or parent
1428 if (this.parent.nodropEnabled || this.parent.$treeScope.nodropEnabled) {
1432 // node was dropped in the same place - do nothing
1433 if (!this.isDirty()) {
1437 // cloneEnabled and cross-tree so copy and do not remove from source
1438 if (this.isClone() && this.isForeign()) {
1439 this.parent.insertNode(this.index, this.sourceInfo.cloneModel);
1440 } else { // Any other case, remove and reinsert
1441 this.source.remove();
1442 this.parent.insertNode(this.index, nodeData);
1450 * @name ui.tree#height
1451 * @methodOf ui.tree.service:UiTreeHelper
1454 * Get the height of an element.
1456 * @param {Object} element Angular element.
1457 * @returns {String} Height
1459 height: function (element) {
1460 return element.prop('scrollHeight');
1465 * @name ui.tree#width
1466 * @methodOf ui.tree.service:UiTreeHelper
1469 * Get the width of an element.
1471 * @param {Object} element Angular element.
1472 * @returns {String} Width
1474 width: function (element) {
1475 return element.prop('scrollWidth');
1480 * @name ui.tree#offset
1481 * @methodOf ui.nestedSortable.service:UiTreeHelper
1484 * Get the offset values of an element.
1486 * @param {Object} element Angular element.
1487 * @returns {Object} Object with properties width, height, top and left
1489 offset: function (element) {
1490 var boundingClientRect = element[0].getBoundingClientRect();
1493 width: element.prop('offsetWidth'),
1494 height: element.prop('offsetHeight'),
1495 top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop),
1496 left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft)
1502 * @name ui.tree#positionStarted
1503 * @methodOf ui.tree.service:UiTreeHelper
1506 * Get the start position of the target element according to the provided event properties.
1508 * @param {Object} e Event
1509 * @param {Object} target Target element
1510 * @returns {Object} Object with properties offsetX, offsetY, startX, startY, nowX and dirX.
1512 positionStarted: function (e, target) {
1517 if (e.originalEvent && e.originalEvent.touches && (e.originalEvent.touches.length > 0)) {
1518 pageX = e.originalEvent.touches[0].pageX;
1519 pageY = e.originalEvent.touches[0].pageY;
1521 pos.offsetX = pageX - this.offset(target).left;
1522 pos.offsetY = pageY - this.offset(target).top;
1523 pos.startX = pos.lastX = pageX;
1524 pos.startY = pos.lastY = pageY;
1525 pos.nowX = pos.nowY = pos.distX = pos.distY = pos.dirAx = 0;
1526 pos.dirX = pos.dirY = pos.lastDirX = pos.lastDirY = pos.distAxX = pos.distAxY = 0;
1530 positionMoved: function (e, pos, firstMoving) {
1531 var pageX = e.pageX,
1534 if (e.originalEvent && e.originalEvent.touches && (e.originalEvent.touches.length > 0)) {
1535 pageX = e.originalEvent.touches[0].pageX;
1536 pageY = e.originalEvent.touches[0].pageY;
1538 // mouse position last events
1539 pos.lastX = pos.nowX;
1540 pos.lastY = pos.nowY;
1542 // mouse position this events
1546 // distance mouse moved between events
1547 pos.distX = pos.nowX - pos.lastX;
1548 pos.distY = pos.nowY - pos.lastY;
1550 // direction mouse was moving
1551 pos.lastDirX = pos.dirX;
1552 pos.lastDirY = pos.dirY;
1554 // direction mouse is now moving (on both axis)
1555 pos.dirX = pos.distX === 0 ? 0 : pos.distX > 0 ? 1 : -1;
1556 pos.dirY = pos.distY === 0 ? 0 : pos.distY > 0 ? 1 : -1;
1558 // axis mouse is now moving on
1559 newAx = Math.abs(pos.distX) > Math.abs(pos.distY) ? 1 : 0;
1561 // do nothing on first move
1568 // calc distance moved on this axis (and direction)
1569 if (pos.dirAx !== newAx) {
1573 pos.distAxX += Math.abs(pos.distX);
1574 if (pos.dirX !== 0 && pos.dirX !== pos.lastDirX) {
1578 pos.distAxY += Math.abs(pos.distY);
1579 if (pos.dirY !== 0 && pos.dirY !== pos.lastDirY) {
1587 elementIsTreeNode: function (element) {
1588 return typeof element.attr('ui-tree-node') !== 'undefined';
1591 elementIsTreeNodeHandle: function (element) {
1592 return typeof element.attr('ui-tree-handle') !== 'undefined';
1594 elementIsTree: function (element) {
1595 return typeof element.attr('ui-tree') !== 'undefined';
1597 elementIsTreeNodes: function (element) {
1598 return typeof element.attr('ui-tree-nodes') !== 'undefined';
1600 elementIsPlaceholder: function (element) {
1601 return element.hasClass(treeConfig.placeholderClass);
1603 elementContainsTreeNodeHandler: function (element) {
1604 return element[0].querySelectorAll('[ui-tree-handle]').length >= 1;
1606 treeNodeHandlerContainerOfElement: function (element) {
1607 return findFirstParentElementWithAttribute('ui-tree-handle', element[0]);
1613 // TODO: optimize this loop
1614 function findFirstParentElementWithAttribute(attributeName, childObj) {
1615 // undefined if the mouse leaves the browser window
1616 if (childObj === undefined) {
1619 var testObj = childObj.parentNode,
1621 // check for setAttribute due to exception thrown by Firefox when a node is dragged outside the browser window
1622 res = (typeof testObj.setAttribute === 'function' && testObj.hasAttribute(attributeName)) ? testObj : null;
1623 while (testObj && typeof testObj.setAttribute === 'function' && !testObj.hasAttribute(attributeName)) {
1624 testObj = testObj.parentNode;
1626 if (testObj === document.documentElement) {