/**
* The SortableList Utility
*
* @module aui-sortable-list
*/
var L = A.Lang,
isString = L.isString,
isFunction = L.isFunction,
DDM = A.DD.DDM;
/**
* A base class for SortableList, providing:
*
* - Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)
* - Sortable list utility
*
* Check the [live demo](http://alloyui.com/examples/sortable-list/).
*
* @class A.SortableList
* @extends Base
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
*/
var SortableList = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property NAME
* @type String
* @static
*/
NAME: 'sortable-list',
/**
* Static property used to define the default attribute
* configuration for the `A.SortableList`.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Drag & Drop plugin attached to the widget.
*
* @attribute dd
* @default null
*/
dd: {
value: null
},
/**
* Validates the condition for an element to be dropped.
*
* @attribute dropCondition
* @type Function
*/
dropCondition: {
value: function() {
return true;
},
setter: function(v) {
return A.bind(v, this);
},
validator: isFunction
},
/**
* The container which serves to host dropped elements.
*
* @attribute dropContainer
* @type Function
*/
dropContainer: {
value: function(event) {
var instance = this;
var drop = event.drop;
var dropNode = drop.get('node');
var dropOn = instance.get('dropOn');
dropNode.attr('dropzone', 'move');
return dropNode.one(dropOn);
},
validator: isFunction
},
/**
* The CSS class name used to define which nodes serve as container to
* be dropped.
*
* @attribute dropOn
* @type String
*/
dropOn: {
validator: isString
},
/**
* Indicates that the element is being dragged.
*
* @attribute helper
* @default null
*/
helper: {
value: null
},
/**
* The CSS class name used to define which nodes are draggable.
*
* @attribute nodes
*/
nodes: {
setter: function(v) {
return this._setNodes(v);
}
},
/**
* Simulates the position of the dragged element.
*
* @attribute placeholder
* @default null
*/
placeholder: {
value: null
},
/**
* Proxy element to be used when dragging.
*
* @attribute proxy
* @default null
*/
proxy: {
value: null,
setter: function(val) {
return A.merge({
moveOnEnd: false,
positionProxy: false
},
val || {}
);
}
},
/**
* Validates the condition for an element to be sorted.
*
* @attribute sortCondition
* @type Function
*/
sortCondition: {
value: function() {
return true;
},
setter: function(v) {
return A.bind(v, this);
},
validator: isFunction
}
},
/**
* Static property used to define which component it extends.
*
* @property EXTENDS
* @type Object
* @static
*/
EXTENDS: A.Base,
prototype: {
/**
* Construction logic executed during `A.SortableList` instantiation.
* Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
var instance = this;
var nodes = instance.get('nodes');
// drag & drop listeners
instance.on('drag:align', instance._onDragAlign);
instance.on('drag:end', instance._onDragEnd);
instance.on('drag:exit', instance._onDragExit);
instance.on('drag:mouseDown', instance._onDragMouseDown);
instance.on('drag:over', instance._onDragOver);
instance.on('drag:start', instance._onDragStart);
instance._createHelper();
if (nodes) {
instance.addAll(nodes);
nodes.each(
function(item) {
item.attr('draggable', true);
}
);
}
},
/**
* Creates a drag instance from a single node.
*
* @method add
* @param node
*/
add: function(node) {
var instance = this;
instance._createDrag(node);
},
/**
* Creates drag instances from a list of nodes.
*
* @method addAll
* @param nodes
*/
addAll: function(nodes) {
var instance = this;
nodes.each(function(node) {
instance.add(node);
});
},
/**
* Creates delayed drag instance.
*
* @method _createDrag
* @param node
* @protected
*/
_createDrag: function(node) {
var instance = this;
var helper = instance.get('helper');
if (!DDM.getDrag(node)) {
var dragOptions = {
bubbleTargets: instance,
node: node,
target: true
};
var proxyOptions = instance.get('proxy');
if (helper) {
proxyOptions.borderStyle = null;
}
// creating delayed drag instance
new A.DD.Drag(
A.mix(dragOptions, instance.get('dd'))
).plug(A.Plugin.DDProxy, proxyOptions)
.plug(A.Plugin.DDWinScroll);
}
},
/**
* Generates the `helper` node in the UI.
*
* @method _createHelper
* @protected
*/
_createHelper: function() {
var instance = this;
var helper = instance.get('helper');
if (helper) {
// append helper to the body
A.one('body').append(helper.hide());
instance.set('helper', helper);
}
},
/**
* Syncs the `placeholder` position in the UI.
*
* @method _updatePlaceholder
* @param event
* @param cancelAppend
* @protected
*/
_updatePlaceholder: function(event, cancelAppend) {
var instance = this;
var drag = event.target;
var drop = event.drop;
var dragNode = drag.get('node');
var dropNode = drop.get('node');
var dropContainer = instance.get('dropContainer');
var container;
if (dropContainer) {
container = dropContainer.apply(instance, arguments);
}
var floating = false;
var xDirection = instance.XDirection;
var yDirection = instance.YDirection;
if (dropNode.getStyle('float') !== 'none') {
floating = true;
}
var placeholder = instance.get('placeholder');
if (!placeholder) {
// if no placeholder use the dragNode instead
placeholder = dragNode;
}
if (!placeholder.contains(dropNode)) {
// check for the user dropCondition
var dropCondition = instance.get('dropCondition');
// if there is a container waiting for nodes to be appended it's
// priority
if (container && !cancelAppend && dropCondition(event)) {
// this checking avoid the parent bubbling drag:over
if (!container.contains(placeholder) && !placeholder.contains(container)) {
// append placeholder on the found container
container.append(placeholder);
}
}
// otherwise, check if it's floating and the xDirection
// or if it's not floating and the yDirection
else {
if ((floating && (xDirection === 'left')) || (!floating && (yDirection === 'up'))) {
// LEFT or UP directions means to place the placeholder
// before
dropNode.placeBefore(placeholder);
}
else {
// RIGHT or DOWN directions means to place the
// placeholder after
dropNode.placeAfter(placeholder);
}
}
}
},
/**
* Triggers when the drag position aligns.
*
* @method _onDragAlign
* @param event
* @protected
*/
_onDragAlign: function(event) {
var instance = this;
var lastX = instance.lastX;
var lastY = instance.lastY;
var xy = event.target.lastXY;
var x = xy[0];
var y = xy[1];
// if the y change
if (y !== lastY) {
// set the drag vertical direction
instance.YDirection = (y < lastY) ? 'up' : 'down';
}
// if the x change
if (x !== lastX) {
// set the drag horizontal direction
instance.XDirection = (x < lastX) ? 'left' : 'right';
}
instance.lastX = x;
instance.lastY = y;
},
/**
* Triggers when the drag event ends.
*
* @method _onDragEnd
* @param event
* @protected
*/
_onDragEnd: function(event) {
var instance = this;
var drag = event.target;
var dragNode = drag.get('node');
var placeholder = instance.get('placeholder');
if (placeholder) {
dragNode.show();
placeholder.hide();
if (!dragNode.contains(placeholder)) {
// position dragNode after the placeholder
placeholder.placeAfter(dragNode);
}
}
},
/**
* Triggers when the drag event exits.
*
* @method _onDragExit
* @param event
* @protected
*/
_onDragExit: function(event) {
var instance = this;
var sortCondition = instance.get('sortCondition');
if (sortCondition(event)) {
instance._updatePlaceholder(event, true);
}
},
/**
* Triggers when the drag mouse down.
*
* @method _onDragMouseDown
* @param event
* @protected
*/
_onDragMouseDown: function(event) {
var instance = this;
var drag = event.target;
var helper = instance.get('helper');
if (helper) {
// update the DRAG_NODE with the new helper
drag.set('dragNode', helper);
}
},
/**
* Triggers when the drag eventstarts.
*
* @method _onDragStart
* @param event
* @protected
*/
_onDragStart: function(event) {
var instance = this;
var drag = event.target;
var node = drag.get('node');
var helper = instance.get('helper');
var placeholder = instance.get('placeholder');
if (placeholder) {
// update placeholder height
placeholder.setStyle(
'height',
node.get('offsetHeight') + 'px'
);
node.hide();
placeholder.show();
// position placeholder after the node
node.placeAfter(placeholder);
if (helper) {
// show helper, we need display block here, yui dd hide it
// with display none
helper.setStyles({
display: 'block',
visibility: 'visible'
}).show();
}
}
},
/**
* Triggers when an element is being dragged over a valid drop target.
*
* @method _onDragOver
* @param event
* @protected
*/
_onDragOver: function(event) {
var instance = this;
var sortCondition = instance.get('sortCondition');
if (sortCondition(event)) {
instance._updatePlaceholder(event);
}
},
/**
* Sets node based in its type.
*
* @method _setNodes
* @param v
* @protected
* @return {NodeList}
*/
_setNodes: function(v) {
if (A.Lang.isNodeList(v)) {
return v;
}
else if (isString(v)) {
return A.all(v);
}
return new A.NodeList([v]);
}
}
});
A.SortableList = SortableList;