/**
* The TreeView Utility
*
* @module aui-tree
* @submodule aui-tree-view
*/
var L = A.Lang,
isBoolean = L.isBoolean,
isString = L.isString,
UA = A.UA,
getCN = A.getClassName,
CSS_TREE_HITAREA = getCN('tree', 'hitarea'),
CSS_TREE_ICON = getCN('tree', 'icon'),
CSS_TREE_LABEL = getCN('tree', 'label'),
CSS_TREE_NODE = getCN('tree', 'node'),
CSS_TREE_NODE_CONTENT = getCN('tree', 'node', 'content'),
CSS_TREE_NODE_CONTENT_INVALID = getCN('tree', 'node', 'content', 'invalid'),
CSS_TREE_ROOT_CONTAINER = getCN('tree', 'root', 'container'),
CSS_TREE_VIEW_CONTENT = getCN('tree', 'view', 'content'),
addClassTreeNode = function(rootNode) {
rootNode.addClass(CSS_TREE_NODE);
rootNode.all('> ul > li').each(function(node) {
addClassTreeNode(node);
});
},
concat = function() {
return Array.prototype.slice.call(arguments).join(' ');
},
isTreeNode = function(v) {
return (v instanceof A.TreeNode);
};
/**
* A base class for TreeView, providing:
*
* - Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)
*
* Check the [live demo](http://alloyui.com/examples/tree/).
*
* @class A.TreeView
* @extends A.Component
* @uses A.TreeData, A.TreeViewPaginator, A.TreeViewIO
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
* @example
```
<div id="myTreeView"></div>
```
* @example
```
YUI().use(
'aui-tree-view',
function(Y) {
// Create an array object for the tree root and child nodes
var children = [
{
children: [
{
label: 'File X'
},
{
label: 'File Y'
},
{
label: 'File Z'
}
],
expanded: true,
label: 'Root'
}
];
// Create a TreeView Component
new Y.TreeView(
{
boundingBox: '#myTreeView',
children: children
}
).render();
}
);
```
*/
var TreeView = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property NAME
* @type String
* @static
*/
NAME: 'tree-view',
/**
* Static property used to define the default attribute
* configuration for the TreeView.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Type of the treeview (i.e. could be 'file' or 'normal').
*
* @attribute type
* @default 'file'
* @type String
*/
type: {
value: 'file',
validator: isString
},
/**
* Last selected TreeNode.
*
* @attribute lastSelected
* @default null
* @type TreeNode
*/
lastSelected: {
value: null,
validator: isTreeNode
},
/**
* Determine if its going to be lazy loaded or not.
*
* @attribute lazyLoad
* @default true
* @type Boolean
*/
lazyLoad: {
validator: isBoolean,
value: true
},
/**
* Determine if its going to be selected on toggle.
*
* @attribute selectOnToggle
* @default false
* @type Boolean
*/
selectOnToggle: {
validator: isBoolean,
value: false
}
},
AUGMENTS: [A.TreeData, A.TreeViewPaginator, A.TreeViewIO],
/**
* @property HTML_PARSER
* @type {Object}
* @protected
* @static
*/
HTML_PARSER: {
contentBox: function(contentBox) {
var root = contentBox.all('> li');
addClassTreeNode(root);
return contentBox;
}
},
prototype: {
CONTENT_TEMPLATE: '<ul></ul>',
/**
* Construction logic executed during TreeView instantiation. Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
var instance = this;
var boundingBox = instance.get('boundingBox');
boundingBox.setData('tree-view', instance);
},
/**
* Bind the events on the TreeView UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function() {
var instance = this;
instance.after('childrenChange', A.bind(instance._afterSetChildren, instance));
instance._delegateDOM();
},
/**
* Create the DOM structure for the TreeView. Lifecycle.
*
* @method renderUI
* @protected
*/
renderUI: function() {
var instance = this;
instance._renderElements();
},
/**
* Fires after a key event is dispatched.
*
* @method _afterKeyNodeEl
* @param {EventFacade} event
* @protected
*/
_afterKeyNodeEl: function(event) {
event.preventDefault();
this._toggleTreeContent(event);
},
/**
* Fire after set children.
*
* @method _afterSetChildren
* @param {EventFacade} event
* @protected
*/
_afterSetChildren: function(event) {
var instance = this;
var paginator = instance.get('paginator');
if (paginator && paginator.total) {
var increment = -1;
if (event.newVal.length > event.prevVal.length) {
increment = 1;
}
paginator.total += increment;
}
instance._syncPaginatorUI();
},
/**
* Create TreeNode from HTML markup.
*
* @method _createFromHTMLMarkup
* @param {Node} container
* @protected
*/
_createFromHTMLMarkup: function(container) {
var instance = this;
container.all('> li').each(function(node) {
// use firstChild as label
var labelEl = node.one('> *').remove();
var label = labelEl.outerHTML();
var deepContainer = node.one('> ul');
var treeNode = new A.TreeNode({
boundingBox: node,
container: deepContainer,
label: label,
leaf: !deepContainer,
ownerTree: instance
});
if (instance.get('lazyLoad')) {
A.setTimeout(function() {
treeNode.render();
}, 50);
}
else {
treeNode.render();
}
// find the parent TreeNode...
var parentNode = node.get('parentNode').get('parentNode');
var parentInstance = parentNode.getData('tree-node');
if (!A.instanceOf(parentInstance, A.TreeNode)) {
parentInstance = parentNode.getData('tree-view');
}
// and simulate the appendChild.
parentInstance.appendChild(treeNode);
if (deepContainer) {
// propagating markup recursion
instance._createFromHTMLMarkup(deepContainer);
}
});
},
/**
* Create Node container.
*
* @method _createNodeContainer
* @protected
*/
_createNodeContainer: function() {
var instance = this;
var contentBox = instance.get('contentBox');
instance.set('container', contentBox);
return contentBox;
},
/**
* Render elements.
*
* @method _renderElements
* @protected
*/
_renderElements: function() {
var instance = this;
var contentBox = instance.get('contentBox');
var children = instance.get('children');
var type = instance.get('type');
var CSS_TREE_TYPE = getCN('tree', type);
contentBox.addClass(CSS_TREE_VIEW_CONTENT);
contentBox.addClass(
concat(CSS_TREE_TYPE, CSS_TREE_ROOT_CONTAINER)
);
if (!children.length) {
// if children not specified try to create from markup
instance._createFromHTMLMarkup(contentBox);
}
},
/**
* Delegate events.
*
* @method _delegateDOM
* @protected
*/
_delegateDOM: function() {
var instance = this;
var boundingBox = instance.get('boundingBox');
// expand/collapse delegations
boundingBox.delegate('click', A.bind(instance._onClickNodeEl, instance), '.' +
CSS_TREE_NODE_CONTENT);
boundingBox.delegate('key', A.bind(instance._afterKeyNodeEl, instance), 'down:enter,space',
'.' + CSS_TREE_NODE_CONTENT);
boundingBox.delegate('dblclick', A.bind(instance._onClickHitArea, instance), '.' + CSS_TREE_ICON);
boundingBox.delegate('dblclick', A.bind(instance._onClickHitArea, instance), '.' + CSS_TREE_LABEL);
// other delegations
boundingBox.delegate(
'mouseenter', A.bind(instance._onMouseEnterNodeEl, instance), '.' + CSS_TREE_NODE_CONTENT);
boundingBox.delegate(
'mouseleave', A.bind(instance._onMouseLeaveNodeEl, instance), '.' + CSS_TREE_NODE_CONTENT);
},
/**
* Fire on click the TreeView.
*
* @method _onClickNodeEl
* @param {EventFacade} event
* @protected
*/
_onClickNodeEl: function(event) {
this._toggleTreeContent(event);
},
/**
* Fire on `mouseEnter` the TreeNode.
*
* @method _onMouseEnterNodeEl
* @param {EventFacade} event
* @protected
*/
_onMouseEnterNodeEl: function(event) {
var instance = this;
var treeNode = instance.getNodeByChild(event.currentTarget);
if (treeNode) {
treeNode.over();
}
},
/**
* Fire on `mouseLeave` the TreeNode.
*
* @method _onMouseLeaveNodeEl
* @param {EventFacade} event
* @protected
*/
_onMouseLeaveNodeEl: function(event) {
var instance = this;
var treeNode = instance.getNodeByChild(event.currentTarget);
if (treeNode) {
treeNode.out();
}
},
/**
* Fire on `click` the TreeNode hitarea.
*
* @method _onClickHitArea
* @param {EventFacade} event
* @protected
*/
_onClickHitArea: function(event) {
var instance = this;
var treeNode = instance.getNodeByChild(event.currentTarget);
if (treeNode) {
treeNode.toggle();
}
},
/**
* Toggles the display of the 'A.TreeView' content (i.e. set the select/unselect state).
*
* @method _toggleTreeContent
* @param {EventFacade} event
* @protected
*/
_toggleTreeContent: function(event) {
var treeNode = this.getNodeByChild(event.currentTarget);
if (treeNode) {
if (event.target.test('.' + CSS_TREE_HITAREA)) {
treeNode.toggle();
if (!this.get('selectOnToggle')) {
return;
}
}
if (!treeNode.isSelected()) {
var lastSelected = this.get('lastSelected');
// select drag node
if (lastSelected) {
lastSelected.unselect();
}
treeNode.select();
}
}
}
}
});
A.TreeView = TreeView;
/*
* TreeViewDD - Drag & Drop
*/
var isNumber = L.isNumber,
DDM = A.DD.DDM,
CSS_CLEARFIX = getCN('clearfix'),
CSS_ICON = getCN('glyphicon'),
CSS_TREE_DRAG_HELPER = getCN('tree', 'drag', 'helper'),
CSS_TREE_DRAG_HELPER_CONTENT = getCN('tree', 'drag', 'helper', 'content'),
CSS_TREE_DRAG_HELPER_LABEL = getCN('tree', 'drag', 'helper', 'label'),
CSS_TREE_DRAG_INSERT_ABOVE = getCN('tree', 'drag', 'insert', 'above'),
CSS_TREE_DRAG_INSERT_APPEND = getCN('tree', 'drag', 'insert', 'append'),
CSS_TREE_DRAG_INSERT_BELOW = getCN('tree', 'drag', 'insert', 'below'),
CSS_TREE_DRAG_STATE_APPEND = getCN('tree', 'drag', 'state', 'append'),
CSS_TREE_DRAG_STATE_INSERT_ABOVE = getCN('tree', 'drag', 'state', 'insert', 'above'),
CSS_TREE_DRAG_STATE_INSERT_BELOW = getCN('tree', 'drag', 'state', 'insert', 'below'),
HELPER_TPL = '<div class="' + CSS_TREE_DRAG_HELPER + '">' +
'<div class="' + [CSS_TREE_DRAG_HELPER_CONTENT, CSS_CLEARFIX].join(' ') + '">' +
'<span class="' + CSS_ICON + '"></span>' +
'<span class="' + CSS_TREE_DRAG_HELPER_LABEL + '"></span>' +
'</div>' +
'</div>';
/**
* A base class for TreeViewDD, providing:
*
* - Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)
* - DragDrop support for the TreeNodes
*
* @class A.TreeViewDD
* @extends A.TreeView
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
*/
var TreeViewDD = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property NAME
* @type String
* @static
*/
NAME: 'tree-drag-drop',
/**
* Static property used to define the default attribute
* configuration for the TreeViewDD.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Dragdrop helper element.
*
* @attribute helper
* @default null
* @type Node | String
*/
helper: {
value: null
},
/**
* Delay of the scroll while dragging the TreeNodes.
*
* @attribute scrollDelay
* @default 100
* @type Number
*/
scrollDelay: {
value: 100,
validator: isNumber
}
},
EXTENDS: A.TreeView,
prototype: {
/**
* Direction of the drag (i.e. could be 'up' or 'down').
*
* @property direction
* @type String
* @protected
*/
direction: 'below',
/**
* Drop action (i.e. could be 'append', 'below' or 'above').
*
* @attribute dropAction
* @default null
* @type String
*/
dropAction: null,
/**
* Last Y.
*
* @attribute lastY
* @default 0
* @type Number
*/
lastY: 0,
/**
* Node.
*
* @attribute node
* @default null
*/
node: null,
/**
* Reference for the current drop node.
*
* @attribute nodeContent
* @default null
* @type Node
*/
nodeContent: null,
/**
* Destructor lifecycle implementation for the `TreeViewDD` class.
* Purges events attached to the node (and all child nodes).
*
* @method destructor
* @protected
*/
destructor: function() {
var instance = this;
var helper = instance.get('helper');
if (helper) {
helper.remove(true);
}
if (instance.ddDelegate) {
instance.ddDelegate.destroy();
}
},
/**
* Bind the events on the TreeViewDD UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function() {
var instance = this;
A.TreeViewDD.superclass.bindUI.apply(this, arguments);
instance._bindDragDrop();
},
/**
* Create the DOM structure for the TreeViewDD. Lifecycle.
*
* @method renderUI
* @protected
*/
renderUI: function() {
var instance = this;
A.TreeViewDD.superclass.renderUI.apply(this, arguments);
// creating drag helper and hiding it
var helper = A.Node.create(HELPER_TPL).hide();
// append helper to the body
A.one('body').append(helper);
instance.set('helper', helper);
// set DRAG_CURSOR to the default arrow
DDM.set('dragCursor', 'default');
},
/**
* Bind DragDrop events.
*
* @method _bindDragDrop
* @protected
*/
_bindDragDrop: function() {
var instance = this,
boundingBox = instance.get('boundingBox'),
dragInitHandle = null;
instance._createDragInitHandler = function() {
instance.ddDelegate = new A.DD.Delegate({
bubbleTargets: instance,
container: boundingBox,
invalid: '.' + CSS_TREE_NODE_CONTENT_INVALID,
nodes: '.' + CSS_TREE_NODE_CONTENT,
target: true
});
var dd = instance.ddDelegate.dd;
dd.plug(A.Plugin.DDProxy, {
moveOnEnd: false,
positionProxy: false,
borderStyle: null
}).plug(A.Plugin.DDNodeScroll, {
scrollDelay: instance.get('scrollDelay'),
node: boundingBox
});
dd.removeInvalid('a');
if (dragInitHandle) {
dragInitHandle.detach();
}
};
// Check for mobile devices and execute _createDragInitHandler
// before events
if (!UA.touch) {
// only create the drag on the init elements if the user
// mouseover the boundingBox for init performance reasons
dragInitHandle = boundingBox.on(['focus', 'mousedown', 'mousemove'], instance._createDragInitHandler);
}
else {
instance._createDragInitHandler();
}
// drag & drop listeners
instance.on('drag:align', instance._onDragAlign);
instance.on('drag:start', instance._onDragStart);
instance.on('drop:exit', instance._onDropExit);
instance.after('drop:hit', instance._afterDropHit);
instance.on('drop:hit', instance._onDropHit);
instance.on('drop:over', instance._onDropOver);
},
/**
* Set the append CSS state on the passed `nodeContent`.
*
* @method _appendState
* @param {Node} nodeContent
* @protected
*/
_appendState: function(nodeContent) {
var instance = this;
instance.dropAction = 'append';
instance.get('helper').addClass(CSS_TREE_DRAG_STATE_APPEND);
nodeContent.addClass(CSS_TREE_DRAG_INSERT_APPEND);
},
/**
* Set the going down CSS state on the passed `nodeContent`.
*
* @method _goingDownState
* @param {Node} nodeContent
* @protected
*/
_goingDownState: function(nodeContent) {
var instance = this;
instance.dropAction = 'below';
instance.get('helper').addClass(CSS_TREE_DRAG_STATE_INSERT_BELOW);
nodeContent.addClass(CSS_TREE_DRAG_INSERT_BELOW);
},
/**
* Set the going up CSS state on the passed `nodeContent`.
*
* @method _goingUpState
* @param {Node} nodeContent
* @protected
*/
_goingUpState: function(nodeContent) {
var instance = this;
instance.dropAction = 'above';
instance.get('helper').addClass(CSS_TREE_DRAG_STATE_INSERT_ABOVE);
nodeContent.addClass(CSS_TREE_DRAG_INSERT_ABOVE);
},
/**
* Set the reset CSS state on the passed `nodeContent`.
*
* @method _resetState
* @param {Node} nodeContent
* @protected
*/
_resetState: function(nodeContent) {
var instance = this;
var helper = instance.get('helper');
helper.removeClass(CSS_TREE_DRAG_STATE_APPEND);
helper.removeClass(CSS_TREE_DRAG_STATE_INSERT_ABOVE);
helper.removeClass(CSS_TREE_DRAG_STATE_INSERT_BELOW);
if (nodeContent) {
nodeContent.removeClass(CSS_TREE_DRAG_INSERT_ABOVE);
nodeContent.removeClass(CSS_TREE_DRAG_INSERT_APPEND);
nodeContent.removeClass(CSS_TREE_DRAG_INSERT_BELOW);
}
},
/**
* Update the CSS node state (i.e. going down, going up, append etc).
*
* @method _updateNodeState
* @param {EventFacade} event
* @protected
*/
_updateNodeState: function(event) {
var instance = this;
var drag = event.drag;
var drop = event.drop;
var nodeContent = drop.get('node');
var dropNode = nodeContent.get('parentNode');
var dragNode = drag.get('node').get('parentNode');
var dropTreeNode = dropNode.getData('tree-node');
// reset the classNames from the last nodeContent
instance._resetState(instance.nodeContent);
// cannot drop the dragged element into any of its children
// nor above an undraggable element
// using DOM contains method for performance reason
if (!!dropTreeNode.get('draggable') && !dragNode.contains(dropNode)) {
// nArea splits the height in 3 areas top/center/bottom these
// areas are responsible for defining the state when the mouse
// is over any of them
var nArea = nodeContent.get('offsetHeight') / 3;
var yTop = nodeContent.getY();
var yCenter = yTop + nArea;
var yBottom = yTop + nArea * 2;
var mouseY = drag.mouseXY[1];
// UP: mouse on the top area of the node
if ((mouseY > yTop) && (mouseY < yCenter)) {
instance._goingUpState(nodeContent);
}
// DOWN: mouse on the bottom area of the node
else if (mouseY > yBottom) {
instance._goingDownState(nodeContent);
}
// APPEND: mouse on the center area of the node
else if ((mouseY > yCenter) && (mouseY < yBottom)) {
// if it's a folder set the state to append
if (dropTreeNode && !dropTreeNode.isLeaf()) {
instance._appendState(nodeContent);
}
// if it's a leaf we need to set the ABOVE or BELOW state
// instead of append
else {
if (instance.direction === 'up') {
instance._goingUpState(nodeContent);
}
else {
instance._goingDownState(nodeContent);
}
}
}
}
instance.nodeContent = nodeContent;
},
/**
* Fire after the drop hit event.
*
* @method _afterDropHit
* @param {EventFacade} event Drop hit event facade
* @protected
*/
_afterDropHit: function(event) {
var instance = this;
var dropAction = instance.dropAction;
var dragNode = event.drag.get('node').get('parentNode');
var dropNode = event.drop.get('node').get('parentNode');
var dropTreeNode = dropNode.getData('tree-node');
var dragTreeNode = dragNode.getData('tree-node');
var output = instance.getEventOutputMap(instance);
output.tree.dropNode = dropTreeNode;
output.tree.dragNode = dragTreeNode;
if (dropAction === 'above') {
dropTreeNode.insertBefore(dragTreeNode);
instance.bubbleEvent('dropInsert', output);
}
else if (dropAction === 'below') {
dropTreeNode.insertAfter(dragTreeNode);
instance.bubbleEvent('dropInsert', output);
}
else if (dropAction === 'append') {
if (dropTreeNode && !dropTreeNode.isLeaf()) {
if (!dropTreeNode.get('expanded')) {
// expand node when drop a child on it
dropTreeNode.expand();
}
dropTreeNode.appendChild(dragTreeNode);
instance.bubbleEvent('dropAppend', output);
}
}
instance._resetState(instance.nodeContent);
// bubbling drop event
instance.bubbleEvent('drop', output);
instance.dropAction = null;
},
/**
* Fire on drag align event.
*
* @method _onDragAlign
* @param {EventFacade} event Append event facade
* @protected
*/
_onDragAlign: function(event) {
var instance = this;
var lastY = instance.lastY;
var y = event.target.lastXY[1];
// if the y change
if (y !== lastY) {
// set the drag direction
instance.direction = (y < lastY) ? 'up' : 'down';
}
instance.lastY = y;
},
/**
* Fire on drag start event.
*
* @method _onDragStart
* @param {EventFacade} event Append event facade
* @protected
*/
_onDragStart: function(event) {
var instance = this;
var drag = event.target;
var dragNode = drag.get('node').get('parentNode');
var dragTreeNode = dragNode.getData('tree-node');
var lastSelected = instance.get('lastSelected');
// select drag node
if (lastSelected) {
lastSelected.unselect();
}
dragTreeNode.select();
// initialize drag helper
var helper = instance.get('helper');
var helperLabel = helper.one('.' + CSS_TREE_DRAG_HELPER_LABEL);
// show helper, we need display block here, yui dd hide it with
// display none
helper.setStyle('display', 'block').show();
// set the CSS_TREE_DRAG_HELPER_LABEL html with the label of the
// dragged node
helperLabel.html(dragTreeNode.get('label'));
// update the DRAG_NODE with the new helper
drag.set('dragNode', helper);
},
/**
* Fire on drop over event.
*
* @method _onDropOver
* @param {EventFacade} event Append event facade
* @protected
*/
_onDropOver: function(event) {
var instance = this;
instance._updateNodeState(event);
},
/**
* Fire on drop hit event.
*
* @method _onDropHit
* @param {EventFacade} event Append event facade
* @protected
*/
_onDropHit: function(event) {
var dropNode = event.drop.get('node').get('parentNode');
var dropTreeNode = dropNode.getData('tree-node');
if (!isTreeNode(dropTreeNode)) {
event.preventDefault();
}
},
/**
* Fire on drop exit event.
*
* @method _onDropExit
* @param {EventFacade} event Append event facade
* @protected
*/
_onDropExit: function() {
var instance = this;
instance.dropAction = null;
instance._resetState(instance.nodeContent);
}
}
});
A.TreeViewDD = TreeViewDD;