Source: widgets/commonCtrls/kekule.widget.movers.js

/**
 * @fileoverview
 * Enable widget or element to be movable by mouse or touch.
 * @author Partridge Jiang
 */


/*
 * requires /lan/classes.js
 * requires /utils/kekule.utils.js
 * requires /utils/kekule.domUtils.js
 * requires /xbrowsers/kekule.x.js
 * requires /widgets/kekule.widget.base.js
 */

(function(){

"use strict";

var EU = Kekule.HtmlElementUtils;
var SU = Kekule.StyleUtils;
var EV = Kekule.X.Event;
var PS = Class.PropertyScope;

/**
 * An helper class tp make an element or widget movable.
 * @class
 * @augments ObjectEx
 *
 * @property {Bool} enabled
 * @property {Object} target Target HTML element or widget to be moved.
 *   Note, the target element must be an absiolute positioned one.
 * @property {Object} gripper When mouse down or touch down on gripper element/widget, movement begins.
 *   If this property is not set, click on target itself will start the moving.
 * @property {Bool} isMoving
 * @property {String} cssPropX CSS property name to set position on X axis, default is 'left'.
 * @property {String} cssPropY CSS property name to set position on Y axis, default is 'top'.
 * @property {String} movingCursor Cursor changed to this one when the moving starts.
 * @property {Int} movingCursorDelay When mouse down, after this delayed millisecond, cursor will be changed to moving one.
 */
Kekule.Widget.MoveHelper = Class.create(ObjectEx,
/** @lends Kekule.Widget.MoveHelper# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Widget.MoveHelper',
	/** @constructs */
	initialize: function($super, target, gripper)
	{
		$super();

		this.reactMouseDownBind = this.reactMouseDown.bind(this);
		this.reactMouseUpBind = this.reactMouseUp.bind(this);
		this.reactMouseMoveBind = this.reactMouseMove.bind(this);
		this.reactTouchDownBind = this.reactTouchDown.bind(this);
		this.reactTouchEndBind = this.reactTouchEnd.bind(this);
		this.reactTouchMoveBind = this.reactTouchMove.bind(this);

		this.__$K_initialMoverInfo__ = {};  // private

		this.setCssPropX('left').setCssPropY('top').setMovingCursor('move').setMovingCursorDelay(500);

		this.setTarget(target);
		this.setGripper(gripper);
		this.setEnabled(true);
	},
	doFinalize: function($super)
	{
		this.setGripper(null);  // uninstall all event handlers
		$super();
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('enabled', {'dataType': DataType.BOOL});
		this.defineProp('target', {'dataType': DataType.OBJECT, 'serializable': false});
		this.defineProp('gripper', {'dataType': DataType.OBJECT, 'serializable': false,
			'getter': function()
			{
				return this.getPropStoreFieldValue('gripper') || this.getTarget();
			},
			'setter': function(value)
			{
				var oldValue = this.getPropStoreFieldValue('gripper');
				if (value !== oldValue)
				{
					this.setPropStoreFieldValue('gripper', value);
					this.gripperChanged(value, oldValue);
				}
			}
		});
		this.defineProp('isMoving', {'dataType': DataType.BOOL, 'serializable': false});
		this.defineProp('cssPropX', {'dataType': DataType.STRING});
		this.defineProp('cssPropY', {'dataType': DataType.STRING});
		this.defineProp('movingCursor', {'dataType': DataType.STRING});
		this.defineProp('movingCursorDelay', {'dataType': DataType.INT});
		this.defineProp('movable', {'dataType': DataType.BOOL, 'setter': null,
			'getter': function()
			{
				return this.getEnabled() && this.getGripper();
			}
		})
	},

	/** @private */
	_objIsWidget: function(obj)
	{
		return obj instanceof Kekule.Widget.BaseWidget;
	},
	/** @private */
	_getElement: function(obj)
	{
		if (this._objIsWidget(obj))
			return obj.getElement();
		else
			return obj;
	},
	/** @private */
	getTargetElement: function()
	{
		var t = this.getTarget();
		return this._objIsWidget(t)? t.getElement(): t;
	},
	/** @private */
	getGripperElement: function()
	{
		var g = this.getGripper();
		return this._objIsWidget(g)? g.getElement(): g;
	},
	/** @private */
	/*
	getGripperWidget: function(gripperElem)
	{
		var g = gripperElem || this.getGripper();
		return this._objIsWidget(g)? g: null;
	},
	*/
	/** @private */
	gripperChanged: function(newGripper, oldGripper)
	{
		if (oldGripper)
			this.installEventHandlers(this._getElement(oldGripper));
		if (newGripper)
			this.installEventHandlers(this._getElement(newGripper));
	},
	/** @private */
	installEventHandlers: function(gripperElem)
	{
		if (gripperElem)
		{
			EV.addListener(gripperElem, 'mousedown', this.reactMouseDownBind);
			//EV.addListener(gripperElem, 'mouseup', this.reactMouseUpBind);
			//EV.addListener(gripperElem, 'mousemove', this.reactMouseMoveBind);
			EV.addListener(gripperElem, 'touchstart', this.reactTouchDownBind);
			//EV.addListener(gripperElem, 'touchend', this.reactTouchEndBind);
			//EV.addListener(gripperElem, 'touchmove', this.reactTouchMoveBind);
		}
	},
	/** @private */
	uninstallEventHandlers: function(gripperElem)
	{
		if (gripperElem)
		{
			EV.removeListener(gripperElem, 'mousedown', this.reactMouseDownBind);
			//EV.removeListener(gripperElem, 'mouseup', this.reactMouseUpBind);
			//EV.removeListener(gripperElem, 'mousemove', this.reactMouseMoveBind);
			EV.removeListener(gripperElem, 'touchstart', this.reactTouchDownBind);
			//EV.removeListener(gripperElem, 'touchend', this.reactTouchEndBind);
			//EV.removeListener(gripperElem, 'touchmove', this.reactTouchMoveBind);
		}
	},

	/** @private */
	_elemSupportCapture: function(elem)
	{
		return !!elem.setCapture;
	},
	/** @private */
	_getMoveEventReceiverElem: function(gripperElem)
	{
		return (/*this.getGripperWidget(gripperElem) ||*/ this._elemSupportCapture(gripperElem))?
				gripperElem: gripperElem.ownerDocument.documentElement;
	},
	/** @private */
	setMouseCapture: function(capture)
	{
		/*
		var w = this.getGripperWidget();
		if (w)
		{
			w.setMouseCapture(capture);
		}
		else // gripper is not widget, just element
		*/
		{
			var gripperElem = this.getGripperElement();
			if (gripperElem)
			{
				if (capture)
				{
					if (gripperElem.setCapture)
						gripperElem.setCapture(true);
				}
				else
				{
					if (gripperElem.releaseCapture)
						gripperElem.releaseCapture();
				}
			}
		}
	},

	/** @private */
	beginMoving: function(initialScreenCoord)
	{
		if (this.getIsMoving())
			return;

		var targetElem = this.getTargetElement();
		EU.makePositioned(targetElem);

		// save initial information
		this.saveInitialInformation(initialScreenCoord);

		// install move event listener
		var moveReceiver = this._getMoveEventReceiverElem(this.getGripperElement());
		EV.addListener(moveReceiver, 'mousemove', this.reactMouseMoveBind);
		EV.addListener(moveReceiver, 'touchmove', this.reactTouchMoveBind);
		EV.addListener(moveReceiver, 'mouseup', this.reactMouseUpBind);
		EV.addListener(moveReceiver, 'touchend', this.reactTouchEndBind);

		/*
		var gripperElem = this.getGripperElement();
		if (gripperElem.setCapture)
			gripperElem.setCapture(true);
		*/
		this.setMouseCapture(true);
		this._setCursorHandle = this.delaySetMoveCursor();
		this.setIsMoving(true);
		//console.log('begin move');
	},
	/** @private */
	endMoving: function()
	{
		this.setIsMoving(false);
		this.setMouseCapture(false);
		// restore gripper cursor and remove moving event listener
		var gripper = this.getGripperElement();
		var moveReceiver = this._getMoveEventReceiverElem(gripper);
		EV.removeListener(moveReceiver, 'mousemove', this.reactMouseMoveBind);
		EV.removeListener(moveReceiver, 'touchmove', this.reactTouchMoveBind);
		EV.removeListener(moveReceiver, 'mouseup', this.reactMouseUpBind);
		EV.removeListener(moveReceiver, 'touchend', this.reactTouchEndBind);

		// restore cursor
		this.clearCursorSetter();
		if (gripper)
		{
			var cursor = this.__$K_initialMoverInfo__.cursor;
			gripper.style.cursor = cursor || '';
			this.__$K_initialMoverInfo__.cursor = null;
		}
		//console.log('begin move');
	},
	/** @private */
	moveTo: function(newPointerCoord)
	{
		// set move cursor instantly
		this.clearCursorSetter();
		this.doSetMoveCursor();
		//console.log('move to ', newPointerCoord);
		var elem = this.getTargetElement();
		if (!elem)
			return;
		var initInfo = this.__$K_initialMoverInfo__;
		var coordDelta = Kekule.CoordUtils.substract(newPointerCoord, initInfo.pointerCoord);
		//console.log(initInfo.pointerCoord.y, newPointerCoord.y, initInfo.elemCoord.y);
		//var newElemPos = Kekule.CoordUtils.add(initInfo.elemCoord, coordDelta);
		if ((this.getCssPropX() || '').toLowerCase() === 'right')
			elem.style.right = (initInfo.elemCoord.x - coordDelta.x) + 'px';
		else
			elem.style.left = (initInfo.elemCoord.x + coordDelta.x) + 'px';
		if ((this.getCssPropY() || '').toLowerCase() === 'bottom')
			elem.style.bottom = (initInfo.elemCoord.y - coordDelta.y) + 'px';
		else
			elem.style.top = (initInfo.elemCoord.y + coordDelta.y) + 'px';
	},
	/** @privat */
	delaySetMoveCursor: function()
	{
		return setTimeout(this.doSetMoveCursor.bind(this), this.getMovingCursorDelay() || 0);
	},
	/** @private */
	doSetMoveCursor: function()
	{
		var gripper = this.getGripperElement();
		if (gripper && this.getMovingCursor())
			gripper.style.cursor = this.getMovingCursor();
	},
	/** @private */
	clearCursorSetter: function()
	{
		if (this._setCursorHandle)
			clearTimeout(this._setCursorHandle);
	},
	/** @private */
	saveInitialInformation: function(pointerScreenCoord)
	{
		var elem = this.getTargetElement();
		if (elem)
		{
			// save mouse initial position
			this.__$K_initialMoverInfo__.pointerCoord = pointerScreenCoord;
			var coord;
			var p = (SU.getComputedStyle(elem, 'position') || '').toLowerCase();
			// save element initial position
			var cssPropX = (this.getCssPropX() || 'left').toLowerCase();
			var cssPropY = (this.getCssPropY() || 'top').toLowerCase();
			if (p === 'relative')
			{
				var left = SU.analysisUnitsValue(SU.getComputedStyle(elem, 'left')).value || 0;
				var top = SU.analysisUnitsValue(SU.getComputedStyle(elem, 'top')).value || 0;
				coord = {'x': left, 'y': top};
				// relative element always need to be moved by t/l
				cssPropX = 'left';
				cssPropY = 'top';
			}
			else  // absolute or fixed
				coord = {'x': elem.offsetLeft, 'y': elem.offsetTop};

			var dim = EU.getElemOffsetDimension(elem);
			if (cssPropX === 'right')
				coord.x += dim.width;
			if (cssPropY === 'bottom')
				coord.y += dim.height;
			this.__$K_initialMoverInfo__.elemCoord = coord;
			if (this.getGripperElement())
			{
				//if (!this.__$K_initialMoverInfo__.cursor)  // avoid set cursor twice
				this.__$K_initialMoverInfo__.cursor = this.getGripperElement().style.cursor;
			}
		}
	},

	/////////// Event handlers //////////////

	/** @private */
	reactMouseDown: function(e)
	{
		if (this.getTargetElement() && this.getEnabled())
		{
			if (e.getButton() === EV.MouseButton.LEFT)
			{
				this.beginMoving({'x': e.getScreenX(), 'y': e.getScreenY()}, e.getTarget());
				e.preventDefault();
			}
		}
	},
	/** @private */
	reactMouseUp: function(e)
	{
		if (e.getButton() === EV.MouseButton.LEFT)
		{
			if (this.getIsMoving())
			{
				this.endMoving();
				e.preventDefault();
			}
		}
	},
	/** @private */
	reactMouseMove: function(e)
	{
		if (this.getIsMoving())
		{
			var mouseCoord = {'x': e.getScreenX(), 'y': e.getScreenY()};
			this.moveTo(mouseCoord);
			e.preventDefault();
		}
	},

	/** @private */
	reactTouchDown: function(e)
	{
		if (this.getEnabled())
		{
			this.beginMoving({'x': e.getScreenX(), 'y': e.getScreenY()}, e.getTarget());
			e.preventDefault();
		}
	},
	/** @private */
	reactTouchEnd: function(e)
	{
		if (this.getIsMoving())
		{
			this.endMoving();
			e.preventDefault();
		}
	},
	/** @private */
	reactTouchMove: function(e)
	{
		if (this.getIsMoving())
		{
			var coord = {'x': e.getScreenX(), 'y': e.getScreenY()};
			this.moveTo(coord);
			e.preventDefault();
		}
	}
});

// extend Kekule.Widget.BaseWidget, add move ability to all widgets
ClassEx.extend(Kekule.Widget.BaseWidget,
/** @lends Kekule.Widget.BaseWidget# */
{
	/** @private */
	_getMoveHelper: function(canCreate)
	{
		var result = this.getMoveHelper();
		if (!result && canCreate)
			result = new Kekule.Widget.MoveHelper(this);
		return result;
	},
	/** @private */
	movingGripperChanged: function()
	{
		var gripper = this.getMovingGripper();
		if (gripper)  // movable now
		{
			var helper = this._getMoveHelper(true);
			helper.setGripper(gripper);
		}
		else  // not movable
		{
			var helper = this._getMoveHelper(false);
			if (helper)
			{
				helper.setGripper(null);
			}
		}
	}
});

ClassEx.defineProps(Kekule.Widget.BaseWidget, [
	{
		'name': 'movable', 'dataType': DataType.BOOL,
		'getter': function()
		{
			return this._getMoveHelper() && this._getMoveHelper().getMovable();
		},
		'setter': function(value)
		{
			var old = this.getMovable();
			if (value !== old)
			{
				if (value)  // setMovable(true)
					this.setMovingGripper((this.getDefaultMovingGripper && this.getDefaultMovingGripper()) || this);
				else  // false
				{
					var helper = this._getMoveHelper();
					if (helper)
						helper.setEnabled(false);
				}
			}
		}
	},
	{
		'name': 'movingGripper', 'dataType': DataType.OBJECT,
		'setter': function(value)
		{
			if (value !== this.getMovingGripper())
			{
				this.setPropStoreFieldValue('movingGripper', value);
				this.movingGripperChanged();
			}
		}
	},
	{
		'name': 'isMoving', 'dataType': DataType.BOOL, 'serializable': false, 'scope': PS.PRIVATE,
		'setter': null,
		'getter': function()
		{
			var helper = this.getMoveHelper();
			return helper? helper.getIsMoving(): false;
		}
	},
	{'name': 'moveHelper', 'dataType': 'Kekule.Widget.MoveHelper', 'serializable': false, 'setter': null, 'scope': PS.PRIVATE}
]);


})();