/**
* The SortableLayout Utility
*
* @module aui-sortable-layout
*/
var Lang = A.Lang,
isBoolean = Lang.isBoolean,
isFunction = Lang.isFunction,
isObject = Lang.isObject,
isString = Lang.isString,
isValue = Lang.isValue,
toInt = Lang.toInt,
ceil = Math.ceil,
DDM = A.DD.DDM,
// caching these values for performance
PLACEHOLDER_MARGIN_BOTTOM = 0,
PLACEHOLDER_MARGIN_TOP = 0,
PLACEHOLDER_TARGET_MARGIN_BOTTOM = 0,
PLACEHOLDER_TARGET_MARGIN_TOP = 0,
concat = function() {
return Array.prototype.slice.call(arguments).join(' ');
},
nodeListSetter = function(val) {
return A.Lang.isNodeList(val) ? val : A.all(val);
},
getNumStyle = function(elem, styleName) {
return toInt(elem.getStyle(styleName));
},
getCN = A.getClassName,
CSS_DRAG_INDICATOR = getCN('sortable-layout', 'drag', 'indicator'),
CSS_DRAG_INDICATOR_ICON = getCN('sortable-layout', 'drag', 'indicator', 'icon'),
CSS_DRAG_INDICATOR_ICON_LEFT = getCN('sortable-layout', 'drag', 'indicator', 'icon', 'left'),
CSS_DRAG_INDICATOR_ICON_RIGHT = getCN('sortable-layout', 'drag', 'indicator', 'icon', 'right'),
CSS_DRAG_TARGET_INDICATOR = getCN('sortable-layout', 'drag', 'target', 'indicator'),
CSS_ICON = getCN('icon'),
CSS_ICON_CIRCLE_TRIANGLE_L = getCN('icon', 'circle', 'triangle', 'l'),
CSS_ICON_CIRCLE_TRIANGLE_R = getCN('icon', 'circle', 'triangle', 'r'),
TPL_PLACEHOLDER = '<div class="' + CSS_DRAG_INDICATOR + '">' +
'<div class="' + concat(CSS_DRAG_INDICATOR_ICON, CSS_DRAG_INDICATOR_ICON_LEFT, CSS_ICON,
CSS_ICON_CIRCLE_TRIANGLE_R) + '"></div>' +
'<div class="' + concat(CSS_DRAG_INDICATOR_ICON, CSS_DRAG_INDICATOR_ICON_RIGHT, CSS_ICON,
CSS_ICON_CIRCLE_TRIANGLE_L) + '"></div>' +
'<div>';
/**
* A base class for SortableLayout, providing:
*
* - Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)
* - DragDrop utility for drag lists, portal layouts (portlets)
*
* Check the [live demo](http://alloyui.com/examples/sortable-layout/).
*
* @class A.SortableLayout
* @extends Base
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
*/
var SortableLayout = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property NAME
* @type String
* @static
*/
NAME: 'sortable-layout',
/**
* Static property used to define the default attribute
* configuration for the `A.SortableLayout`.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Configuration object for delegate.
*
* @attribute delegateConfig
* @default null
* @type Object
*/
delegateConfig: {
value: null,
setter: function(val) {
var instance = this;
var config = A.merge({
bubbleTargets: instance,
dragConfig: {},
nodes: instance.get('dragNodes'),
target: true
},
val
);
A.mix(config.dragConfig, {
groups: instance.get('groups'),
startCentered: true
});
return config;
},
validator: isObject
},
/**
* Proxy drag node used instead of dragging the original node.
*
* @attribute proxyNode
*/
proxyNode: {
setter: function(val) {
return isString(val) ? A.Node.create(val) : val;
}
},
/**
* The CSS class name used to define which nodes are draggable.
*
* @attribute dragNodes
* @type String
*/
dragNodes: {
validator: isString
},
/**
* The container which serves to host dropped elements.
*
* @attribute dropContainer
* @type Function
*/
dropContainer: {
value: function(dropNode) {
return dropNode;
},
validator: isFunction
},
/**
* The CSS class name used to define which nodes serve as container to
* be dropped.
*
* @attribute dropNodes
*/
dropNodes: {
setter: '_setDropNodes'
},
/**
* List of elements to add this sortable layout into.
*
* @attribute groups
* @type Array
*/
groups: {
value: ['sortable-layout']
},
/**
* Specifies if the start should be delayed.
*
* @attribute lazyStart
* @default false
* @type Boolean
*/
lazyStart: {
value: false,
validator: isBoolean
},
/**
* Simulates the position of the dragged element.
*
* @attribute placeholder
*/
placeholder: {
value: TPL_PLACEHOLDER,
setter: function(val) {
var placeholder = isString(val) ? A.Node.create(val) : val;
if (!placeholder.inDoc()) {
A.getBody().prepend(
placeholder.hide()
);
}
PLACEHOLDER_MARGIN_BOTTOM = getNumStyle(placeholder, 'marginBottom');
PLACEHOLDER_MARGIN_TOP = getNumStyle(placeholder, 'marginTop');
placeholder.addClass(CSS_DRAG_TARGET_INDICATOR);
PLACEHOLDER_TARGET_MARGIN_BOTTOM = getNumStyle(placeholder, 'marginBottom');
PLACEHOLDER_TARGET_MARGIN_TOP = getNumStyle(placeholder, 'marginTop');
return placeholder;
}
},
/**
* Proxy element to be used when dragging.
*
* @attribute proxy
* @default null
*/
proxy: {
value: null,
setter: function(val) {
var instance = this;
var defaults = {
moveOnEnd: false,
positionProxy: false
};
// if proxyNode is set remove the border from the default proxy
if (instance.get('proxyNode')) {
defaults.borderStyle = null;
}
return A.merge(defaults, val || {});
}
}
},
/**
* Static property used to define which component it extends.
*
* @property EXTENDS
* @type Object
* @static
*/
EXTENDS: A.Base,
prototype: {
/**
* Construction logic executed during `A.SortableLayout` instantiation.
* Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
var instance = this;
instance.bindUI();
},
/**
* Bind the events on the `A.SortableLayout` UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function() {
var instance = this;
// publishing placeholderAlign event
instance.publish('placeholderAlign', {
defaultFn: instance._defPlaceholderAlign,
queuable: false,
emitFacade: true,
bubbles: true
});
instance._bindDDEvents();
instance._bindDropZones();
},
/**
* Checks if the `Node` isn't a drop node. If not, creates a new Drop
* instance and adds to drop target group.
*
* @method addDropNode
* @param node
* @param config
*/
addDropNode: function(node, config) {
var instance = this;
node = A.one(node);
if (!DDM.getDrop(node)) {
instance.addDropTarget(
// Do not use DropPlugin to create the DropZones on
// this component, the ".drop" namespace is used to check
// for the DD.Delegate target nodes
new A.DD.Drop(
A.merge({
bubbleTargets: instance,
groups: instance.get('groups'),
node: node
},
config
)
)
);
}
},
/**
* Adds a Drop instance to a group.
*
* @method addDropTarget
* @param drop
*/
addDropTarget: function(drop) {
var instance = this;
drop.addToGroup(
instance.get('groups')
);
},
/**
* Sync placeholder size and set its X and Y positions.
*
* @method alignPlaceholder
* @param region
* @param isTarget
*/
alignPlaceholder: function(region, isTarget) {
var instance = this;
var placeholder = instance.get('placeholder');
if (!instance.lazyEvents) {
placeholder.show();
}
// sync placeholder size
instance._syncPlaceholderSize();
placeholder.setXY(
instance.getPlaceholderXY(region, isTarget)
);
},
/**
* Calculates drag's X and Y directions.
*
* @method calculateDirections
* @param drag
*/
calculateDirections: function(drag) {
var instance = this;
var lastY = instance.lastY;
var lastX = instance.lastX;
var x = drag.lastXY[0];
var y = drag.lastXY[1];
// if the x change
if (x !== lastX) {
// set the drag direction
instance.XDirection = (x < lastX) ? 'left' : 'right';
}
// if the y change
if (y !== lastY) {
// set the drag direction
instance.YDirection = (y < lastY) ? 'up' : 'down';
}
instance.lastX = x;
instance.lastY = y;
},
/**
* Calculates quadrant position.
*
* @method calculateQuadrant
* @param drag
* @param drop
* @return {Number}
*/
calculateQuadrant: function(drag, drop) {
var instance = this;
var quadrant = 1;
var region = drop.get('node').get('region');
var mouseXY = drag.mouseXY;
var mouseX = mouseXY[0];
var mouseY = mouseXY[1];
var top = region.top;
var left = region.left;
// (region.bottom - top) finds the height of the region
var vCenter = top + (region.bottom - top) / 2;
// (region.right - left) finds the width of the region
var hCenter = left + (region.right - left) / 2;
if (mouseY < vCenter) {
quadrant = (mouseX > hCenter) ? 1 : 2;
}
else {
quadrant = (mouseX < hCenter) ? 3 : 4;
}
instance.quadrant = quadrant;
return quadrant;
},
/**
* Gets placeholder X and Y positions.
*
* @method getPlaceholderXY
* @param region
* @param isTarget
* @return {Array}
*/
getPlaceholderXY: function(region, isTarget) {
var instance = this;
var placeholder = instance.get('placeholder');
var marginBottom = PLACEHOLDER_MARGIN_BOTTOM;
var marginTop = PLACEHOLDER_MARGIN_TOP;
if (isTarget) {
// update the margin values in case of the target placeholder
// has a different margin
marginBottom = PLACEHOLDER_TARGET_MARGIN_BOTTOM;
marginTop = PLACEHOLDER_TARGET_MARGIN_TOP;
}
// update the className of the placeholder when interact with target
// (drag/drop) elements
placeholder.toggleClass(CSS_DRAG_TARGET_INDICATOR, isTarget);
var regionBottom = ceil(region.bottom);
var regionLeft = ceil(region.left);
var regionTop = ceil(region.top);
var x = regionLeft;
// 1 and 2 quadrants are the top quadrants, so align to the
// region.top when quadrant < 3
var y = (instance.quadrant < 3) ?
(regionTop - (placeholder.get('offsetHeight') + marginBottom)) : (regionBottom + marginTop);
return [x, y];
},
/**
* Removes a Drop instance from group.
*
* @method removeDropTarget
* @param drop
*/
removeDropTarget: function(drop) {
var instance = this;
drop.removeFromGroup(
instance.get('groups')
);
},
/**
* Checks if active drag and active drop satisfies the align condition.
*
* @method _alignCondition
* @protected
* @return {Boolean}
*/
_alignCondition: function() {
var instance = this;
var activeDrag = DDM.activeDrag;
var activeDrop = instance.activeDrop;
var retVal = true;
if (activeDrag && activeDrop) {
var dragNode = activeDrag.get('node');
var dropNode = activeDrop.get('node');
if (!dragNode.compareTo(dropNode) && dragNode.contains(dropNode)) {
retVal = false;
}
}
return retVal;
},
/**
* Creates `DD.Delegate` instance, plugs it to the `DDProxy`, and binds
* Drag and Drop events.
*
* @method _bindDDEvents
* @protected
*/
_bindDDEvents: function() {
var instance = this;
var delegateConfig = instance.get('delegateConfig');
var proxy = instance.get('proxy');
// creating DD.Delegate instance
instance.delegate = new A.DD.Delegate(delegateConfig);
// plugging the DDProxy
instance.delegate.dd.plug(A.Plugin.DDProxy, proxy);
instance.on('drag:end', A.bind(instance._onDragEnd, instance));
instance.on('drag:enter', A.bind(instance._onDragEnter, instance));
instance.on('drag:exit', A.bind(instance._onDragExit, instance));
instance.on('drag:over', A.bind(instance._onDragOver, instance));
instance.on('drag:start', A.bind(instance._onDragStart, instance));
instance.after('drag:start', A.bind(instance._afterDragStart, instance));
instance.on('quadrantEnter', instance._syncPlaceholderUI);
instance.on('quadrantExit', instance._syncPlaceholderUI);
},
/**
* Bind drop zones.
*
* @method _bindDropZones
* @protected
*/
_bindDropZones: function() {
var instance = this;
var dropNodes = instance.get('dropNodes');
if (dropNodes) {
dropNodes.each(function(node) {
instance.addDropNode(node);
});
}
},
/**
* Defines `placeholder` alignment.
*
* @method _defPlaceholderAlign
* @param event
* @protected
*/
_defPlaceholderAlign: function() {
var instance = this;
var activeDrop = instance.activeDrop;
var placeholder = instance.get('placeholder');
if (activeDrop && placeholder) {
var node = activeDrop.get('node');
// DD.Delegate use the Drop Plugin on its "target" items. Using
// Drop Plugin a "node.drop" namespace is created. Using the
// .drop namespace to detect when the node is also a "target"
// DD.Delegate node
var isTarget = !! node.drop;
instance.lastAlignDrop = activeDrop;
instance.alignPlaceholder(
activeDrop.get('node').get('region'),
isTarget
);
}
},
/**
* Gets a collection formed by `drag`, `drop`, `quadrant`, `XDirection`,
* and `YDirection` instances.
*
* @method _evOutput
* @protected
* @return {Object}
*/
_evOutput: function() {
var instance = this;
return {
drag: DDM.activeDrag,
drop: instance.activeDrop,
quadrant: instance.quadrant,
XDirection: instance.XDirection,
YDirection: instance.YDirection
};
},
/**
* Fire quadrant events and updates "last" informations.
*
* @method _fireQuadrantEvents
* @protected
*/
_fireQuadrantEvents: function() {
var instance = this;
var evOutput = instance._evOutput();
var lastQuadrant = instance.lastQuadrant;
var quadrant = instance.quadrant;
if (quadrant !== lastQuadrant) {
// only trigger exit if it has previously entered in any quadrant
if (lastQuadrant) {
// merging event with the "last" information
instance.fire(
'quadrantExit',
A.merge({
lastDrag: instance.lastDrag,
lastDrop: instance.lastDrop,
lastQuadrant: instance.lastQuadrant,
lastXDirection: instance.lastXDirection,
lastYDirection: instance.lastYDirection
},
evOutput
)
);
}
// firing EV_QUADRANT_ENTER event
instance.fire('quadrantEnter', evOutput);
}
// firing EV_QUADRANT_OVER, align event fires like the drag over
// without bubbling for performance reasons
instance.fire('quadrantOver', evOutput);
// updating "last" information
instance.lastDrag = DDM.activeDrag;
instance.lastDrop = instance.activeDrop;
instance.lastQuadrant = quadrant;
instance.lastXDirection = instance.XDirection;
instance.lastYDirection = instance.YDirection;
},
/**
* Gets node from the currently active draggable object.
*
* @method _getAppendNode
* @protected
* @return {Node}
*/
_getAppendNode: function() {
return DDM.activeDrag.get('node');
},
/**
* Sets the position of drag/drop nodes.
*
* @method _positionNode
* @param event
* @protected
*/
_positionNode: function() {
var instance = this;
var activeDrop = instance.lastAlignDrop || instance.activeDrop;
if (activeDrop) {
var dragNode = instance._getAppendNode();
var dropNode = activeDrop.get('node');
// detects if the activeDrop is a dd target (portlet) or a drop
// area only (column) DD.Delegate use the Drop Plugin on its
// "target" items. Using Drop Plugin a "node.drop" namespace is
// created. Using the .drop namespace to detect when the node is
// also a "target" DD.Delegate node
var isTarget = isValue(dropNode.drop);
var topQuadrants = (instance.quadrant < 3);
if (instance._alignCondition()) {
if (isTarget) {
dropNode[topQuadrants ? 'placeBefore' : 'placeAfter'](dragNode);
}
// interacting with the columns (drop areas only)
else {
// find the dropContainer of the dropNode, the default
// DROP_CONTAINER function returns the dropNode
var dropContainer = instance.get('dropContainer').apply(instance, [dropNode]);
dropContainer[topQuadrants ? 'prepend' : 'append'](dragNode);
}
}
}
},
/**
* Sync `placeholder` attribute in the UI.
*
* @method _syncPlaceholderUI
* @param event
* @protected
*/
_syncPlaceholderUI: function(event) {
var instance = this;
if (instance._alignCondition()) {
// firing placeholderAlign event
instance.fire('placeholderAlign', {
drop: instance.activeDrop,
originalEvent: event
});
}
},
/**
* Sync `placeholder` node size.
*
* @method _syncPlaceholderSize
* @protected
*/
_syncPlaceholderSize: function() {
var instance = this;
var node = instance.activeDrop.get('node');
var placeholder = instance.get('placeholder');
if (placeholder) {
placeholder.set(
'offsetWidth',
node.get('offsetWidth')
);
}
},
/**
* Sync `proxyNode` attribute in the UI.
*
* @method _syncProxyNodeUI
* @param event
* @protected
*/
_syncProxyNodeUI: function() {
var instance = this;
var dragNode = DDM.activeDrag.get('dragNode');
var proxyNode = instance.get('proxyNode');
if (proxyNode && !proxyNode.compareTo(dragNode)) {
dragNode.append(proxyNode);
instance._syncProxyNodeSize();
}
},
/**
* Sync `proxyNode` height and width.
*
* @method _syncProxyNodeSize
* @protected
*/
_syncProxyNodeSize: function() {
var instance = this;
var node = DDM.activeDrag.get('node');
var proxyNode = instance.get('proxyNode');
if (node && proxyNode) {
proxyNode.set(
'offsetHeight',
node.get('offsetHeight')
);
proxyNode.set(
'offsetWidth',
node.get('offsetWidth')
);
}
},
/**
* Triggers after drag event starts.
*
* @method _afterDragStart
* @param event
* @protected
*/
_afterDragStart: function(event) {
var instance = this;
if (instance.get('proxy')) {
instance._syncProxyNodeUI(event);
}
},
/**
* Triggers when the drag event ends.
*
* @method _onDragEnd
* @param event
* @protected
*/
_onDragEnd: function(event) {
var instance = this;
var placeholder = instance.get('placeholder');
var proxyNode = instance.get('proxyNode');
if (!instance.lazyEvents) {
instance._positionNode(event);
}
if (proxyNode) {
proxyNode.remove();
}
if (placeholder) {
placeholder.hide();
}
// reset the last information
instance.lastQuadrant = null;
instance.lastXDirection = null;
instance.lastYDirection = null;
},
/**
* Triggers when the dragged object first interacts with another
* targettable drag and drop object.
*
* @method _onDragEnter
* @param event
* @protected
*/
_onDragEnter: function(event) {
var instance = this;
instance.activeDrop = DDM.activeDrop;
// check if lazyEvents is true and if there is a lastActiveDrop the
// checking for lastActiveDrop prevents fire the _syncPlaceholderUI
// when quadrant* events fires
if (instance.lazyEvents && instance.lastActiveDrop) {
instance.lazyEvents = false;
instance._syncPlaceholderUI(event);
}
// lastActiveDrop is always updated by the drag exit, but if there
// is no lastActiveDrop update it on drag enter update it
if (!instance.lastActiveDrop) {
instance.lastActiveDrop = DDM.activeDrop;
}
},
/**
* Triggers when the drag event exits.
*
* @method _onDragExit
* @param event
* @protected
*/
_onDragExit: function(event) {
var instance = this;
instance._syncPlaceholderUI(event);
instance.activeDrop = DDM.activeDrop;
instance.lastActiveDrop = DDM.activeDrop;
},
/**
* Triggers when an element is being dragged over a valid drop target.
*
* @method _onDragOver
* @param event
* @protected
*/
_onDragOver: function(event) {
var instance = this;
var drag = event.drag;
// prevent drag over bubbling, filtering the top most element
if (instance.activeDrop === DDM.activeDrop) {
instance.calculateDirections(drag);
instance.calculateQuadrant(drag, instance.activeDrop);
instance._fireQuadrantEvents();
}
},
/**
* Triggers when the drag event starts.
*
* @method _onDragStart
* @param event
* @protected
*/
_onDragStart: function() {
var instance = this;
if (instance.get('lazyStart')) {
instance.lazyEvents = true;
}
instance.lastActiveDrop = null;
instance.activeDrop = DDM.activeDrop;
},
/**
* Sets group of drop nodes.
*
* @method _setDropNodes
* @param val
* @protected
* @return {NodeList}
*/
_setDropNodes: function(val) {
var instance = this;
if (isFunction(val)) {
val = val.call(instance);
}
return nodeListSetter(val);
}
}
});
A.SortableLayout = SortableLayout;