angular-ui-tree
Version:
An AngularJS UI component that can sort nested lists, provides drag & drop support and doesn't depend on jQuery
595 lines (509 loc) • 24.8 kB
JavaScript
(function () {
'use strict';
angular.module('ui.tree')
.directive('uiTreeNode', ['treeConfig', 'UiTreeHelper', '$window', '$document', '$timeout', '$q',
function (treeConfig, UiTreeHelper, $window, $document, $timeout, $q) {
return {
require: ['^uiTreeNodes', '^uiTree'],
restrict: 'A',
controller: 'TreeNodeController',
link: function (scope, element, attrs, controllersArr) {
// todo startPos is unused
var config = {},
hasTouch = 'ontouchstart' in window,
startPos, firstMoving, dragInfo, pos,
placeElm, hiddenPlaceElm, dragElm,
treeScope = null,
elements, // As a parameter for callbacks
dragDelaying = true,
dragStarted = false,
dragTimer = null,
body = document.body,
html = document.documentElement,
document_height,
document_width,
dragStart,
tagName,
dragMove,
dragEnd,
dragStartEvent,
dragMoveEvent,
dragEndEvent,
dragCancelEvent,
dragDelay,
bindDragStartEvents,
bindDragMoveEvents,
unbindDragMoveEvents,
keydownHandler,
outOfBounds,
isHandleChild,
el;
angular.extend(config, treeConfig);
if (config.nodeClass) {
element.addClass(config.nodeClass);
}
scope.init(controllersArr);
scope.collapsed = !!UiTreeHelper.getNodeAttribute(scope, 'collapsed') || treeConfig.defaultCollapsed;
scope.sourceOnly = scope.nodropEnabled || scope.$treeScope.nodropEnabled;
scope.$watch(attrs.collapsed, function (val) {
if ((typeof val) == 'boolean') {
scope.collapsed = val;
}
});
scope.$watch('collapsed', function (val) {
UiTreeHelper.setNodeAttribute(scope, 'collapsed', val);
attrs.$set('collapsed', val);
});
scope.$on('angular-ui-tree:collapse-all', function () {
scope.collapsed = true;
});
scope.$on('angular-ui-tree:expand-all', function () {
scope.collapsed = false;
});
/**
* Called when the user has grabbed a node and started dragging it
* @param e
*/
dragStart = function (e) {
// disable right click
if (!hasTouch && (e.button === 2 || e.which === 3)) {
return;
}
// event has already fired in other scope
if (e.uiTreeDragging || (e.originalEvent && e.originalEvent.uiTreeDragging)) {
return;
}
// the node being dragged
var eventElm = angular.element(e.target),
isHandleChild, cloneElm, eventElmTagName, tagName,
eventObj, tdElm, hStyle,
isTreeNode,
isTreeNodeHandle;
// if the target element is a child element of a ui-tree-handle,
// use the containing handle element as target element
isHandleChild = UiTreeHelper.treeNodeHandlerContainerOfElement(eventElm);
if (isHandleChild) {
eventElm = angular.element(isHandleChild);
}
cloneElm = element.clone();
isTreeNode = UiTreeHelper.elementIsTreeNode(eventElm);
isTreeNodeHandle = UiTreeHelper.elementIsTreeNodeHandle(eventElm);
if (!isTreeNode && !isTreeNodeHandle) {
return;
}
if (isTreeNode && UiTreeHelper.elementContainsTreeNodeHandler(eventElm)) {
return;
}
eventElmTagName = eventElm.prop('tagName').toLowerCase();
if (eventElmTagName == 'input' ||
eventElmTagName == 'textarea' ||
eventElmTagName == 'button' ||
eventElmTagName == 'select') { // if it's a input or button, ignore it
return;
}
// check if it or it's parents has a 'data-nodrag' attribute
el = angular.element(e.target);
while (el && el[0] && el[0] !== element) {
if (UiTreeHelper.nodrag(el)) { // if the node mark as `nodrag`, DONOT drag it.
return;
}
el = el.parent();
}
if (!scope.beforeDrag(scope)) {
return;
}
e.uiTreeDragging = true; // stop event bubbling
if (e.originalEvent) {
e.originalEvent.uiTreeDragging = true;
}
e.preventDefault();
eventObj = UiTreeHelper.eventObj(e);
firstMoving = true;
dragInfo = UiTreeHelper.dragInfo(scope);
tagName = element.prop('tagName');
if (tagName.toLowerCase() === 'tr') {
placeElm = angular.element($window.document.createElement(tagName));
tdElm = angular.element($window.document.createElement('td'))
.addClass(config.placeholderClass)
.attr('colspan', element[0].children.length);
placeElm.append(tdElm);
} else {
placeElm = angular.element($window.document.createElement(tagName))
.addClass(config.placeholderClass);
}
hiddenPlaceElm = angular.element($window.document.createElement(tagName));
if (config.hiddenClass) {
hiddenPlaceElm.addClass(config.hiddenClass);
}
pos = UiTreeHelper.positionStarted(eventObj, element);
placeElm.css('height', UiTreeHelper.height(element) + 'px');
dragElm = angular.element($window.document.createElement(scope.$parentNodesScope.$element.prop('tagName')))
.addClass(scope.$parentNodesScope.$element.attr('class')).addClass(config.dragClass);
dragElm.css('width', UiTreeHelper.width(element) + 'px');
dragElm.css('z-index', 9999);
// Prevents cursor to change rapidly in Opera 12.16 and IE when dragging an element
hStyle = (element[0].querySelector('.angular-ui-tree-handle') || element[0]).currentStyle;
if (hStyle) {
document.body.setAttribute('ui-tree-cursor', $document.find('body').css('cursor') || '');
$document.find('body').css({'cursor': hStyle.cursor + '!important'});
}
if (scope.sourceOnly) {
placeElm.css('display', 'none');
}
element.after(placeElm);
element.after(hiddenPlaceElm);
if (dragInfo.isClone() && scope.sourceOnly) {
dragElm.append(cloneElm);
} else {
dragElm.append(element);
}
$document.find('body').append(dragElm);
dragElm.css({
'left': eventObj.pageX - pos.offsetX + 'px',
'top': eventObj.pageY - pos.offsetY + 'px'
});
elements = {
placeholder: placeElm,
dragging: dragElm
};
bindDragMoveEvents();
// Fire dragStart callback
scope.$apply(function () {
scope.$treeScope.$callbacks.dragStart(dragInfo.eventArgs(elements, pos));
});
document_height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
document_width = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth);
};
dragMove = function (e) {
var eventObj = UiTreeHelper.eventObj(e),
prev,
next,
leftElmPos,
topElmPos,
top_scroll,
bottom_scroll,
target,
decrease,
targetX,
targetY,
displayElm,
targetNode,
targetElm,
isEmpty,
scrollDownBy,
targetOffset,
targetBefore;
if (dragElm) {
e.preventDefault();
if ($window.getSelection) {
$window.getSelection().removeAllRanges();
} else if ($window.document.selection) {
$window.document.selection.empty();
}
leftElmPos = eventObj.pageX - pos.offsetX;
topElmPos = eventObj.pageY - pos.offsetY;
//dragElm can't leave the screen on the left
if (leftElmPos < 0) {
leftElmPos = 0;
}
//dragElm can't leave the screen on the top
if (topElmPos < 0) {
topElmPos = 0;
}
//dragElm can't leave the screen on the bottom
if ((topElmPos + 10) > document_height) {
topElmPos = document_height - 10;
}
//dragElm can't leave the screen on the right
if ((leftElmPos + 10) > document_width) {
leftElmPos = document_width - 10;
}
dragElm.css({
'left': leftElmPos + 'px',
'top': topElmPos + 'px'
});
top_scroll = window.pageYOffset || $window.document.documentElement.scrollTop;
bottom_scroll = top_scroll + (window.innerHeight || $window.document.clientHeight || $window.document.clientHeight);
// to scroll down if cursor y-position is greater than the bottom position the vertical scroll
if (bottom_scroll < eventObj.pageY && bottom_scroll < document_height) {
scrollDownBy = Math.min(document_height - bottom_scroll, 10);
window.scrollBy(0, scrollDownBy);
}
// to scroll top if cursor y-position is less than the top position the vertical scroll
if (top_scroll > eventObj.pageY) {
window.scrollBy(0, -10);
}
UiTreeHelper.positionMoved(e, pos, firstMoving);
if (firstMoving) {
firstMoving = false;
return;
}
// check if add it as a child node first
// todo decrease is unused
decrease = (UiTreeHelper.offset(dragElm).left - UiTreeHelper.offset(placeElm).left) >= config.threshold;
targetX = eventObj.pageX - ($window.pageXOffset ||
$window.document.body.scrollLeft ||
$window.document.documentElement.scrollLeft) -
($window.document.documentElement.clientLeft || 0);
targetY = eventObj.pageY - ($window.pageYOffset ||
$window.document.body.scrollTop ||
$window.document.documentElement.scrollTop) -
($window.document.documentElement.clientTop || 0);
// Select the drag target. Because IE does not support CSS 'pointer-events: none', it will always
// pick the drag element itself as the target. To prevent this, we hide the drag element while
// selecting the target.
if (angular.isFunction(dragElm.hide)) {
dragElm.hide();
} else {
displayElm = dragElm[0].style.display;
dragElm[0].style.display = 'none';
}
// when using elementFromPoint() inside an iframe, you have to call
// elementFromPoint() twice to make sure IE8 returns the correct value
$window.document.elementFromPoint(targetX, targetY);
targetElm = angular.element($window.document.elementFromPoint(targetX, targetY));
// if the target element is a child element of a ui-tree-handle,
// use the containing handle element as target element
isHandleChild = UiTreeHelper.treeNodeHandlerContainerOfElement(targetElm);
if (isHandleChild) {
targetElm = angular.element(isHandleChild);
}
if (angular.isFunction(dragElm.show)) {
dragElm.show();
} else {
dragElm[0].style.display = displayElm;
}
outOfBounds = !UiTreeHelper.elementIsTreeNodeHandle(targetElm) &&
!UiTreeHelper.elementIsTreeNode(targetElm) &&
!UiTreeHelper.elementIsTreeNodes(targetElm) &&
!UiTreeHelper.elementIsTree(targetElm) &&
!UiTreeHelper.elementIsPlaceholder(targetElm);
// Detect out of bounds condition, update drop target display, and prevent drop
if (outOfBounds) {
// Remove the placeholder
placeElm.remove();
// If the target was an empty tree, replace the empty element placeholder
if (treeScope) {
treeScope.resetEmptyElement();
treeScope = null;
}
}
// move horizontal
if (pos.dirAx && pos.distAxX >= config.levelThreshold) {
pos.distAxX = 0;
// increase horizontal level if previous sibling exists and is not collapsed
if (pos.distX > 0) {
prev = dragInfo.prev();
if (prev && !prev.collapsed
&& prev.accept(scope, prev.childNodesCount())) {
prev.$childNodesScope.$element.append(placeElm);
dragInfo.moveTo(prev.$childNodesScope, prev.childNodes(), prev.childNodesCount());
}
}
// decrease horizontal level
if (pos.distX < 0) {
// we can't decrease a level if an item preceeds the current one
next = dragInfo.next();
if (!next) {
target = dragInfo.parentNode(); // As a sibling of it's parent node
if (target
&& target.$parentNodesScope.accept(scope, target.index() + 1)) {
target.$element.after(placeElm);
dragInfo.moveTo(target.$parentNodesScope, target.siblings(), target.index() + 1);
}
}
}
}
// move vertical
if (!pos.dirAx) {
if (UiTreeHelper.elementIsTree(targetElm)) {
targetNode = targetElm.controller('uiTree').scope;
} else if (UiTreeHelper.elementIsTreeNodeHandle(targetElm)) {
targetNode = targetElm.controller('uiTreeHandle').scope;
} else if (UiTreeHelper.elementIsTreeNode(targetElm)) {
targetNode = targetElm.controller('uiTreeNode').scope;
} else if (UiTreeHelper.elementIsTreeNodes(targetElm)) {
targetNode = targetElm.controller('uiTreeNodes').scope;
} else if (UiTreeHelper.elementIsPlaceholder(targetElm)) {
targetNode = targetElm.controller('uiTreeNodes').scope;
} else if (targetElm.controller('uiTreeNode')) {
// is a child element of a node
targetNode = targetElm.controller('uiTreeNode').scope;
}
// check it's new position
isEmpty = false;
if (!targetNode) {
return;
}
// Show the placeholder if it was hidden for nodrop-enabled and this is a new tree
if (targetNode.$treeScope && !targetNode.$parent.nodropEnabled && !targetNode.$treeScope.nodropEnabled) {
placeElm.css('display', '');
}
if (targetNode.$type == 'uiTree' && targetNode.dragEnabled) {
isEmpty = targetNode.isEmpty(); // Check if it's empty tree
}
if (targetNode.$type == 'uiTreeHandle') {
targetNode = targetNode.$nodeScope;
}
if (targetNode.$type != 'uiTreeNode'
&& !isEmpty) { // Check if it is a uiTreeNode or it's an empty tree
return;
}
// if placeholder move from empty tree, reset it.
if (treeScope && placeElm.parent()[0] != treeScope.$element[0]) {
treeScope.resetEmptyElement();
treeScope = null;
}
if (isEmpty) { // it's an empty tree
treeScope = targetNode;
if (targetNode.$nodesScope.accept(scope, 0)) {
targetNode.place(placeElm);
dragInfo.moveTo(targetNode.$nodesScope, targetNode.$nodesScope.childNodes(), 0);
}
} else if (targetNode.dragEnabled()) { // drag enabled
targetElm = targetNode.$element; // Get the element of ui-tree-node
targetOffset = UiTreeHelper.offset(targetElm);
targetBefore = targetNode.horizontal ? eventObj.pageX < (targetOffset.left + UiTreeHelper.width(targetElm) / 2)
: eventObj.pageY < (targetOffset.top + UiTreeHelper.height(targetElm) / 2);
if (targetNode.$parentNodesScope.accept(scope, targetNode.index())) {
if (targetBefore) {
targetElm[0].parentNode.insertBefore(placeElm[0], targetElm[0]);
dragInfo.moveTo(targetNode.$parentNodesScope, targetNode.siblings(), targetNode.index());
} else {
targetElm.after(placeElm);
dragInfo.moveTo(targetNode.$parentNodesScope, targetNode.siblings(), targetNode.index() + 1);
}
} else if (!targetBefore && targetNode.accept(scope, targetNode.childNodesCount())) { // we have to check if it can add the dragging node as a child
targetNode.$childNodesScope.$element.append(placeElm);
dragInfo.moveTo(targetNode.$childNodesScope, targetNode.childNodes(), targetNode.childNodesCount());
} else {
outOfBounds = true;
}
}
}
scope.$apply(function () {
scope.$treeScope.$callbacks.dragMove(dragInfo.eventArgs(elements, pos));
});
}
};
dragEnd = function (e) {
var dragEventArgs = dragInfo.eventArgs(elements, pos);
e.preventDefault();
unbindDragMoveEvents();
scope.$treeScope.$apply(function () {
$q.when(scope.$treeScope.$callbacks.beforeDrop(dragEventArgs))
// promise resolved (or callback didn't return false)
.then(function (allowDrop) {
if (allowDrop !== false && scope.$$allowNodeDrop && !outOfBounds) { // node drop accepted)
dragInfo.apply();
// fire the dropped callback only if the move was successful
scope.$treeScope.$callbacks.dropped(dragEventArgs);
} else { // drop canceled - revert the node to its original position
bindDragStartEvents();
}
})
// promise rejected - revert the node to its original position
.catch(function () {
bindDragStartEvents();
})
.finally(function () {
hiddenPlaceElm.replaceWith(scope.$element);
placeElm.remove();
if (dragElm) { // drag element is attached to the mouse pointer
dragElm.remove();
dragElm = null;
}
scope.$treeScope.$callbacks.dragStop(dragEventArgs);
scope.$$allowNodeDrop = false;
dragInfo = null;
// Restore cursor in Opera 12.16 and IE
var oldCur = document.body.getAttribute('ui-tree-cursor');
if (oldCur !== null) {
$document.find('body').css({'cursor': oldCur});
document.body.removeAttribute('ui-tree-cursor');
}
});
});
};
dragStartEvent = function (e) {
if (scope.dragEnabled()) {
dragStart(e);
}
};
dragMoveEvent = function (e) {
dragMove(e);
};
dragEndEvent = function (e) {
scope.$$allowNodeDrop = true;
dragEnd(e);
};
dragCancelEvent = function (e) {
dragEnd(e);
};
dragDelay = (function () {
var to;
return {
exec: function (fn, ms) {
if (!ms) {
ms = 0;
}
this.cancel();
to = $timeout(fn, ms);
},
cancel: function () {
$timeout.cancel(to);
}
};
})();
/**
* Binds the mouse/touch events to enable drag start for this node
*/
bindDragStartEvents = function () {
element.bind('touchstart mousedown', function (e) {
dragDelay.exec(function () {
dragStartEvent(e);
}, scope.dragDelay || 0);
});
element.bind('touchend touchcancel mouseup', function () {
dragDelay.cancel();
});
};
bindDragStartEvents();
/**
* Binds mouse/touch events that handle moving/dropping this dragged node
*/
bindDragMoveEvents = function () {
angular.element($document).bind('touchend', dragEndEvent);
angular.element($document).bind('touchcancel', dragEndEvent);
angular.element($document).bind('touchmove', dragMoveEvent);
angular.element($document).bind('mouseup', dragEndEvent);
angular.element($document).bind('mousemove', dragMoveEvent);
angular.element($document).bind('mouseleave', dragCancelEvent);
};
/**
* Unbinds mouse/touch events that handle moving/dropping this dragged node
*/
unbindDragMoveEvents = function () {
angular.element($document).unbind('touchend', dragEndEvent);
angular.element($document).unbind('touchcancel', dragEndEvent);
angular.element($document).unbind('touchmove', dragMoveEvent);
angular.element($document).unbind('mouseup', dragEndEvent);
angular.element($document).unbind('mousemove', dragMoveEvent);
angular.element($document).unbind('mouseleave', dragCancelEvent);
};
keydownHandler = function (e) {
if (e.keyCode == 27) {
scope.$$allowNodeDrop = false;
dragEnd(e);
}
};
angular.element($window.document).bind('keydown', keydownHandler);
//unbind handler that retains scope
scope.$on('$destroy', function () {
angular.element($window.document).unbind('keydown', keydownHandler);
});
}
};
}
]);
})();