1867 lines
68 KiB
JavaScript
1867 lines
68 KiB
JavaScript
/**
|
|
* @license Angular UI Tree v2.22.5
|
|
* (c) 2010-2017. https://github.com/angular-ui-tree/angular-ui-tree
|
|
* License: MIT
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('ui.tree', [])
|
|
.constant('treeConfig', {
|
|
treeClass: 'angular-ui-tree',
|
|
emptyTreeClass: 'angular-ui-tree-empty',
|
|
dropzoneClass: 'angular-ui-tree-dropzone',
|
|
hiddenClass: 'angular-ui-tree-hidden',
|
|
nodesClass: 'angular-ui-tree-nodes',
|
|
nodeClass: 'angular-ui-tree-node',
|
|
handleClass: 'angular-ui-tree-handle',
|
|
placeholderClass: 'angular-ui-tree-placeholder',
|
|
dragClass: 'angular-ui-tree-drag',
|
|
dragThreshold: 3,
|
|
defaultCollapsed: false,
|
|
appendChildOnHover: true
|
|
});
|
|
|
|
})();
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('ui.tree')
|
|
|
|
.controller('TreeHandleController', ['$scope', '$element',
|
|
function ($scope, $element) {
|
|
this.scope = $scope;
|
|
|
|
$scope.$element = $element;
|
|
$scope.$nodeScope = null;
|
|
$scope.$type = 'uiTreeHandle';
|
|
|
|
}
|
|
]);
|
|
})();
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('ui.tree')
|
|
.controller('TreeNodeController', ['$scope', '$element',
|
|
function ($scope, $element) {
|
|
this.scope = $scope;
|
|
|
|
$scope.$element = $element;
|
|
$scope.$modelValue = null; // Model value for node;
|
|
$scope.$parentNodeScope = null; // uiTreeNode Scope of parent node;
|
|
$scope.$childNodesScope = null; // uiTreeNodes Scope of child nodes.
|
|
$scope.$parentNodesScope = null; // uiTreeNodes Scope of parent nodes.
|
|
$scope.$treeScope = null; // uiTree scope
|
|
$scope.$handleScope = null; // it's handle scope
|
|
$scope.$type = 'uiTreeNode';
|
|
$scope.$$allowNodeDrop = false;
|
|
$scope.collapsed = false;
|
|
$scope.expandOnHover = false;
|
|
|
|
//Called by uiTreeNode Directive on load.
|
|
$scope.init = function (controllersArr) {
|
|
var treeNodesCtrl = controllersArr[0];
|
|
$scope.$treeScope = controllersArr[1] ? controllersArr[1].scope : null;
|
|
|
|
//Find the scope of it's parent node.
|
|
$scope.$parentNodeScope = treeNodesCtrl.scope.$nodeScope;
|
|
|
|
//modelValue for current node.
|
|
$scope.$modelValue = treeNodesCtrl.scope.$modelValue[$scope.$index];
|
|
$scope.$parentNodesScope = treeNodesCtrl.scope;
|
|
|
|
//Init sub nodes.
|
|
treeNodesCtrl.scope.initSubNode($scope);
|
|
|
|
$element.on('$destroy', function () {
|
|
|
|
//Destroy sub nodes.
|
|
treeNodesCtrl.scope.destroySubNode($scope);
|
|
});
|
|
};
|
|
|
|
//Return the index of child node in parent node (nodesScope).
|
|
$scope.index = function () {
|
|
return $scope.$parentNodesScope.$modelValue.indexOf($scope.$modelValue);
|
|
};
|
|
|
|
$scope.dragEnabled = function () {
|
|
return !($scope.$treeScope && !$scope.$treeScope.dragEnabled);
|
|
};
|
|
|
|
$scope.isSibling = function (targetNode) {
|
|
return $scope.$parentNodesScope == targetNode.$parentNodesScope;
|
|
};
|
|
|
|
$scope.isChild = function (targetNode) {
|
|
var nodes = $scope.childNodes();
|
|
return nodes && nodes.indexOf(targetNode) > -1;
|
|
};
|
|
|
|
//TODO(jcarter): This method is on uiTreeHelper already.
|
|
$scope.prev = function () {
|
|
var index = $scope.index();
|
|
if (index > 0) {
|
|
return $scope.siblings()[index - 1];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
//Calls childNodes on parent.
|
|
$scope.siblings = function () {
|
|
return $scope.$parentNodesScope.childNodes();
|
|
};
|
|
|
|
$scope.childNodesCount = function () {
|
|
return $scope.childNodes() ? $scope.childNodes().length : 0;
|
|
};
|
|
|
|
$scope.hasChild = function () {
|
|
return $scope.childNodesCount() > 0;
|
|
};
|
|
|
|
$scope.childNodes = function () {
|
|
return $scope.$childNodesScope && $scope.$childNodesScope.$modelValue ?
|
|
$scope.$childNodesScope.childNodes() :
|
|
null;
|
|
};
|
|
|
|
$scope.accept = function (sourceNode, destIndex) {
|
|
return $scope.$childNodesScope &&
|
|
$scope.$childNodesScope.$modelValue &&
|
|
$scope.$childNodesScope.accept(sourceNode, destIndex);
|
|
};
|
|
|
|
$scope.remove = function () {
|
|
return $scope.$parentNodesScope.removeNode($scope);
|
|
};
|
|
|
|
$scope.toggle = function () {
|
|
$scope.collapsed = !$scope.collapsed;
|
|
$scope.$treeScope.$callbacks.toggle($scope.collapsed, $scope);
|
|
};
|
|
|
|
$scope.collapse = function () {
|
|
$scope.collapsed = true;
|
|
};
|
|
|
|
$scope.expand = function () {
|
|
$scope.collapsed = false;
|
|
};
|
|
|
|
$scope.depth = function () {
|
|
var parentNode = $scope.$parentNodeScope;
|
|
if (parentNode) {
|
|
return parentNode.depth() + 1;
|
|
}
|
|
return 1;
|
|
};
|
|
|
|
/**
|
|
* Returns the depth of the deepest subtree under this node
|
|
* @param scope a TreeNodesController scope object
|
|
* @returns Depth of all nodes *beneath* this node. If scope belongs to a leaf node, the
|
|
* result is 0 (it has no subtree).
|
|
*/
|
|
function countSubTreeDepth(scope) {
|
|
if (!scope) {
|
|
return 0;
|
|
}
|
|
var thisLevelDepth = 0,
|
|
childNodes = scope.childNodes(),
|
|
childNode,
|
|
childDepth,
|
|
i;
|
|
if (!childNodes || childNodes.length === 0) {
|
|
return 0;
|
|
}
|
|
for (i = childNodes.length - 1; i >= 0 ; i--) {
|
|
childNode = childNodes[i],
|
|
childDepth = 1 + countSubTreeDepth(childNode);
|
|
thisLevelDepth = Math.max(thisLevelDepth, childDepth);
|
|
}
|
|
return thisLevelDepth;
|
|
}
|
|
|
|
$scope.maxSubDepth = function () {
|
|
return $scope.$childNodesScope ? countSubTreeDepth($scope.$childNodesScope) : 0;
|
|
};
|
|
}
|
|
]);
|
|
})();
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('ui.tree')
|
|
|
|
.controller('TreeNodesController', ['$scope', '$element',
|
|
function ($scope, $element) {
|
|
this.scope = $scope;
|
|
|
|
$scope.$element = $element;
|
|
$scope.$modelValue = null;
|
|
$scope.$nodeScope = null; // the scope of node which the nodes belongs to
|
|
$scope.$treeScope = null;
|
|
$scope.$type = 'uiTreeNodes';
|
|
$scope.$nodesMap = {};
|
|
|
|
$scope.nodropEnabled = false;
|
|
$scope.maxDepth = 0;
|
|
$scope.cloneEnabled = false;
|
|
|
|
$scope.initSubNode = function (subNode) {
|
|
if (!subNode.$modelValue) {
|
|
return null;
|
|
}
|
|
$scope.$nodesMap[subNode.$modelValue.$$hashKey] = subNode;
|
|
};
|
|
|
|
$scope.destroySubNode = function (subNode) {
|
|
if (!subNode.$modelValue) {
|
|
return null;
|
|
}
|
|
$scope.$nodesMap[subNode.$modelValue.$$hashKey] = null;
|
|
};
|
|
|
|
$scope.accept = function (sourceNode, destIndex) {
|
|
return $scope.$treeScope.$callbacks.accept(sourceNode, $scope, destIndex);
|
|
};
|
|
|
|
$scope.beforeDrag = function (sourceNode) {
|
|
return $scope.$treeScope.$callbacks.beforeDrag(sourceNode);
|
|
};
|
|
|
|
$scope.isParent = function (node) {
|
|
return node.$parentNodesScope == $scope;
|
|
};
|
|
|
|
$scope.hasChild = function () {
|
|
return $scope.$modelValue.length > 0;
|
|
};
|
|
|
|
$scope.safeApply = function (fn) {
|
|
var phase = this.$root.$$phase;
|
|
if (phase == '$apply' || phase == '$digest') {
|
|
if (fn && (typeof (fn) === 'function')) {
|
|
fn();
|
|
}
|
|
} else {
|
|
this.$apply(fn);
|
|
}
|
|
};
|
|
|
|
//Called in apply method of UiTreeHelper.dragInfo.
|
|
$scope.removeNode = function (node) {
|
|
var index = $scope.$modelValue.indexOf(node.$modelValue);
|
|
if (index > -1) {
|
|
$scope.safeApply(function () {
|
|
$scope.$modelValue.splice(index, 1)[0];
|
|
});
|
|
return $scope.$treeScope.$callbacks.removed(node);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
//Called in apply method of UiTreeHelper.dragInfo.
|
|
$scope.insertNode = function (index, nodeData) {
|
|
$scope.safeApply(function () {
|
|
$scope.$modelValue.splice(index, 0, nodeData);
|
|
});
|
|
};
|
|
|
|
$scope.childNodes = function () {
|
|
var i, nodes = [];
|
|
if ($scope.$modelValue) {
|
|
for (i = 0; i < $scope.$modelValue.length; i++) {
|
|
nodes.push($scope.$nodesMap[$scope.$modelValue[i].$$hashKey]);
|
|
}
|
|
}
|
|
return nodes;
|
|
};
|
|
|
|
$scope.depth = function () {
|
|
if ($scope.$nodeScope) {
|
|
return $scope.$nodeScope.depth();
|
|
}
|
|
return 0; // if it has no $nodeScope, it's root
|
|
};
|
|
|
|
// check if depth limit has reached
|
|
$scope.outOfDepth = function (sourceNode) {
|
|
var maxDepth = $scope.maxDepth || $scope.$treeScope.maxDepth;
|
|
if (maxDepth > 0) {
|
|
return $scope.depth() + sourceNode.maxSubDepth() + 1 > maxDepth;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
}
|
|
]);
|
|
})();
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('ui.tree')
|
|
|
|
.controller('TreeController', ['$scope', '$element',
|
|
function ($scope, $element) {
|
|
this.scope = $scope;
|
|
|
|
$scope.$element = $element;
|
|
$scope.$nodesScope = null; // root nodes
|
|
$scope.$type = 'uiTree';
|
|
$scope.$emptyElm = null;
|
|
$scope.$dropzoneElm = null;
|
|
$scope.$callbacks = null;
|
|
|
|
$scope.dragEnabled = true;
|
|
$scope.emptyPlaceholderEnabled = true;
|
|
$scope.maxDepth = 0;
|
|
$scope.dragDelay = 0;
|
|
$scope.cloneEnabled = false;
|
|
$scope.nodropEnabled = false;
|
|
$scope.dropzoneEnabled = false;
|
|
|
|
// Check if it's a empty tree
|
|
$scope.isEmpty = function () {
|
|
return ($scope.$nodesScope && $scope.$nodesScope.$modelValue
|
|
&& $scope.$nodesScope.$modelValue.length === 0);
|
|
};
|
|
|
|
// add placeholder to empty tree
|
|
$scope.place = function (placeElm) {
|
|
$scope.$nodesScope.$element.append(placeElm);
|
|
$scope.$emptyElm.remove();
|
|
};
|
|
|
|
this.resetEmptyElement = function () {
|
|
if ((!$scope.$nodesScope.$modelValue || $scope.$nodesScope.$modelValue.length === 0) &&
|
|
$scope.emptyPlaceholderEnabled) {
|
|
$element.append($scope.$emptyElm);
|
|
} else {
|
|
$scope.$emptyElm.remove();
|
|
}
|
|
};
|
|
|
|
this.resetDropzoneElement = function () {
|
|
if ((!$scope.$nodesScope.$modelValue || $scope.$nodesScope.$modelValue.length !== 0) &&
|
|
$scope.dropzoneEnabled) {
|
|
$element.append($scope.$dropzoneElm);
|
|
} else {
|
|
$scope.$dropzoneElm.remove();
|
|
}
|
|
};
|
|
|
|
$scope.resetEmptyElement = this.resetEmptyElement;
|
|
$scope.resetDropzoneElement = this.resetDropzoneElement;
|
|
}
|
|
]);
|
|
})();
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('ui.tree')
|
|
.directive('uiTree', ['treeConfig', '$window',
|
|
function (treeConfig, $window) {
|
|
return {
|
|
restrict: 'A',
|
|
scope: true,
|
|
controller: 'TreeController',
|
|
link: function (scope, element, attrs, ctrl) {
|
|
var callbacks = {
|
|
accept: null,
|
|
beforeDrag: null
|
|
},
|
|
config = {},
|
|
tdElm,
|
|
$trElm,
|
|
emptyElmColspan;
|
|
|
|
//Adding configured class to uiTree.
|
|
angular.extend(config, treeConfig);
|
|
|
|
if (config.treeClass) {
|
|
element.addClass(config.treeClass);
|
|
}
|
|
|
|
//Determining if uiTree is on a table.
|
|
if (element.prop('tagName').toLowerCase() === 'table') {
|
|
scope.$emptyElm = angular.element($window.document.createElement('tr'));
|
|
$trElm = element.find('tr');
|
|
|
|
//If we can find a tr, then we can use its td children as the empty element colspan.
|
|
if ($trElm.length > 0) {
|
|
emptyElmColspan = angular.element($trElm).children().length;
|
|
} else {
|
|
|
|
//If not, by setting a huge colspan we make sure it takes full width.
|
|
//TODO(jcarter): Check for negative side effects.
|
|
emptyElmColspan = 1000000;
|
|
}
|
|
tdElm = angular.element($window.document.createElement('td'))
|
|
.attr('colspan', emptyElmColspan);
|
|
scope.$emptyElm.append(tdElm);
|
|
} else {
|
|
scope.$emptyElm = angular.element($window.document.createElement('div'));
|
|
scope.$dropzoneElm = angular.element($window.document.createElement('div'));
|
|
}
|
|
|
|
if (config.emptyTreeClass) {
|
|
scope.$emptyElm.addClass(config.emptyTreeClass);
|
|
}
|
|
if (config.dropzoneClass) {
|
|
scope.$dropzoneElm.addClass(config.dropzoneClass);
|
|
}
|
|
|
|
scope.$watch('$nodesScope.$modelValue.length', function (val) {
|
|
if (!angular.isNumber(val)) {
|
|
return;
|
|
}
|
|
|
|
ctrl.resetEmptyElement();
|
|
ctrl.resetDropzoneElement();
|
|
}, true);
|
|
|
|
scope.$watch(attrs.dragEnabled, function (val) {
|
|
if ((typeof val) == 'boolean') {
|
|
scope.dragEnabled = val;
|
|
}
|
|
});
|
|
|
|
scope.$watch(attrs.emptyPlaceholderEnabled, function (val) {
|
|
if ((typeof val) == 'boolean') {
|
|
scope.emptyPlaceholderEnabled = val;
|
|
ctrl.resetEmptyElement();
|
|
}
|
|
});
|
|
|
|
scope.$watch(attrs.nodropEnabled, function (val) {
|
|
if ((typeof val) == 'boolean') {
|
|
scope.nodropEnabled = val;
|
|
}
|
|
});
|
|
|
|
scope.$watch(attrs.dropzoneEnabled, function (val) {
|
|
if ((typeof val) == 'boolean') {
|
|
scope.dropzoneEnabled = val;
|
|
ctrl.resetDropzoneElement();
|
|
}
|
|
});
|
|
|
|
scope.$watch(attrs.cloneEnabled, function (val) {
|
|
if ((typeof val) == 'boolean') {
|
|
scope.cloneEnabled = val;
|
|
}
|
|
});
|
|
|
|
scope.$watch(attrs.maxDepth, function (val) {
|
|
if ((typeof val) == 'number') {
|
|
scope.maxDepth = val;
|
|
}
|
|
});
|
|
|
|
scope.$watch(attrs.dragDelay, function (val) {
|
|
if ((typeof val) == 'number') {
|
|
scope.dragDelay = val;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Callback checks if the destination node can accept the dragged node.
|
|
* By default, ui-tree will check that 'data-nodrop-enabled' is not set for the
|
|
* destination ui-tree-nodes, and that the 'max-depth' attribute will not be exceeded
|
|
* if it is set on the ui-tree or ui-tree-nodes.
|
|
* This callback can be overridden, but callers must manually enforce nodrop and max-depth
|
|
* themselves if they need those to be enforced.
|
|
* @param sourceNodeScope Scope of the ui-tree-node being dragged
|
|
* @param destNodesScope Scope of the ui-tree-nodes where the node is hovering
|
|
* @param destIndex Index in the destination nodes array where the source node will drop
|
|
* @returns {boolean} True if the node is permitted to be dropped here
|
|
*/
|
|
callbacks.accept = function (sourceNodeScope, destNodesScope, destIndex) {
|
|
return !(destNodesScope.nodropEnabled || destNodesScope.$treeScope.nodropEnabled || destNodesScope.outOfDepth(sourceNodeScope));
|
|
};
|
|
|
|
callbacks.beforeDrag = function (sourceNodeScope) {
|
|
return true;
|
|
};
|
|
|
|
callbacks.expandTimeoutStart = function()
|
|
{
|
|
|
|
};
|
|
|
|
callbacks.expandTimeoutCancel = function()
|
|
{
|
|
|
|
};
|
|
|
|
callbacks.expandTimeoutEnd = function()
|
|
{
|
|
|
|
};
|
|
|
|
callbacks.removed = function (node) {
|
|
|
|
};
|
|
|
|
/**
|
|
* Callback is fired when a node is successfully dropped in a new location
|
|
* @param event
|
|
*/
|
|
callbacks.dropped = function (event) {
|
|
|
|
};
|
|
|
|
/**
|
|
* Callback is fired each time the user starts dragging a node
|
|
* @param event
|
|
*/
|
|
callbacks.dragStart = function (event) {
|
|
|
|
};
|
|
|
|
/**
|
|
* Callback is fired each time a dragged node is moved with the mouse/touch.
|
|
* @param event
|
|
*/
|
|
callbacks.dragMove = function (event) {
|
|
|
|
};
|
|
|
|
/**
|
|
* Callback is fired when the tree exits drag mode. If the user dropped a node, the drop may have been
|
|
* accepted or reverted.
|
|
* @param event
|
|
*/
|
|
callbacks.dragStop = function (event) {
|
|
|
|
};
|
|
|
|
/**
|
|
* Callback is fired when a user drops a node (but prior to processing the drop action)
|
|
* beforeDrop can return a Promise, truthy, or falsy (returning nothing is falsy).
|
|
* If it returns falsy, or a resolve Promise, the node move is accepted
|
|
* If it returns truthy, or a rejected Promise, the node move is reverted
|
|
* @param event
|
|
* @returns {Boolean|Promise} Truthy (or rejected Promise) to cancel node move; falsy (or resolved promise)
|
|
*/
|
|
callbacks.beforeDrop = function (event) {
|
|
|
|
};
|
|
|
|
/**
|
|
* Callback is fired when a user toggles node (but after processing the toggle action)
|
|
* @param sourceNodeScope
|
|
* @param collapsed
|
|
*/
|
|
callbacks.toggle = function (collapsed, sourceNodeScope) {
|
|
|
|
};
|
|
|
|
scope.$watch(attrs.uiTree, function (newVal, oldVal) {
|
|
angular.forEach(newVal, function (value, key) {
|
|
if (callbacks[key]) {
|
|
if (typeof value === 'function') {
|
|
callbacks[key] = value;
|
|
}
|
|
}
|
|
});
|
|
|
|
scope.$callbacks = callbacks;
|
|
}, true);
|
|
|
|
|
|
}
|
|
};
|
|
}
|
|
]);
|
|
})();
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('ui.tree')
|
|
.directive('uiTreeHandle', ['treeConfig',
|
|
function (treeConfig) {
|
|
return {
|
|
require: '^uiTreeNode',
|
|
restrict: 'A',
|
|
scope: true,
|
|
controller: 'TreeHandleController',
|
|
link: function (scope, element, attrs, treeNodeCtrl) {
|
|
var config = {};
|
|
angular.extend(config, treeConfig);
|
|
if (config.handleClass) {
|
|
element.addClass(config.handleClass);
|
|
}
|
|
// connect with the tree node.
|
|
if (scope != treeNodeCtrl.scope) {
|
|
scope.$nodeScope = treeNodeCtrl.scope;
|
|
treeNodeCtrl.scope.$handleScope = scope;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
]);
|
|
})();
|
|
|
|
(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) {
|
|
var config = {},
|
|
hasTouch = 'ontouchstart' in window,
|
|
firstMoving,
|
|
dragInfo,
|
|
pos,
|
|
placeElm,
|
|
hiddenPlaceElm,
|
|
dragElm,
|
|
scrollContainerElm,
|
|
unhover,
|
|
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,
|
|
isHandleChild,
|
|
el,
|
|
isUiTreeRoot,
|
|
treeOfOrigin;
|
|
|
|
//Adding configured class to ui-tree-node.
|
|
angular.extend(config, treeConfig);
|
|
|
|
if (config.nodeClass) {
|
|
element.addClass(config.nodeClass);
|
|
}
|
|
|
|
//Call init function in nodeCtrl, sets parent node and sets up sub nodes.
|
|
scope.init(controllersArr);
|
|
|
|
scope.collapsed = !!UiTreeHelper.getNodeAttribute(scope, 'collapsed') || treeConfig.defaultCollapsed;
|
|
scope.expandOnHover = !!UiTreeHelper.getNodeAttribute(scope, 'expandOnHover');
|
|
scope.scrollContainer = UiTreeHelper.getNodeAttribute(scope, 'scrollContainer') || attrs.scrollContainer || null;
|
|
scope.sourceOnly = scope.nodropEnabled || scope.$treeScope.nodropEnabled;
|
|
|
|
scope.$watch(attrs.collapsed, function (val) {
|
|
if ((typeof val) == 'boolean') {
|
|
scope.collapsed = val;
|
|
}
|
|
});
|
|
|
|
//Watches to trigger behavior based on actions and settings.
|
|
scope.$watch('collapsed', function (val) {
|
|
UiTreeHelper.setNodeAttribute(scope, 'collapsed', val);
|
|
attrs.$set('collapsed', val);
|
|
});
|
|
|
|
scope.$watch(attrs.expandOnHover, function(val) {
|
|
if ((typeof val) === 'boolean' || (typeof val) === 'number') {
|
|
scope.expandOnHover = val;
|
|
}
|
|
});
|
|
|
|
scope.$watch('expandOnHover', function (val) {
|
|
UiTreeHelper.setNodeAttribute(scope, 'expandOnHover', val);
|
|
attrs.$set('expandOnHover', val);
|
|
});
|
|
|
|
attrs.$observe('scrollContainer', function(val) {
|
|
if ((typeof val) === 'string') {
|
|
scope.scrollContainer = val;
|
|
}
|
|
});
|
|
|
|
scope.$watch('scrollContainer', function(val) {
|
|
UiTreeHelper.setNodeAttribute(scope, 'scrollContainer', val);
|
|
attrs.$set('scrollContainer', val);
|
|
scrollContainerElm = document.querySelector(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 {MouseEvent} e event that is triggered by DOM.
|
|
* @return undefined?
|
|
*/
|
|
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 we are not triggering mousedown on our uiTree or any of it's parts, return.
|
|
if (!isTreeNode && !isTreeNodeHandle) {
|
|
return;
|
|
}
|
|
|
|
//If we are not triggering mousedown on our uiTree or any of it's parts, return.
|
|
if (isTreeNode && UiTreeHelper.elementContainsTreeNodeHandler(eventElm)) {
|
|
return;
|
|
}
|
|
|
|
//Dragging not allowed on inputs or buttons.
|
|
eventElmTagName = eventElm.prop('tagName').toLowerCase();
|
|
if (eventElmTagName == 'input' ||
|
|
eventElmTagName == 'textarea' ||
|
|
eventElmTagName == 'button' ||
|
|
eventElmTagName == 'select') {
|
|
return;
|
|
}
|
|
|
|
//Check if it or it's parents has a 'data-nodrag' attribute
|
|
el = angular.element(e.target);
|
|
isUiTreeRoot = el[0].attributes['ui-tree'];
|
|
while (el && el[0] && el[0] !== element && !isUiTreeRoot) {
|
|
|
|
//Checking that I can access attributes.
|
|
if (el[0].attributes) {
|
|
isUiTreeRoot = el[0].attributes['ui-tree'];
|
|
}
|
|
|
|
//If the node mark as `nodrag`, DONOT drag it.
|
|
if (UiTreeHelper.nodrag(el)) {
|
|
return;
|
|
}
|
|
el = el.parent();
|
|
}
|
|
|
|
//If users beforeDrag calback returns falsey, do not initiate.
|
|
if (!scope.beforeDrag(scope)) {
|
|
return;
|
|
}
|
|
|
|
//Set property checked at start of function to prevent running logic again.
|
|
e.uiTreeDragging = true;
|
|
if (e.originalEvent) {
|
|
e.originalEvent.uiTreeDragging = true;
|
|
}
|
|
e.preventDefault();
|
|
|
|
//Get original event if TouchEvent.
|
|
eventObj = UiTreeHelper.eventObj(e);
|
|
|
|
//Set boolean used to specify beginning of move.
|
|
firstMoving = true;
|
|
|
|
//Setting drag info properties and methods in scope of node being moved.
|
|
dragInfo = UiTreeHelper.dragInfo(scope);
|
|
|
|
//Setting original tree to adjust horizontal behavior in drag move.
|
|
treeOfOrigin = dragInfo.source.$treeScope.$id;
|
|
|
|
//Determine tage name of element ui-tree-node is on.
|
|
tagName = element.prop('tagName');
|
|
|
|
if (tagName.toLowerCase() === 'tr') {
|
|
|
|
//Create a new table column as placeholder.
|
|
placeElm = angular.element($window.document.createElement(tagName));
|
|
|
|
//Create a column placeholder and set colspan to whole row length.
|
|
tdElm = angular.element($window.document.createElement('td'))
|
|
.addClass(config.placeholderClass)
|
|
.attr('colspan', element[0].children.length);
|
|
placeElm.append(tdElm);
|
|
} else {
|
|
|
|
//If not a table just duplicate element and add placeholder class.
|
|
placeElm = angular.element($window.document.createElement(tagName))
|
|
.addClass(config.placeholderClass);
|
|
}
|
|
|
|
//Create a hidden placeholder and add class from config.
|
|
hiddenPlaceElm = angular.element($window.document.createElement(tagName));
|
|
if (config.hiddenClass) {
|
|
hiddenPlaceElm.addClass(config.hiddenClass);
|
|
}
|
|
|
|
//Getting starting position of element being moved.
|
|
pos = UiTreeHelper.positionStarted(eventObj, element);
|
|
placeElm.css('height', element.prop('offsetHeight') + 'px');
|
|
|
|
//Creating drag element to represent node.
|
|
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 tree is sourceOnly (noDragDrop) don't show placeholder when moving about it.
|
|
if (scope.sourceOnly) {
|
|
placeElm.css('display', 'none');
|
|
}
|
|
|
|
//Insert placeholder.
|
|
element.after(placeElm);
|
|
element.after(hiddenPlaceElm);
|
|
if (dragInfo.isClone() && scope.sourceOnly) {
|
|
dragElm.append(cloneElm);
|
|
} else {
|
|
dragElm.append(element);
|
|
}
|
|
|
|
//Create drag element.
|
|
$document.find('body').append(dragElm);
|
|
|
|
//Set drag elements position on screen.
|
|
dragElm.css({
|
|
'left': eventObj.pageX - pos.offsetX + 'px',
|
|
'top': eventObj.pageY - pos.offsetY + 'px'
|
|
});
|
|
elements = {
|
|
placeholder: placeElm,
|
|
dragging: dragElm
|
|
};
|
|
|
|
//Create all drag/move bindings.
|
|
bindDragMoveEvents();
|
|
|
|
//Fire dragStart callback.
|
|
scope.$apply(function () {
|
|
scope.$treeScope.$callbacks.dragStart(dragInfo.eventArgs(elements, pos));
|
|
});
|
|
|
|
//Get bounds of document.
|
|
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,
|
|
scrollContainerElmRect,
|
|
target,
|
|
targetX,
|
|
targetY,
|
|
displayElm,
|
|
targetNode,
|
|
targetElm,
|
|
isEmpty,
|
|
scrollDownBy,
|
|
scrollUpBy,
|
|
targetOffset,
|
|
targetBefore,
|
|
moveWithinTree,
|
|
targetBeforeBuffer,
|
|
targetHeight,
|
|
targetChildElm,
|
|
targetChildHeight,
|
|
isDropzone;
|
|
|
|
//If check ensures that drag element was created.
|
|
if (dragElm) {
|
|
e.preventDefault();
|
|
|
|
//Deselect anything (text, etc.) that was selected when move began.
|
|
if ($window.getSelection) {
|
|
$window.getSelection().removeAllRanges();
|
|
} else if ($window.document.selection) {
|
|
$window.document.selection.empty();
|
|
}
|
|
|
|
//Get top left positioning of element being moved.
|
|
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;
|
|
}
|
|
|
|
//Updating element being moved css.
|
|
dragElm.css({
|
|
'left': leftElmPos + 'px',
|
|
'top': topElmPos + 'px'
|
|
});
|
|
|
|
if (scrollContainerElm) {
|
|
//Getting position to top and bottom of container element.
|
|
scrollContainerElmRect = scrollContainerElm.getBoundingClientRect();
|
|
top_scroll = scrollContainerElm.scrollTop;
|
|
bottom_scroll = top_scroll + scrollContainerElm.clientHeight;
|
|
|
|
//To scroll down if cursor y-position is greater than the bottom position of the container vertical scroll
|
|
if (scrollContainerElmRect.bottom < eventObj.clientY && bottom_scroll < scrollContainerElm.scrollHeight) {
|
|
scrollDownBy = Math.min(scrollContainerElm.scrollHeight - bottom_scroll, 10);
|
|
scrollContainerElm.scrollTop += scrollDownBy;
|
|
}
|
|
|
|
//To scroll top if cursor y-position is less than the top position of the container vertical scroll
|
|
if (scrollContainerElmRect.top > eventObj.clientY && top_scroll > 0) {
|
|
scrollUpBy = Math.min(top_scroll, 10);
|
|
scrollContainerElm.scrollTop -= scrollUpBy;
|
|
}
|
|
} else {
|
|
//Getting position to top and bottom of page.
|
|
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 of the window 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 of the window vertical scroll
|
|
if (top_scroll > eventObj.pageY) {
|
|
scrollUpBy = Math.min(top_scroll, 10);
|
|
window.scrollBy(0, -scrollUpBy);
|
|
}
|
|
}
|
|
|
|
//Calling service to update position coordinates based on move.
|
|
UiTreeHelper.positionMoved(e, pos, firstMoving);
|
|
if (firstMoving) {
|
|
firstMoving = false;
|
|
return;
|
|
}
|
|
|
|
//Setting X point for elementFromPoint.
|
|
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
|
|
//MDN: The elementFromPoint() method of the Document interface returns the topmost element at the specified coordinates.
|
|
$window.document.elementFromPoint(targetX, targetY);
|
|
|
|
//Set target element (element in specified x/y coordinates).
|
|
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;
|
|
}
|
|
|
|
//Assigning scope to target you are moving draggable over.
|
|
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 (UiTreeHelper.elementIsDropzone(targetElm)) {
|
|
targetNode = targetElm.controller('uiTree').scope;
|
|
isDropzone = true;
|
|
} else if (targetElm.controller('uiTreeNode')) {
|
|
//Is a child element of a node.
|
|
targetNode = targetElm.controller('uiTreeNode').scope;
|
|
}
|
|
|
|
moveWithinTree = (targetNode && targetNode.$treeScope && targetNode.$treeScope.$id && targetNode.$treeScope.$id === treeOfOrigin);
|
|
|
|
/* (jcarter) Notes to developers:
|
|
* pos.dirAx is either 0 or 1
|
|
* 1 means horizontal movement is happening
|
|
* 0 means vertical movement is happening
|
|
*/
|
|
|
|
// Move nodes up and down in nesting level.
|
|
if (moveWithinTree && pos.dirAx) {
|
|
|
|
// increase horizontal level if previous sibling exists and is not collapsed
|
|
// example 1.1.1 becomes 1.2
|
|
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
|
|
// example 1.2 become 1.1.1
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
} else { //Either in origin tree and moving horizontally OR you are moving within a new tree.
|
|
|
|
//Check it's new position.
|
|
isEmpty = false;
|
|
|
|
//Exit if target is not a uiTree or child of one.
|
|
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', '');
|
|
}
|
|
|
|
//Set whether target tree is empty or not.
|
|
if (targetNode.$type === 'uiTree' && targetNode.dragEnabled) {
|
|
isEmpty = targetNode.isEmpty();
|
|
}
|
|
|
|
//If target is a handle set new target to handle's node.
|
|
if (targetNode.$type === 'uiTreeHandle') {
|
|
targetNode = targetNode.$nodeScope;
|
|
}
|
|
|
|
//Check if it is a uiTreeNode or it's an empty tree or it's a dropzone.
|
|
if (targetNode.$type !== 'uiTreeNode' && !isEmpty && !isDropzone) {
|
|
|
|
// Allow node to return to its original position if no longer hovering over target
|
|
if (config.appendChildOnHover) {
|
|
next = dragInfo.next();
|
|
if (!next && unhover) {
|
|
target = dragInfo.parentNode();
|
|
target.$element.after(placeElm);
|
|
dragInfo.moveTo(target.$parentNodesScope, target.siblings(), target.index() + 1);
|
|
unhover = false;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
//If placeholder move from empty tree, reset it.
|
|
if (treeScope && placeElm.parent()[0] != treeScope.$element[0]) {
|
|
treeScope.resetEmptyElement();
|
|
treeScope.resetDropzoneElement();
|
|
treeScope = null;
|
|
}
|
|
|
|
//It's an empty tree
|
|
if (isEmpty) {
|
|
treeScope = targetNode;
|
|
if (targetNode.$nodesScope.accept(scope, 0)) {
|
|
dragInfo.moveTo(targetNode.$nodesScope, targetNode.$nodesScope.childNodes(), 0);
|
|
}
|
|
//It's a dropzone
|
|
} else if (isDropzone) {
|
|
treeScope = targetNode;
|
|
if (targetNode.$nodesScope.accept(scope, targetNode.$nodesScope.childNodes().length)) {
|
|
dragInfo.moveTo(targetNode.$nodesScope, targetNode.$nodesScope.childNodes(), targetNode.$nodesScope.childNodes().length);
|
|
}
|
|
//Not empty and drag enabled.
|
|
} else if (targetNode.dragEnabled()) {
|
|
|
|
//Setting/Resetting data for exanding on hover.
|
|
if (angular.isDefined(scope.expandTimeoutOn) && scope.expandTimeoutOn !== targetNode.id) {
|
|
$timeout.cancel(scope.expandTimeout);
|
|
delete scope.expandTimeout;
|
|
delete scope.expandTimeoutOn;
|
|
|
|
scope.$callbacks.expandTimeoutCancel();
|
|
}
|
|
|
|
//Determining if expansion is needed.
|
|
if (targetNode.collapsed) {
|
|
if (scope.expandOnHover === true || (angular.isNumber(scope.expandOnHover) && scope.expandOnHover === 0)) {
|
|
targetNode.collapsed = false;
|
|
targetNode.$treeScope.$callbacks.toggle(false, targetNode);
|
|
} else if (scope.expandOnHover !== false && angular.isNumber(scope.expandOnHover) && scope.expandOnHover > 0) {
|
|
|
|
//Triggering expansion.
|
|
if (angular.isUndefined(scope.expandTimeoutOn)) {
|
|
scope.expandTimeoutOn = targetNode.$id;
|
|
|
|
scope.$callbacks.expandTimeoutStart();
|
|
scope.expandTimeout = $timeout(function()
|
|
{
|
|
scope.$callbacks.expandTimeoutEnd();
|
|
targetNode.collapsed = false;
|
|
targetNode.$treeScope.$callbacks.toggle(false, targetNode);
|
|
}, scope.expandOnHover);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Get the element of ui-tree-node
|
|
targetElm = targetNode.$element;
|
|
targetOffset = UiTreeHelper.offset(targetElm);
|
|
targetHeight = UiTreeHelper.height(targetElm);
|
|
targetChildElm = targetNode.$childNodesScope ? targetNode.$childNodesScope.$element : null;
|
|
targetChildHeight = targetChildElm ? UiTreeHelper.height(targetChildElm) : 0;
|
|
targetHeight -= targetChildHeight;
|
|
targetBeforeBuffer = config.appendChildOnHover ? targetHeight * 0.25 : UiTreeHelper.height(targetElm) / 2;
|
|
targetBefore = eventObj.pageY < (targetOffset.top + targetBeforeBuffer);
|
|
|
|
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 {
|
|
// Try to append as a child if dragged upwards onto targetNode
|
|
if (config.appendChildOnHover && targetNode.accept(scope, targetNode.childNodesCount())) {
|
|
targetNode.$childNodesScope.$element.prepend(placeElm);
|
|
dragInfo.moveTo(targetNode.$childNodesScope, targetNode.childNodes(), 0);
|
|
unhover = true;
|
|
} else {
|
|
targetElm.after(placeElm);
|
|
dragInfo.moveTo(targetNode.$parentNodesScope, targetNode.siblings(), targetNode.index() + 1);
|
|
}
|
|
}
|
|
|
|
//We have to check if it can add the dragging node as a child.
|
|
} else if (!targetBefore && targetNode.accept(scope, targetNode.childNodesCount())) {
|
|
targetNode.$childNodesScope.$element.append(placeElm);
|
|
dragInfo.moveTo(targetNode.$childNodesScope, targetNode.childNodes(), targetNode.childNodesCount());
|
|
}
|
|
}
|
|
}
|
|
|
|
//Triggering dragMove callback.
|
|
scope.$apply(function () {
|
|
scope.$treeScope.$callbacks.dragMove(dragInfo.eventArgs(elements, pos));
|
|
});
|
|
}
|
|
};
|
|
|
|
dragEnd = function (e) {
|
|
|
|
var dragEventArgs = dragInfo.eventArgs(elements, pos);
|
|
|
|
e.preventDefault();
|
|
|
|
//TODO(jcarter): Is dragStart need to be unbound?
|
|
unbindDragMoveEvents();
|
|
|
|
//This cancel the collapse/expand login running.
|
|
$timeout.cancel(scope.expandTimeout);
|
|
|
|
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) {
|
|
//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 () {
|
|
|
|
//Replace placeholder with newly dropped element.
|
|
hiddenPlaceElm.replaceWith(scope.$element);
|
|
placeElm.remove();
|
|
|
|
//Remove drag element if still in DOM.
|
|
if (dragElm) {
|
|
dragElm.remove();
|
|
dragElm = null;
|
|
}
|
|
|
|
//Fire dragStope callback.
|
|
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);
|
|
}
|
|
};
|
|
})();
|
|
|
|
keydownHandler = function (e) {
|
|
if (e.keyCode === 27) {
|
|
dragEndEvent(e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Binds the mouse/touch events to enable drag start for this node.
|
|
*/
|
|
//This is outside of bindDragMoveEvents because of the potential for a delay setting.
|
|
bindDragStartEvents = function () {
|
|
element.bind('touchstart mousedown', function (e) {
|
|
//Don't call drag delay if no delay was specified.
|
|
if (scope.dragDelay > 0) {
|
|
dragDelay.exec(function () {
|
|
dragStartEvent(e);
|
|
}, scope.dragDelay);
|
|
} else {
|
|
dragStartEvent(e);
|
|
}
|
|
});
|
|
element.bind('touchend touchcancel mouseup', function () {
|
|
if (scope.dragDelay > 0) {
|
|
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);
|
|
angular.element($document).bind('keydown', keydownHandler);
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
angular.element($document).unbind('keydown', keydownHandler);
|
|
};
|
|
}
|
|
};
|
|
}
|
|
]);
|
|
})();
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('ui.tree')
|
|
.directive('uiTreeNodes', ['treeConfig', '$window',
|
|
function (treeConfig) {
|
|
return {
|
|
require: ['ngModel', '?^uiTreeNode', '^uiTree'],
|
|
restrict: 'A',
|
|
scope: true,
|
|
controller: 'TreeNodesController',
|
|
link: function (scope, element, attrs, controllersArr) {
|
|
|
|
var config = {},
|
|
ngModel = controllersArr[0],
|
|
treeNodeCtrl = controllersArr[1],
|
|
treeCtrl = controllersArr[2];
|
|
|
|
angular.extend(config, treeConfig);
|
|
if (config.nodesClass) {
|
|
element.addClass(config.nodesClass);
|
|
}
|
|
|
|
if (treeNodeCtrl) {
|
|
treeNodeCtrl.scope.$childNodesScope = scope;
|
|
scope.$nodeScope = treeNodeCtrl.scope;
|
|
} else {
|
|
// find the root nodes if there is no parent node and have a parent ui-tree
|
|
treeCtrl.scope.$nodesScope = scope;
|
|
}
|
|
scope.$treeScope = treeCtrl.scope;
|
|
|
|
if (ngModel) {
|
|
ngModel.$render = function () {
|
|
scope.$modelValue = ngModel.$modelValue;
|
|
};
|
|
}
|
|
|
|
scope.$watch(function () {
|
|
return attrs.maxDepth;
|
|
}, function (val) {
|
|
if ((typeof val) == 'number') {
|
|
scope.maxDepth = val;
|
|
}
|
|
});
|
|
|
|
scope.$watch(function () {
|
|
return attrs.nodropEnabled;
|
|
}, function (newVal) {
|
|
if ((typeof newVal) != 'undefined') {
|
|
scope.nodropEnabled = true;
|
|
}
|
|
}, true);
|
|
|
|
}
|
|
};
|
|
}
|
|
]);
|
|
})();
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('ui.tree')
|
|
|
|
/**
|
|
* @ngdoc service
|
|
* @name ui.tree.service:UiTreeHelper
|
|
* @requires ng.$document
|
|
* @requires ng.$window
|
|
*
|
|
* @description
|
|
* angular-ui-tree.
|
|
*/
|
|
.factory('UiTreeHelper', ['$document', '$window', 'treeConfig',
|
|
function ($document, $window, treeConfig) {
|
|
return {
|
|
|
|
/**
|
|
* A hashtable used to storage data of nodes
|
|
* @type {Object}
|
|
*/
|
|
nodesData: {},
|
|
|
|
setNodeAttribute: function (scope, attrName, val) {
|
|
if (!scope.$modelValue) {
|
|
return null;
|
|
}
|
|
var data = this.nodesData[scope.$modelValue.$$hashKey];
|
|
if (!data) {
|
|
data = {};
|
|
this.nodesData[scope.$modelValue.$$hashKey] = data;
|
|
}
|
|
data[attrName] = val;
|
|
},
|
|
|
|
getNodeAttribute: function (scope, attrName) {
|
|
if (!scope.$modelValue) {
|
|
return null;
|
|
}
|
|
var data = this.nodesData[scope.$modelValue.$$hashKey];
|
|
if (data) {
|
|
return data[attrName];
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @methodOf ui.tree.service:$nodrag
|
|
* @param {Object} targetElm angular element
|
|
* @return {Bool} check if the node can be dragged.
|
|
*/
|
|
nodrag: function (targetElm) {
|
|
if (typeof targetElm.attr('data-nodrag') != 'undefined') {
|
|
return targetElm.attr('data-nodrag') !== 'false';
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Get the event object for touches.
|
|
*
|
|
* @param {MouseEvent|TouchEvent} e MouseEvent or TouchEvent that kicked off dragX method.
|
|
* @return {MouseEvent|TouchEvent} Object returned as original event object.
|
|
*/
|
|
eventObj: function (e) {
|
|
var obj = e;
|
|
if (e.targetTouches !== undefined) {
|
|
//Set obj equal to the first Touch object in the TouchList.
|
|
obj = e.targetTouches.item(0);
|
|
//Logic to set obj to original TouchEvent.
|
|
} else if (e.originalEvent !== undefined && e.originalEvent.targetTouches !== undefined) {
|
|
obj = e.originalEvent.targetTouches.item(0);
|
|
}
|
|
return obj;
|
|
},
|
|
|
|
/**
|
|
* Generate object used to store data about node being moved.
|
|
*
|
|
* {angular.$scope} node Scope of the node that is being moved.
|
|
*/
|
|
dragInfo: function (node) {
|
|
return {
|
|
source: node,
|
|
sourceInfo: {
|
|
cloneModel: node.$treeScope.cloneEnabled === true ? angular.copy(node.$modelValue) : undefined,
|
|
nodeScope: node,
|
|
index: node.index(),
|
|
nodesScope: node.$parentNodesScope
|
|
},
|
|
index: node.index(),
|
|
|
|
//Slice(0) just duplicates an array.
|
|
siblings: node.siblings().slice(0),
|
|
parent: node.$parentNodesScope,
|
|
|
|
//Reset parent to source parent.
|
|
resetParent: function() {
|
|
this.parent = node.$parentNodesScope;
|
|
},
|
|
|
|
//Move the node to a new position, determining where the node will be inserted to when dropped happens here.
|
|
moveTo: function (parent, siblings, index) {
|
|
this.parent = parent;
|
|
|
|
//Duplicate siblings array.
|
|
this.siblings = siblings.slice(0);
|
|
|
|
//If source node is in the target nodes
|
|
var i = this.siblings.indexOf(this.source);
|
|
if (i > -1) {
|
|
this.siblings.splice(i, 1);
|
|
if (this.source.index() < index) {
|
|
index--;
|
|
}
|
|
}
|
|
|
|
this.siblings.splice(index, 0, this.source);
|
|
this.index = index;
|
|
},
|
|
|
|
//Get parent nodes nodeScope.
|
|
parentNode: function () {
|
|
return this.parent.$nodeScope;
|
|
},
|
|
|
|
//Get previous sibling node.
|
|
prev: function () {
|
|
if (this.index > 0) {
|
|
return this.siblings[this.index - 1];
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
//Get next sibling node.
|
|
next: function () {
|
|
if (this.index < this.siblings.length - 1) {
|
|
return this.siblings[this.index + 1];
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
//Return what cloneEnabled is set to on uiTree.
|
|
isClone: function () {
|
|
return this.source.$treeScope.cloneEnabled === true;
|
|
},
|
|
|
|
//Returns a copy of node passed in.
|
|
clonedNode: function (node) {
|
|
return angular.copy(node);
|
|
},
|
|
|
|
//Returns true if parent or index have changed (move happened within any uiTree).
|
|
isDirty: function () {
|
|
return this.source.$parentNodesScope != this.parent ||
|
|
this.source.index() != this.index;
|
|
},
|
|
|
|
//Return whether node has a new parent (set on moveTo method).
|
|
isForeign: function () {
|
|
return this.source.$treeScope !== this.parent.$treeScope;
|
|
},
|
|
|
|
//Sets arguments passed to user callbacks.
|
|
eventArgs: function (elements, pos) {
|
|
return {
|
|
source: this.sourceInfo,
|
|
dest: {
|
|
index: this.index,
|
|
nodesScope: this.parent
|
|
},
|
|
elements: elements,
|
|
pos: pos
|
|
};
|
|
},
|
|
|
|
//Method that actually manipulates the node being moved.
|
|
apply: function () {
|
|
|
|
var nodeData = this.source.$modelValue;
|
|
|
|
//Nodrop enabled on tree or parent
|
|
if (this.parent.nodropEnabled || this.parent.$treeScope.nodropEnabled) {
|
|
return;
|
|
}
|
|
|
|
//Node was dropped in the same place - do nothing.
|
|
if (!this.isDirty()) {
|
|
return;
|
|
}
|
|
|
|
//CloneEnabled and cross-tree so copy and do not remove from source.
|
|
if (this.isClone() && this.isForeign()) {
|
|
this.parent.insertNode(this.index, this.sourceInfo.cloneModel);
|
|
//Any other case, remove and reinsert.
|
|
} else {
|
|
this.source.remove();
|
|
this.parent.insertNode(this.index, nodeData);
|
|
}
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ui.tree#height
|
|
* @methodOf ui.tree.service:UiTreeHelper
|
|
*
|
|
* @description
|
|
* Get the height of an element.
|
|
*
|
|
* @param {Object} element Angular element.
|
|
* @returns {String} Height
|
|
*/
|
|
height: function (element) {
|
|
return element.prop('scrollHeight');
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ui.tree#width
|
|
* @methodOf ui.tree.service:UiTreeHelper
|
|
*
|
|
* @description
|
|
* Get the width of an element.
|
|
*
|
|
* @param {Object} element Angular element.
|
|
* @returns {String} Width
|
|
*/
|
|
width: function (element) {
|
|
return element.prop('scrollWidth');
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ui.tree#offset
|
|
* @methodOf ui.nestedSortable.service:UiTreeHelper
|
|
*
|
|
* @description
|
|
* Get the offset values of an element.
|
|
*
|
|
* @param {Object} element Angular element.
|
|
* @returns {Object} Object with properties width, height, top and left
|
|
*/
|
|
offset: function (element) {
|
|
var boundingClientRect = element[0].getBoundingClientRect();
|
|
|
|
return {
|
|
width: element.prop('offsetWidth'),
|
|
height: element.prop('offsetHeight'),
|
|
top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop),
|
|
left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft)
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ui.tree#positionStarted
|
|
* @methodOf ui.tree.service:UiTreeHelper
|
|
*
|
|
* @description
|
|
* Get the start position of the target element according to the provided event properties.
|
|
*
|
|
* @param {Object} e Event
|
|
* @param {Object} target Target element
|
|
* @returns {Object} Object with properties offsetX, offsetY, startX, startY, nowX and dirX.
|
|
*/
|
|
positionStarted: function (e, target) {
|
|
var pos = {},
|
|
pageX = e.pageX,
|
|
pageY = e.pageY;
|
|
|
|
//Check to set correct data for TouchEvents
|
|
if (e.originalEvent && e.originalEvent.touches && (e.originalEvent.touches.length > 0)) {
|
|
pageX = e.originalEvent.touches[0].pageX;
|
|
pageY = e.originalEvent.touches[0].pageY;
|
|
}
|
|
pos.offsetX = pageX - this.offset(target).left;
|
|
pos.offsetY = pageY - this.offset(target).top;
|
|
pos.startX = pos.lastX = pageX;
|
|
pos.startY = pos.lastY = pageY;
|
|
pos.nowX = pos.nowY = pos.distX = pos.distY = pos.dirAx = 0;
|
|
pos.dirX = pos.dirY = pos.lastDirX = pos.lastDirY = pos.distAxX = pos.distAxY = 0;
|
|
return pos;
|
|
},
|
|
|
|
positionMoved: function (e, pos, firstMoving) {
|
|
|
|
var pageX = e.pageX,
|
|
pageY = e.pageY,
|
|
newAx;
|
|
|
|
//If there are multiple touch points, choose one to use as X and Y.
|
|
if (e.originalEvent && e.originalEvent.touches && (e.originalEvent.touches.length > 0)) {
|
|
pageX = e.originalEvent.touches[0].pageX;
|
|
pageY = e.originalEvent.touches[0].pageY;
|
|
}
|
|
|
|
//Mouse position last event.
|
|
pos.lastX = pos.nowX;
|
|
pos.lastY = pos.nowY;
|
|
|
|
//Mouse position this event.
|
|
pos.nowX = pageX;
|
|
pos.nowY = pageY;
|
|
|
|
//Distance mouse moved between events.
|
|
pos.distX = pos.nowX - pos.lastX;
|
|
pos.distY = pos.nowY - pos.lastY;
|
|
|
|
//Direction mouse was moving.
|
|
pos.lastDirX = pos.dirX;
|
|
pos.lastDirY = pos.dirY;
|
|
|
|
//Direction mouse is now moving (on both axis).
|
|
pos.dirX = pos.distX === 0 ? 0 : pos.distX > 0 ? 1 : -1;
|
|
pos.dirY = pos.distY === 0 ? 0 : pos.distY > 0 ? 1 : -1;
|
|
|
|
//Axis mouse is now moving on.
|
|
newAx = Math.abs(pos.distX) > Math.abs(pos.distY) ? 1 : 0;
|
|
|
|
//Do nothing on first move.
|
|
if (firstMoving) {
|
|
pos.dirAx = newAx;
|
|
pos.moving = true;
|
|
return;
|
|
}
|
|
|
|
//Calc distance moved on this axis (and direction).
|
|
if (pos.dirAx !== newAx) {
|
|
pos.distAxX = 0;
|
|
pos.distAxY = 0;
|
|
} else {
|
|
pos.distAxX += Math.abs(pos.distX);
|
|
if (pos.dirX !== 0 && pos.dirX !== pos.lastDirX) {
|
|
pos.distAxX = 0;
|
|
}
|
|
pos.distAxY += Math.abs(pos.distY);
|
|
if (pos.dirY !== 0 && pos.dirY !== pos.lastDirY) {
|
|
pos.distAxY = 0;
|
|
}
|
|
}
|
|
pos.dirAx = newAx;
|
|
},
|
|
|
|
elementIsTreeNode: function (element) {
|
|
return typeof element.attr('ui-tree-node') !== 'undefined';
|
|
},
|
|
|
|
elementIsTreeNodeHandle: function (element) {
|
|
return typeof element.attr('ui-tree-handle') !== 'undefined';
|
|
},
|
|
elementIsTree: function (element) {
|
|
return typeof element.attr('ui-tree') !== 'undefined';
|
|
},
|
|
elementIsTreeNodes: function (element) {
|
|
return typeof element.attr('ui-tree-nodes') !== 'undefined';
|
|
},
|
|
elementIsPlaceholder: function (element) {
|
|
return element.hasClass(treeConfig.placeholderClass);
|
|
},
|
|
elementIsDropzone: function (element) {
|
|
return element.hasClass(treeConfig.dropzoneClass);
|
|
},
|
|
elementContainsTreeNodeHandler: function (element) {
|
|
return element[0].querySelectorAll('[ui-tree-handle]').length >= 1;
|
|
},
|
|
treeNodeHandlerContainerOfElement: function (element) {
|
|
return findFirstParentElementWithAttribute('ui-tree-handle', element[0]);
|
|
}
|
|
};
|
|
}
|
|
]);
|
|
|
|
// TODO: optimize this loop
|
|
//(Jcarter): Suggest adding a parent element property on uiTree, then all these bubble
|
|
// to <html> can trigger to stop when they reach the parent.
|
|
function findFirstParentElementWithAttribute(attributeName, childObj) {
|
|
//Undefined if the mouse leaves the browser window
|
|
if (childObj === undefined) {
|
|
return null;
|
|
}
|
|
var testObj = childObj.parentNode,
|
|
count = 1,
|
|
//Check for setAttribute due to exception thrown by Firefox when a node is dragged outside the browser window
|
|
res = (typeof testObj.setAttribute === 'function' && testObj.hasAttribute(attributeName)) ? testObj : null;
|
|
while (testObj && typeof testObj.setAttribute === 'function' && !testObj.hasAttribute(attributeName)) {
|
|
testObj = testObj.parentNode;
|
|
res = testObj;
|
|
//Stop once we reach top of page.
|
|
if (testObj === document.documentElement) {
|
|
res = null;
|
|
break;
|
|
}
|
|
count++;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
})(); |