/** * @class Ext.util.Draggable * @extends Ext.util.Observable * A core util class to bring Draggable behavior to any DOM element, acts as a base class for Scroller and Sortable * @constructor * @param {Mixed} el The element you want to make draggable. * @param {Object} config The configuration object for this Draggable. */ Ext.util.Draggable = Ext.extend(Ext.util.Observable, { baseCls: 'x-draggable', draggingCls: 'x-dragging', proxyCls: 'x-draggable-proxy', // @private outOfBoundRestrictFactor: 1, /** * @cfg {String} direction * Possible values: 'vertical', 'horizontal', or 'both' * Defaults to 'both' */ direction: 'both', /** * @cfg {Element/Mixed} constrain Can be either a DOM element, an instance of Ext.Element, 'parent' or null for no constrain */ constrain: window, /** * The amount of pixels you have to move before the drag operation starts. * Defaults to 0 * @type Number */ threshold: 0, /** * @cfg {Number} delay * How many milliseconds a user must hold the draggable before starting a * drag operation. Defaults to 0 or immediate. */ delay: 0, /** * @cfg {String} cancelSelector * A simple CSS selector that represents elements within the draggable * that should NOT initiate a drag. */ cancelSelector: null, /** * @cfg {Boolean} disabled * Whether or not the draggable behavior is disabled on instantiation * Defaults to false */ disabled: false, /** * @cfg {Boolean} revert * Whether or not the element or it's proxy will be reverted back (with animation) * when it's not dropped and held by a Droppable */ revert: false, /** * @cfg {String} group * Draggable and Droppable objects can participate in a group which are * capable of interacting. Defaults to 'base' */ group: 'base', /** * @cfg {Ext.Element/Element/String} eventTarget * The element to actually bind touch events to, the only string accepted is 'parent' * for convenience. * Defaults to this class' element itself */ /** * @cfg {Boolean} useCssTransform * Whether or not to translate the element using CSS Transform (much faster) instead of * left and top properties, defaults to true */ useCssTransform: true, // not implemented yet. grid: null, snap: null, proxy: null, stack: false, // Properties /** * Read-only Property representing the region that the Draggable * is constrained to. * @type Ext.util.Region */ offsetBoundary: null, /** * Read-only Property representing whether or not the Draggable is currently * dragging or not. * @type Boolean */ dragging: false, /** * Read-only value representing whether the Draggable can be moved vertically. * This is automatically calculated by Draggable by the direction configuration. * @type Boolean */ vertical: false, /** * Read-only value representing whether the Draggable can be moved horizontally. * This is automatically calculated by Draggable by the direction configuration. * @type Boolean */ horizontal: false, /** * How long animations for this draggable take by default when using setOffset with animate being true. * This defaults to 300. * @type Number */ animationDuration: 300, // @private monitorOrientation: true, constructor: function(el, config) { this.el = Ext.get(el); this.id = el.id; config = config || {}; Ext.apply(this, config); this.addEvents( /** * @event offsetchange * @param {Ext.Draggable} this * @param {Ext.util.Offset} offset */ 'offsetchange' ); Ext.util.Draggable.superclass.constructor.call(this, config); if (this.eventTarget === 'parent') { this.eventTarget = this.el.parent(); } else { this.eventTarget = (this.eventTarget) ? Ext.get(this.eventTarget) : this.el; } if (this.direction == 'both') { this.horizontal = true; this.vertical = true; } else if (this.direction == 'horizontal') { this.horizontal = true; } else { this.vertical = true; } this.el.addCls(this.baseCls); if (this.proxy) { this.getProxyEl().addCls(this.proxyCls); } this.startEventName = (this.delay > 0) ? 'taphold' : 'dragstart'; this.dragOptions = (this.delay > 0) ? {holdThreshold: this.delay} : { direction: this.direction, dragThreshold: this.threshold }; this.container = window; if (this.constrain) { if (this.constrain === 'parent') { this.container = this.el.parent(); } else if (this.constrain !== window) { this.container = Ext.get(this.constrain); } } this.offset = new Ext.util.Offset(); this.linearAnimation = { x: new Ext.util.Draggable.Animation.Linear(), y: new Ext.util.Draggable.Animation.Linear() }; this.updateBoundary(true); this.setDragging(false); if (!this.disabled) { this.enable(); } return this; }, /** * Enable the Draggable. * @return {Ext.util.Draggable} this This Draggable instance */ enable: function() { return this.setEnabled(true); }, /** * Disable the Draggable. * @return {Ext.util.Draggable} this This Draggable instance */ disable: function() { return this.setEnabled(false); }, /** * Combined method to disable or enable the Draggable. This method is called by the enable and * disable methods. * @param {Boolean} enabled True to enable, false to disable. Defaults to false. * @return {Ext.util.Draggable} this This Draggable instance */ setEnabled: function(enabled) { this.eventTarget[enabled ? 'on' : 'un'](this.startEventName, this.onStart, this, this.dragOptions); this.eventTarget[enabled ? 'on' : 'un']('drag', this.onDrag, this, this.dragOptions); this.eventTarget[enabled ? 'on' : 'un']('dragend', this.onDragEnd, this, this.dragOptions); this.eventTarget[enabled ? 'on' : 'un']('touchstart', this.onTouchStart, this); if (enabled) { Ext.EventManager.onOrientationChange(this.onOrientationChange, this); } else { Ext.EventManager.orientationEvent.removeListener(this.onOrientationChange, this); } this.disabled = !enabled; return this; }, /** * Change the Draggable to use css transforms instead of style offsets * or the other way around. * @param {Boolean} useCssTransform True to use css transforms instead of style offsets. * @return {Ext.util.Draggable} this This Draggable instance * @public */ setUseCssTransform: function(useCssTransform) { if (typeof useCssTransform == 'undefined') { useCssTransform = true; } if (useCssTransform != this.useCssTransform) { this.useCssTransform = useCssTransform; var resetOffset = new Ext.util.Offset(); if (useCssTransform == false) { this.setStyleOffset(this.offset); this.setTransformOffset(resetOffset); } else { this.setTransformOffset(this.offset); this.setStyleOffset(resetOffset); } } return this; }, /** * Sets the offset of this Draggable relatively to its original offset. * @param {Ext.util.Offset/Object} offset An object or Ext.util.Offset instance containing the * x and y coordinates. * @param {Boolean/Number} animate Whether or not to animate the setting of the offset. True * to use the default animationDuration, a number to specify the duration for this operation. * @return {Ext.util.Draggable} this This Draggable instance */ setOffset: function(offset, animate) { if (!this.horizontal) { offset.x = 0; } if (!this.vertical) { offset.y = 0; } if (animate) { this.startAnimation(offset, animate); } else { this.offset = offset; this.region = new Ext.util.Region( this.initialRegion.top + offset.y, this.initialRegion.right + offset.x, this.initialRegion.bottom + offset.y, this.initialRegion.left + offset.x ); if (this.useCssTransform) { this.setTransformOffset(offset); } else { this.setStyleOffset(offset); } this.fireEvent('offsetchange', this, this.offset); } return this; }, /** * Internal method that sets the transform of the proxyEl. * @param {Ext.util.Offset/Object} offset An object or Ext.util.Offset instance containing the * x and y coordinates for the transform. * @return {Ext.util.Draggable} this This Draggable instance * @private */ setTransformOffset: function(offset) { // Ext.Element.cssTransform(this.getProxyEl(), {translate: [offset.x, offset.y]}); // Temporarily use this instead of Ext.Element.cssTransform to save some CPU Ext.Element.cssTranslate(this.getProxyEl(), offset); return this; }, /** * Internal method that sets the left and top of the proxyEl. * @param {Ext.util.Offset/Object} offset An object or Ext.util.Offset instance containing the * x and y coordinates. * @return {Ext.util.Draggable} this This Draggable instance * @private */ setStyleOffset: function(offset) { this.getProxyEl().setLeft(offset.x); this.getProxyEl().setTop(offset.y); return this; }, /** * Internal method that sets the offset of the Draggable using an animation * @param {Ext.util.Offset/Object} offset An object or Ext.util.Offset instance containing the * x and y coordinates for the transform. * @param {Boolean/Number} animate Whether or not to animate the setting of the offset. True * to use the default animationDuration, a number to specify the duration for this operation. * @return {Ext.util.Draggable} this This Draggable instance * @private */ startAnimation: function(offset, animate) { this.stopAnimation(); var currentTime = Date.now(); animate = Ext.isNumber(animate) ? animate : this.animationDuration; this.linearAnimation.x.set({ startOffset: this.offset.x, endOffset: offset.x, startTime: currentTime, duration: animate }); this.linearAnimation.y.set({ startOffset: this.offset.y, endOffset: offset.y, startTime: currentTime, duration: animate }); this.isAnimating = true; this.animationTimer = Ext.defer(this.handleAnimationFrame, 0, this); return this; }, /** * Internal method that stops the current offset animation * @private */ stopAnimation: function() { if (this.isAnimating) { clearTimeout(this.animationTimer); this.isAnimating = false; this.setDragging(false); } return this; }, /** * Internal method that handles a frame of the offset animation. * @private */ handleAnimationFrame: function() { if (!this.isAnimating) { return; } var newOffset = new Ext.util.Offset(); newOffset.x = this.linearAnimation.x.getOffset(); newOffset.y = this.linearAnimation.y.getOffset(); this.setOffset(newOffset); this.animationTimer = Ext.defer(this.handleAnimationFrame, 10, this); if ((newOffset.x === this.linearAnimation.x.endOffset) && (newOffset.y === this.linearAnimation.y.endOffset)) { this.stopAnimation(); } }, /** * Returns the current offset relative to the original location of this Draggable. * @return {Ext.util.Offset} offset An Ext.util.Offset instance containing the offset. */ getOffset: function() { var offset = this.offset.copy(); offset.y = -offset.y; offset.x = -offset.x; return offset; }, /** * Updates the boundary information for this Draggable. This method shouldn't * have to be called by the developer and is mostly used for internal purposes. * Might be useful when creating subclasses of Draggable. * @param {Boolean} init Whether or not this is happing during instantiation, which we need * to apply the transform / style to the DOM element * @return {Ext.util.Draggable} this This Draggable instance * @private */ updateBoundary: function(init) { var offsetBoundary; if (typeof init == 'undefined') init = false; this.size = { width: this.el.dom.scrollWidth, height: this.el.dom.scrollHeight }; if (this.container === window) { this.containerBox = { left: 0, top: 0, right: this.container.innerWidth, bottom: this.container.innerHeight, width: this.container.innerWidth, height: this.container.innerHeight }; } else { this.containerBox = this.container.getPageBox(); } var elXY = this.el.getXY(); this.elBox = { left: elXY[0] - this.offset.x, top: elXY[1] - this.offset.y, width: this.size.width, height: this.size.height }; this.elBox.bottom = this.elBox.top + this.elBox.height; this.elBox.right = this.elBox.left + this.elBox.width; this.initialRegion = this.region = new Ext.util.Region( elXY[1], elXY[0] + this.elBox.width, elXY[1] + this.elBox.height, elXY[0] ); var top = 0, right = 0, bottom = 0, left = 0; if (this.elBox.left < this.containerBox.left) { right += this.containerBox.left - this.elBox.left; } else { left -= this.elBox.left - this.containerBox.left; } if (this.elBox.right > this.containerBox.right) { left -= this.elBox.right - this.containerBox.right; } else { right += this.containerBox.right - this.elBox.right; } if (this.elBox.top < this.containerBox.top) { bottom += this.containerBox.top - this.elBox.top; } else { top -= this.elBox.top - this.containerBox.top; } if (this.elBox.bottom > this.containerBox.bottom) { top -= this.elBox.bottom - this.containerBox.bottom; } else { bottom += this.containerBox.bottom - this.elBox.bottom; } offsetBoundary = new Ext.util.Region(top, right, bottom, left).round(); if (this.offsetBoundary && this.offsetBoundary.equals(offsetBoundary)) { return this; } this.offsetBoundary = offsetBoundary; var currentComputedOffset; if (this.useCssTransform) { currentComputedOffset = Ext.Element.getComputedTransformOffset(this.getProxyEl()); } else { currentComputedOffset = new Ext.util.Offset(this.getProxyEl().getLeft(), this.getProxyEl().getTop()); } if(!this.offset.equals(currentComputedOffset) || init) { this.setOffset(currentComputedOffset); } return this; }, // @private onTouchStart: function() { }, /** * Fires when the Drag operation starts. Internal use only. * @param {Event} e The event object for the drag operation * @private */ onStart: function(e) { this.updateBoundary(); this.stopAnimation(); if (this.dragging) { this.onDragEnd(e); } this.setDragging(true); this.startTouchPoint = Ext.util.Point.fromEvent(e); this.startOffset = this.offset.copy(); this.fireEvent('dragstart', this, e); return true; }, /** * Gets the new offset from a touch offset. * @param {Ext.util.Offset} touchPoint The touch offset instance. * @private */ getNewOffsetFromTouchPoint: function(touchPoint) { var xDelta = touchPoint.x - this.startTouchPoint.x, yDelta = touchPoint.y - this.startTouchPoint.y, newOffset = this.offset.copy(); if(xDelta == 0 && yDelta == 0) { return newOffset; } if (this.horizontal) newOffset.x = this.startOffset.x + xDelta; if (this.vertical) newOffset.y = this.startOffset.y + yDelta; return newOffset; }, /** * Fires when a drag events happens. Internal use only. * @param {Event} e The event object for the drag event * @private */ onDrag: function(e) { if (!this.dragging) { return; } this.lastTouchPoint = Ext.util.Point.fromEvent(e); var newOffset = this.getNewOffsetFromTouchPoint(this.lastTouchPoint); if (this.offsetBoundary != null) { newOffset = this.offsetBoundary.restrict(newOffset, this.outOfBoundRestrictFactor); } this.setOffset(newOffset); this.fireEvent('drag', this, e); // This 'return true' here is to let sub-classes determine whether // there's an interuption return before that return true; }, /** * Fires when a dragend event happens. Internal use only. * @param {Event} e The event object for the dragend event * @private */ onDragEnd: function(e) { if (this.dragging) { this.fireEvent('beforedragend', this, e); if (this.revert && !this.cancelRevert) { this.setOffset(this.startOffset, true); } else { this.setDragging(false); } this.fireEvent('dragend', this, e); } // This 'return true' here is to let sub-classes determine whether // there's an interuption return before that return true; }, /** * Fires when the orientation changes. Internal use only. * @private */ onOrientationChange: function() { this.updateBoundary(); }, /** * Sets the dragging flag and adds a dragging class to the element. * @param {Boolean} dragging True to enable dragging, false to disable. * @private */ setDragging: function(dragging) { if (dragging) { if (!this.dragging) { this.dragging = true; this.getProxyEl().addCls(this.draggingCls); } } else { if (this.dragging) { this.dragging = false; this.getProxyEl().removeCls(this.draggingCls); } } return this; }, /** * Returns the element thats is being visually dragged. * @returns {Ext.Element} proxy The proxy element. */ getProxyEl: function() { return this.proxy || this.el; }, /** * Destroys this Draggable instance. */ destroy: function() { this.el.removeCls(this.baseCls); this.getProxyEl().removeCls(this.proxyCls); this.clearListeners(); this.disable(); }, /** * This method will reset the initial region of the Draggable. * @private */ reset: function() { this.startOffset = new Ext.util.Offset(0, 0); this.setOffset(this.startOffset); var oldInitialRegion = this.initialRegion.copy(); this.updateBoundary(); this.initialRegion = this.region = this.getProxyEl().getPageBox(true); this.startTouchPoint.x += this.initialRegion.left - oldInitialRegion.left; this.startTouchPoint.y += this.initialRegion.top - oldInitialRegion.top; }, /** * Use this to move the draggable to a coordinate on the screen. * @param {Number} x the vertical coordinate in pixels * @param {Number} y the horizontal coordinate in pixels * @return {Ext.util.Draggable} this This Draggable instance */ moveTo: function(x, y) { this.setOffset(new Ext.util.Offset(x - this.initialRegion.left, y - this.initialRegion.top)); return this; }, /** * Method to determine whether this Draggable is currently dragging. * @return {Boolean} */ isDragging: function() { return this.dragging; }, /** * Method to determine whether this Draggable can be dragged on the y-axis * @return {Boolean} */ isVertical : function() { return this.vertical; }, /** * Method to determine whether this Draggable can be dragged on the x-axis * @return {Boolean} */ isHorizontal : function() { return this.horizontal; } }); Ext.util.Draggable.Animation = {}; /** * @class Ext.util.Draggable.Animation.Abstract * @extends Object * * Provides the abstract methods for a Draggable animation. * @private * @ignore */ Ext.util.Draggable.Animation.Abstract = Ext.extend(Object, { /** * @cfg {Number} startTime The time the Animation started * @private */ startTime: null, /** * @cfg {Object/Ext.util.Offset} startOffset Object containing the x and y coordinates the * Draggable had when the Animation started. * @private */ startOffset: 0, /** * The constructor for an Abstract animation. Applies the config to the Animation. * @param {Object} config Object containing the configuration for this Animation. * @private */ constructor: function(config) { config = config || {}; this.set(config); if (!this.startTime) this.startTime = Date.now(); }, /** * Sets a config value for this Animation. * @param {String} name The name of this configuration * @param {Mixed} value The value for this configuration */ set: function(name, value) { if (Ext.isObject(name)) { Ext.apply(this, name); } else { this[name] = value; } return this; }, /** * This method will return the offset of the coordinate that is being animated for any * given offset in time based on a different set of variables. Usually these variables are * a combination of the startOffset, endOffset, startTime and duration. * @return {Number} The offset for the coordinate that is being animated */ getOffset: Ext.emptyFn }); /** * @class Ext.util.Draggable.Animation.Linear * @extends Ext.util.Draggable.Animation.Abstract * * A linear animation that is being used by Draggable's setOffset by default. * @private * @ignore */ Ext.util.Draggable.Animation.Linear = Ext.extend(Ext.util.Draggable.Animation.Abstract, { /** * @cfg {Number} duration The duration of this animation in milliseconds. */ duration: 0, /** * @cfg {Object/Ext.util.Offset} endOffset Object containing the x and y coordinates the * Draggable is animating to. * @private */ endOffset: 0, getOffset : function() { var distance = this.endOffset - this.startOffset, deltaTime = Date.now() - this.startTime, omegaTime = Math.min(1, (deltaTime / this.duration)); return this.startOffset + (omegaTime * distance); } });