/** * A core util class to bring Draggable behavior to a Component. This class is specifically designed only for * absolutely positioned elements starting from top: 0, left: 0. The initialOffset can then be set via configuration * to have the elements in a different position. */Ext.define('Ext.util.Draggable', { isDraggable: true, mixins: [ 'Ext.mixin.Observable' ], requires: [ 'Ext.util.Translatable' ], /** * @event dragstart * @preventable * Fires whenever the component starts to be dragged * @param {Ext.util.Draggable} this * @param {Ext.event.Event} e the event object * @param {Number} offsetX The current offset value on the x axis * @param {Number} offsetY The current offset value on the y axis */ /** * @event drag * Fires whenever the component is dragged * @param {Ext.util.Draggable} this * @param {Ext.event.Event} e the event object * @param {Number} offsetX The new offset value on the x axis * @param {Number} offsetY The new offset value on the y axis */ /** * @event dragend * Fires whenever the component is dragged * @param {Ext.util.Draggable} this * @param {Ext.event.Event} e the event object * @param {Number} offsetX The current offset value on the x axis * @param {Number} offsetY The current offset value on the y axis */ config: { cls: Ext.baseCSSPrefix + 'draggable', draggingCls: Ext.baseCSSPrefix + 'dragging', element: null, constraint: 'container', disabled: null, /** * @cfg {String} direction * Possible values: 'vertical', 'horizontal', or 'both' * @accessor */ direction: 'both', /** * @cfg {Object/Number} initialOffset * The initial draggable offset. When specified as Number, * both x and y will be set to that value. */ initialOffset: { x: 0, y: 0 }, translatable: {} }, DIRECTION_BOTH: 'both', DIRECTION_VERTICAL: 'vertical', DIRECTION_HORIZONTAL: 'horizontal', defaultConstraint: { min: { x: -Infinity, y: -Infinity }, max: { x: Infinity, y: Infinity } }, containerWidth: 0, containerHeight: 0, width: 0, height: 0, /** * Creates new Draggable. * @param {Object} config The configuration object for this Draggable. */ constructor: function(config) { var element; this.extraConstraint = {}; this.initialConfig = config; this.offset = { x: 0, y: 0 }; this.elementListeners = { dragstart: 'onDragStart', drag : 'onDrag', dragend : 'onDragEnd', resize : 'onElementResize', touchstart : 'onPress', touchend : 'onRelease', // high priority ensures that these listeners run before user listeners // so that draggable state is correct in user handlers priority: 2000, scope: this }; if (config && config.element) { element = config.element; delete config.element; this.setElement(element); } return this; }, applyElement: function(element) { if (!element) { return; } return Ext.get(element); }, updateElement: function(element) { element.on(this.elementListeners); this.mixins.observable.constructor.call(this, this.initialConfig); }, updateInitialOffset: function(initialOffset) { if (typeof initialOffset == 'number') { initialOffset = { x: initialOffset, y: initialOffset }; } var offset = this.offset, x, y; offset.x = x = initialOffset.x; offset.y = y = initialOffset.y; this.getTranslatable().translate(x, y); }, updateCls: function(cls) { this.getElement().addCls(cls); }, applyTranslatable: function(translatable, currentInstance) { translatable = Ext.factory(translatable, Ext.util.Translatable, currentInstance); if (translatable) { translatable.setElement(this.getElement()); } return translatable; }, setExtraConstraint: function(constraint) { this.extraConstraint = constraint || {}; this.refreshConstraint(); return this; }, addExtraConstraint: function(constraint) { Ext.merge(this.extraConstraint, constraint); this.refreshConstraint(); return this; }, applyConstraint: function(newConstraint) { this.currentConstraint = newConstraint; if (!newConstraint) { newConstraint = this.defaultConstraint; } if (newConstraint === 'container') { return Ext.merge(this.getContainerConstraint(), this.extraConstraint); } return Ext.merge({}, this.extraConstraint, newConstraint); }, updateConstraint: function() { this.refreshOffset(); }, getContainerConstraint: function() { var container = this.getContainer(), element = this.getElement(), borders; if (!container || !element.dom) { return this.defaultConstraint; } borders = container.getBorders(); return { min: { x: 0, y: 0 }, max: { x: this.containerWidth - this.width - borders.beforeX - borders.afterX, y: this.containerHeight - this.height - borders.beforeY - borders.afterY } }; }, getContainer: function() { var container = this.container; if (!container) { container = this.getElement().getParent(); if (container) { this.container = container; container.on({ resize: 'onContainerResize', destroy: 'onContainerDestroy', scope: this, // The resize listener must have a high priority, so that the draggable // instance is refreshed prior to other parties who may be listening // for resize on the same element. For example, slider listens to // resize on its element and expects that the draggable thumbs have // already had their draggable instances refreshed. priority: 2000 }); } } return container; }, onElementResize: function(element, info) { this.width = info.width; this.height = info.height; this.refresh(); }, onContainerResize: function(container, info) { this.containerWidth = info.contentWidth; this.containerHeight = info.contentHeight; this.refresh(); }, refreshContainerSize: function() { // refreshes container size from dom. Useful when the draggable element did not // have a parentNode at the time the draggable was initialized. Invoke this // as soon as the element is appended to its parent to ensure correct constraining var me = this, container = me.getContainer(); me.containerWidth = container.getWidth(); me.containerHeight = container.getHeight(); this.refresh(); return me; }, onContainerDestroy: function() { delete this.container; delete this.containerSizeMonitor; }, detachListeners: function() { this.getElement().un(this.elementListeners); }, isAxisEnabled: function(axis) { var direction = this.getDirection(); if (axis === 'x') { return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_HORIZONTAL); } return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_VERTICAL); }, onPress: function(e) { this.fireEvent('touchstart', this, e); }, onRelease: function(e) { this.fireEvent('touchend', this, e); }, onDragStart: function(e) { var me = this, offset = me.offset; if (me.getDisabled()) { return false; } me.fireEventedAction('dragstart', [me, e, offset.x, offset.y], me.initDragStart, me); }, initDragStart: function(me, e, offsetX, offsetY) { this.dragStartOffset = { x: offsetX, y: offsetY }; this.isDragging = true; this.getElement().addCls(this.getDraggingCls()); }, onDrag: function(e) { if (!this.isDragging) { return; } var startOffset = this.dragStartOffset; this.fireAction('drag', [this, e, startOffset.x + e.deltaX, startOffset.y + e.deltaY], this.doDrag); }, doDrag: function(me, e, offsetX, offsetY) { me.setOffset(offsetX, offsetY); }, onDragEnd: function(e) { if (!this.isDragging) { return; } this.onDrag(e); this.isDragging = false; this.getElement().removeCls(this.getDraggingCls()); this.fireEvent('dragend', this, e, this.offset.x, this.offset.y); }, setOffset: function(x, y, animation) { var currentOffset = this.offset, constraint = this.getConstraint(), minOffset = constraint.min, maxOffset = constraint.max, min = Math.min, max = Math.max; if (this.isAxisEnabled('x') && typeof x == 'number') { x = min(max(x, minOffset.x), maxOffset.x); } else { x = currentOffset.x; } if (this.isAxisEnabled('y') && typeof y == 'number') { y = min(max(y, minOffset.y), maxOffset.y); } else { y = currentOffset.y; } currentOffset.x = x; currentOffset.y = y; this.getTranslatable().translate(x, y, animation); }, getOffset: function() { return this.offset; }, refreshConstraint: function() { this.setConstraint(this.currentConstraint); }, refreshOffset: function() { var offset = this.offset; this.setOffset(offset.x, offset.y); }, refresh: function() { this.refreshConstraint(); this.getTranslatable().refresh(); this.refreshOffset(); }, /** * Enable the Draggable. * @return {Ext.util.Draggable} This Draggable instance */ enable: function() { return this.setDisabled(false); }, /** * Disable the Draggable. * @return {Ext.util.Draggable} This Draggable instance */ disable: function() { return this.setDisabled(true); }, destroy: function() { var me = this, translatable = me.getTranslatable(); var element = me.getElement(); if (element && !element.destroyed) { element.removeCls(me.getCls()); } me.detachListeners(); if (translatable) { translatable.destroy(); } me.callParent(); }});