Alloy UI

aui-portal-layout  1.0.1

 
Filters
AUI.add('aui-portal-layout', function(A) {
/**
 * The PortalLayout Utility - Full documentation coming soon.
 *
 * @module aui-portal-layout
 */

var Lang = A.Lang,
	isBoolean = Lang.isBoolean,
	isFunction = Lang.isFunction,
	isObject = Lang.isObject,
	isString = Lang.isString,
	isValue = Lang.isValue,

	ceil = Math.ceil,

	DDM = A.DD.DDM,

	APPEND = 'append',
	CIRCLE = 'circle',
	DELEGATE_CONFIG = 'delegateConfig',
	DOWN = 'down',
	DRAG = 'drag',
	DRAG_NODE = 'dragNode',
	DRAG_NODES = 'dragNodes',
	DROP_CONTAINER = 'dropContainer',
	DROP_NODES = 'dropNodes',
	GROUPS = 'groups',
	ICON = 'icon',
	INDICATOR = 'indicator',
	L = 'l',
	LAZY_START = 'lazyStart',
	LEFT = 'left',
	MARGIN_BOTTOM = 'marginBottom',
	MARGIN_TOP = 'marginTop',
	NODE = 'node',
	OFFSET_HEIGHT = 'offsetHeight',
	OFFSET_WIDTH = 'offsetWidth',
	PLACEHOLDER = 'placeholder',
	PLACE_AFTER = 'placeAfter',
	PLACE_BEFORE = 'placeBefore',
	PORTAL_LAYOUT = 'portal-layout',
	PREPEND = 'prepend',
	PROXY = 'proxy',
	PROXY_NODE = 'proxyNode',
	R = 'r',
	REGION = 'region',
	RIGHT = 'right',
	SPACE = ' ',
	TARGET = 'target',
	TRIANGLE = 'triangle',
	UP = 'up',

	EV_PLACEHOLDER_ALIGN = 'placeholderAlign',
	EV_QUADRANT_ENTER = 'quadrantEnter',
	EV_QUADRANT_EXIT = 'quadrantExit',
	EV_QUADRANT_OVER = 'quadrantOver',

	// caching these values for performance
	PLACEHOLDER_MARGIN_BOTTOM = 0,
	PLACEHOLDER_MARGIN_TOP = 0,
	PLACEHOLDER_TARGET_MARGIN_BOTTOM = 0,
	PLACEHOLDER_TARGET_MARGIN_TOP = 0,

	isNodeList = function(v) {
		return (v instanceof A.NodeList);
	},

	concat = function() {
		return Array.prototype.slice.call(arguments).join(SPACE);
	},

	nodeListSetter = function(val) {
		return isNodeList(val) ? val : A.all(val);
	},

	getNumStyle = function(elem, styleName) {
		return parseInt(elem.getStyle(styleName), 10) || 0;
	},

	getCN = A.ClassNameManager.getClassName,

	CSS_DRAG_INDICATOR = getCN(PORTAL_LAYOUT, DRAG, INDICATOR),
	CSS_DRAG_INDICATOR_ICON = getCN(PORTAL_LAYOUT, DRAG, INDICATOR, ICON),
	CSS_DRAG_INDICATOR_ICON_LEFT = getCN(PORTAL_LAYOUT, DRAG, INDICATOR, ICON, LEFT),
	CSS_DRAG_INDICATOR_ICON_RIGHT = getCN(PORTAL_LAYOUT, DRAG, INDICATOR, ICON, RIGHT),
	CSS_DRAG_TARGET_INDICATOR = getCN(PORTAL_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 PortalLayout, providing:
 * <ul>
 *    <li>Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)</li>
 *    <li>DragDrop utility for drag lists, portal layouts (portlets)</li>
 * </ul>
 *
 * Quick Example:<br/>
 *
 * <pre><code>var portalLayout = new A.PortalLayout({
 *  	dragNodes: '.portlet',
 *  	dropNodes: '.column',
 *  	proxyNode: A.Node.create('<div class="aui-portal-layout-proxy"></div>'),
 *  	lazyStart: true
 * </code></pre>
 *
 * Check the list of <a href="PortalLayout.html#configattributes">Configuration Attributes</a> available for
 * PortalLayout.
 *
 * @param config {Object} Object literal specifying widget configuration properties.
 *
 * @class PortalLayout
 * @constructor
 * @extends Base
 */
var PortalLayout = A.Component.create(
	{
		/**
		 * Static property provides a string to identify the class.
		 *
		 * @property PortalLayout.NAME
		 * @type String
		 * @static
		 */
		NAME: PORTAL_LAYOUT,

		/**
		 * Static property used to define the default attribute
		 * configuration for the PortalLayout.
		 *
		 * @property PortalLayout.ATTRS
		 * @type Object
		 * @static
		 */
		ATTRS: {
			delegateConfig: {
				value: null,
				setter: function(val) {
					var instance = this;

					var config = A.merge(
						{
							bubbleTargets: instance,
							dragConfig: {},
							nodes: instance.get(DRAG_NODES),
							target: true
						},
						val
					);

					A.mix(config.dragConfig, {
						groups: instance.get(GROUPS),
						startCentered: true
					});

					return config;
				},
				validator: isObject
			},

			proxyNode: {
				setter: function(val) {
					return isString(val) ? A.Node.create(val) : val;
				}
			},

			dragNodes: {
				validator: isString
			},

			dropContainer: {
				value: function(dropNode) {
					return dropNode;
				},
				validator: isFunction
			},

			dropNodes: {
				value: false,
				setter: nodeListSetter
			},

			groups: {
				value: [PORTAL_LAYOUT]
			},

			lazyStart: {
				value: false,
				validator: isBoolean
			},

			placeholder: {
				value: TPL_PLACEHOLDER,
				setter: function(val) {
					var placeholder = isString(val) ? A.Node.create(val) : val;

					if (!placeholder.inDoc()) {
						A.getBody().append(
							placeholder.hide()
						);
					}

					PLACEHOLDER_MARGIN_BOTTOM = getNumStyle(placeholder, MARGIN_BOTTOM);
					PLACEHOLDER_MARGIN_TOP = getNumStyle(placeholder, MARGIN_TOP);

					placeholder.addClass(CSS_DRAG_TARGET_INDICATOR);

					PLACEHOLDER_TARGET_MARGIN_BOTTOM = getNumStyle(placeholder, MARGIN_BOTTOM);
					PLACEHOLDER_TARGET_MARGIN_TOP = getNumStyle(placeholder, MARGIN_TOP);

					return placeholder;
				}
			},

			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(PROXY_NODE)) {
						defaults.borderStyle = null;
					}

					return A.merge(defaults, val || {});
				}
			}
		},

		EXTENDS: A.Base,

		prototype: {
			/**
			 * Construction logic executed during PortalLayout instantiation. Lifecycle.
			 *
			 * @method initializer
			 * @protected
			 */
			initializer: function() {
				var instance = this;

				instance.bindUI();
			},

			bindUI: function() {
				var instance = this;

				// publishing placeholderAlign event
				instance.publish(EV_PLACEHOLDER_ALIGN, {
		            defaultFn: instance._defPlaceholderAlign,
		            queuable: false,
		            emitFacade: true,
		            bubbles: true
		        });

				instance._bindDDEvents();
				instance._bindDropZones();
			},

			/*
			* Methods
			*/
			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,
									node: node
								},
								config
							)
						)
					);
				}
			},

			addDropTarget: function(drop) {
				var instance = this;

				drop.addToGroup(
					instance.get(GROUPS)
				);
			},

			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)
				);
			},

			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;
			},

			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;
			},

			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(OFFSET_HEIGHT) + marginBottom)) : (regionBottom + marginTop);

				return [ x, y ];
			},

			removeDropTarget: function(drop) {
				var instance = this;

				drop.removeFromGroup(
					instance.get(GROUPS)
				);
			},

			_alignCondition: function() {
				var instance = this;
				var activeDrag = DDM.activeDrag;
				var activeDrop = instance.activeDrop;

				if (activeDrag && activeDrop) {
					var dragNode = activeDrag.get(NODE);
					var dropNode = activeDrop.get(NODE);

					return !dragNode.contains(dropNode);
				}

				return true;
			},

			_bindDDEvents: function() {
				var instance = this;
				var delegateConfig = instance.get(DELEGATE_CONFIG);
				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(EV_QUADRANT_ENTER, instance._syncPlaceholderUI);
				instance.on(EV_QUADRANT_EXIT, instance._syncPlaceholderUI);
			},

			_bindDropZones: function() {
				var instance = this;

				instance.get(DROP_NODES).each(function(node, i) {
					instance.addDropNode(node);
				});
			},

			_defPlaceholderAlign: function(event) {
				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
					);
				}
			},

			_evOutput: function() {
				var instance = this;

				return {
					drag: DDM.activeDrag,
					drop: instance.activeDrop,
					quadrant: instance.quadrant,
					XDirection: instance.XDirection,
					YDirection: instance.YDirection
				};
			},

			_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(
							EV_QUADRANT_EXIT,
							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(EV_QUADRANT_ENTER, evOutput);
				}

				// firing EV_QUADRANT_OVER, align event fires like the drag over without bubbling for performance reasons
				instance.fire(EV_QUADRANT_OVER, evOutput);

				// updating "last" information
				instance.lastDrag = DDM.activeDrag;
				instance.lastDrop = instance.activeDrop;
				instance.lastQuadrant = quadrant;
				instance.lastXDirection = instance.XDirection;
				instance.lastYDirection = instance.YDirection;
			},

			_getAppendNode: function() {
				return DDM.activeDrag.get(NODE);
			},

			_positionNode: function(event) {
				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 ? PLACE_BEFORE : PLACE_AFTER ](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(DROP_CONTAINER).apply(instance, [dropNode]);

							dropContainer[ topQuadrants ? PREPEND : APPEND ](dragNode);
						}
					}
				}
			},

			_syncPlaceholderUI: function(event) {
				var instance = this;

				if (instance._alignCondition()) {
					// firing placeholderAlign event
					instance.fire(EV_PLACEHOLDER_ALIGN, {
						drop: instance.activeDrop,
						originalEvent: event
					});
				}
			},

			_syncPlaceholderSize: function() {
				var instance = this;
				var node = instance.activeDrop.get(NODE);

				var placeholder = instance.get(PLACEHOLDER);

				if (placeholder) {
					placeholder.set(
						OFFSET_WIDTH,
						node.get(OFFSET_WIDTH)
					);
				}
			},

			_syncProxyNodeUI: function(event) {
				var instance = this;
				var dragNode = DDM.activeDrag.get(DRAG_NODE);
				var proxyNode = instance.get(PROXY_NODE);

				if (proxyNode && !proxyNode.compareTo(dragNode)) {
					dragNode.append(proxyNode);

					instance._syncProxyNodeSize();
				}
			},

			_syncProxyNodeSize: function() {
				var instance = this;
				var node = DDM.activeDrag.get(NODE);
				var proxyNode = instance.get(PROXY_NODE);

				if (node && proxyNode) {
					proxyNode.set(
						OFFSET_HEIGHT,
						node.get(OFFSET_HEIGHT)
					);

					proxyNode.set(
						OFFSET_WIDTH,
						node.get(OFFSET_WIDTH)
					);
				}
			},

			/*
			* Listeners
			*/
			_afterDragStart: function(event) {
				var instance = this;

				if (instance.get(PROXY)) {
					instance._syncProxyNodeUI(event);
				}
			},

			_onDragEnd: function(event) {
				var instance = this;
				var placeholder = instance.get(PLACEHOLDER);
				var proxyNode = instance.get(PROXY_NODE);

				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;
			},

			// fires after drag:start
			_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;
				}
			},

			_onDragExit: function(event) {
				var instance = this;

				instance._syncPlaceholderUI(event);

				instance.activeDrop = DDM.activeDrop;

				instance.lastActiveDrop = DDM.activeDrop;
			},

			_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();
				}
			},

			// fires before drag:enter
			_onDragStart: function(event) {
				var instance = this;

				if (instance.get(LAZY_START)) {
					instance.lazyEvents = true;
				}

				instance.lastActiveDrop = null;

				instance.activeDrop = DDM.activeDrop;
			}
		}
	}
);

A.PortalLayout = PortalLayout;

}, '@VERSION@' ,{skinnable:true, requires:['aui-base','dd-drag','dd-delegate','dd-drop','dd-proxy']});