/** * A wrapper around a DOM element that allows it to be dragged. * * ## Constraining * * The {@link #constrain} config gives various options for limiting drag, for example: * - Vertical or horizontal only * - Minimum/maximum x/y values. * - Snap to grid * - Constrain to an element or region. * * See {@link Ext.drag.Constraint} for detailed options. * * * new Ext.drag.Source({ * element: dragEl, * constrain: { * // Drag only vertically in 30px increments * vertical: true, * snap: { * y: 30 * } * } * }); * * ## Data * * Data representing the underlying drag is driven by the {@link #method!describe} method. This method * is called once at the beginning of the drag. It should populate the info object with data using * the {@link Ext.drag.Info#setData setData} method. It accepts 2 arguments. * * - The `type` is used to indicate to {@link Ext.drag.Target targets} the type(s) of data being provided. * This allows the {@link Ext.drag.Target target} to decide whether it is able to interact with the source. * All types added are available in {@link Ext.drag.Info#types types}. * - The value can be a static value, or a function reference. In the latter case, the function is evaluated * when the data is requested. * * The {@link Ext.drag.Info#getData} method may be called once the drop completes. The data for the relevant type * is retrieved. All values from this method return a {@link Ext.Promise} to allow for consistency when dealing * with synchronous and asynchronous data. * * ## Proxy * * A {@link #proxy} is an element that follows the mouse cursor during a drag. This may be the {@link #element}, * a newly created element, or none at all (if the purpose is to just track the cursor). * * See {@link Ext.drag.proxy.None for details}. * * var data = [{ * id: 1, * name: 'Adam' * }, { * id: 2, * name: 'Barbara' * }, { * id: 3, * name: 'Charlie' * }]; * * var tpl = new Ext.XTemplate( * '<div class="container">', * '<tpl for=".">', * '<div class="child" data-id="{id}">{name}</div>', * '</tpl>', * '</div>' * ); * * var el = tpl.append(Ext.getBody(), data); * * new Ext.drag.Source({ * element: el, * handle: '.child', * proxy: { * type: 'placeholder', * getElement: function(info) { * return Ext.getBody().createChild({ * cls: 'foo', * html: info.eventTarget.innerHTML * }); * } * } * }); * * * ## Handle * * A {@link #handle} is a CSS selector that allows certain child elements of the {@link #element} * to begin a drag. This is useful in 2 case: * - Where only a certain part of the element should trigger a drag, but the whole element should move. * - When there are several repeated elements that may represent objects. * * In the example below, each child element becomes draggable and * the describe method is used to extract the id from the DOM element. * * * var data = [{ * id: 1, * name: 'Adam' * }, { * id: 2, * name: 'Barbara' * }, { * id: 3, * name: 'Charlie' * }]; * * var tpl = new Ext.XTemplate( * '<div class="container">', * '<tpl for=".">', * '<div class="child" data-id="{id}">{name}</div>', * '</tpl>', * '</div>' * ); * * var el = tpl.append(Ext.getBody(), data); * * new Ext.drag.Source({ * element: el, * handle: '.child', * describe: function(info) { * info.setData('item', Ext.fly(info.eventTarget).getAttribute('data-id')); * } * }); * */Ext.define('Ext.drag.Source', { extend: 'Ext.drag.Item', defaultIdPrefix: 'source-', requires: [ 'Ext.GlobalEvents', 'Ext.drag.Constraint' ], config: { /** * @cfg {Boolean/String/String[]} activeOnLongPress * `true` to always begin a drag with longpress. `false` to * never drag with longpress. If a string (or strings) are passed, it should * correspond to the pointer event type that should initiate a a drag on * longpress. See {@link Ext.event.Event#pointerType} for available types. */ activateOnLongPress: false, /** * @cfg {String} activeCls * A css class to add to the {@link #element} while dragging is * active. */ activeCls: null, /** * @cfg {Object/Ext.util.Region/Ext.dom.Element} constrain * * Adds constraining behavior for this drag source. See {@link Ext.drag.Constraint} for * configuration options. As a shortcut, a {@link Ext.util.Region Region} * or {@link Ext.dom.Element} may be passed, which will be mapped to the * appropriate configuration on the constraint. */ constrain: null, /** * @cfg {String} handle * A CSS selector to identify child elements of the {@link #element} that will cause * a drag to be activated. If this is not specified, the entire {@link #element} will * be draggable. */ handle: null, local: null, // @cmd-auto-dependency {aliasPrefix: "drag.proxy."} /** * @cfg {String/Object/Ext.drag.proxy.Base} proxy * The proxy to show while this element is dragging. This may be * the alias, a config, or instance of a proxy. * * See {@link Ext.drag.proxy.None None}, {@link Ext.drag.proxy.Original Original}, * {@link Ext.drag.proxy.Placeholder Placeholder}. */ proxy: 'original', /** * @cfg {Boolean/Object} revert * `true` (or an animation configuration) to animate the {@link #proxy} (which may be * the {@link #element}) back to the original position after drag. */ revert: false }, /** * @cfg {Function} describe * See {@link #method-describe}. */ /** * @event beforedragstart * Fires before drag starts on this source. Return `false` to cancel the drag. * * @param {Ext.drag.Source} this This source. * @param {Ext.drag.Info} info The drag info. * @param {Ext.event.Event} event The event. */ /** * @event dragstart * Fires when the drag starts on this source. * * @param {Ext.drag.Source} this This source. * @param {Ext.drag.Info} info The drag info. * @param {Ext.event.Event} event The event. */ /** * @event dragmove * Fires continuously as this source is dragged. * * @param {Ext.drag.Source} this This source. * @param {Ext.drag.Info} info The drag info. * @param {Ext.event.Event} event The event. */ /** * @event dragend * Fires when the drag ends on this source. * * @param {Ext.drag.Source} this This source. * @param {Ext.drag.Info} info The drag info. * @param {Ext.event.Event} event The event. */ /** * @event dragcancel * Fires when a drag is cancelled. * * @param {Ext.drag.Source} this This source. * @param {Ext.drag.Info} info The drag info. * @param {Ext.event.Event} event The event. */ /** * @property {Boolean} dragging * `true` if this source is currently dragging. * * @protected */ dragging: false, constructor: function(config) { var describe = config && config.describe; if (describe) { this.describe = describe; // Don't mutate the object the user passed. Need to do this // here otherwise initConfig will complain about writing over // the method. config = Ext.apply({}, config); delete config.describe; } this.callParent([config]); // Use bracket syntax to prevent Cmd from creating an // auto dependency. Will be pulled in by the target if // required. this.manager = Ext.drag['Manager']; }, /** * @method * Sets up the underlying data that describes the drag. This method * is called once at the start of the drag operation. * * Data should be set on the {@link Ext.drag.Info info} using the * {@link Ext.drag.Info#setData setData} method. See * {@link Ext.drag.Info#setData setData} for more information. * * This method should not be called by user code. * * @param {Ext.drag.Info} info The drag info. * * @protected */ describe: Ext.emptyFn, /** * Checks whether this source is actively dragging. * @return {Boolean} `true` if this source is dragging. */ isDragging: function() { return this.dragging; }, /** * @method * Called before a drag starts. Return `false` to veto the drag. * @param {Ext.drag.Info} The drag info. * * @return {Boolean} `false` to veto the drag. * * @protected * @template */ beforeDragStart: Ext.emptyFn, /** * @method * Called when a drag is cancelled. * * @protected * @template */ onDragCancel: Ext.emptyFn, /** * @method * Called when a drag ends. * * @protected * @template */ onDragEnd: Ext.emptyFn, /** * @method * Called for each move in a drag. * * @protected * @template */ onDragMove: Ext.emptyFn, /** * @method * Called when a drag starts. * * @protected * @template */ onDragStart: Ext.emptyFn, applyActivateOnLongPress: function(activateOnLongPress) { if (typeof activateOnLongPress === 'string') { activateOnLongPress = [activateOnLongPress]; } return activateOnLongPress; }, updateActivateOnLongPress: function(activateOnLongPress) { if (!this.isConfiguring) { this.setupListeners(); } }, updateActiveCls: function(cls, oldCls) { if (this.dragging) { var el = this.getElement(); el.replaceCls(oldCls, cls); } }, applyConstrain: function(constrain) { if (constrain && !constrain.$isClass) { if (constrain.isRegion) { constrain = { region: constrain }; } else if (constrain.isElement || !Ext.isObject(constrain)) { constrain = { element: constrain }; } constrain = Ext.apply({ source: this }, constrain); constrain = Ext.Factory.dragConstraint(constrain); } return constrain; }, updateElement: function(element, oldElement) { // We can't bind/unbind these listeners with getElListeners because // they will conflict with the dragstart gesture event if (oldElement && !oldElement.destroyed) { oldElement.un('dragstart', 'stopNativeDrag', this); } if (element && !this.getHandle()) { element.setTouchAction({ panX: false, panY: false }); // Suppress translation and delegation for this to avoid event firing on // synthetic dragstart published by Gesture from pointermove. We need the // native event here. element.on('dragstart', 'stopNativeDrag', this, {translate: false, delegated: false}); } this.callParent([ element, oldElement ]); }, updateHandle: function() { if (!this.isConfiguring) { this.setupListeners(); } }, applyProxy: function(proxy) { if (proxy) { proxy = Ext.Factory.dragproxy(proxy); } return proxy; }, updateProxy: function(proxy, oldProxy) { if (oldProxy) { oldProxy.destroy(); } if (proxy) { proxy.setSource(this); } }, resolveListenerScope: function () { var ownerCmp = this.ownerCmp, a = arguments; if (ownerCmp) { return ownerCmp.resolveListenerScope.apply(ownerCmp, a); } return this.callParent(a); }, destroy: function() { var me = this; me.manager = me.initialEvent = null; me.setConstrain(null); me.setProxy(null); me.callParent(); }, privates: { /** * @property {String} draggingCls * A class to add while dragging to give a high z-index and * disable pointer events. * * @private */ draggingCls: Ext.baseCSSPrefix + 'drag-dragging', /** * @property {Ext.drag.Info} info * The info. Only available while a drag is active. * * @private */ info: null, /** * @property {String} revertCls * A class to add to the proxy element while a revert is active. * * @private */ revertCls: Ext.baseCSSPrefix + 'drag-revert', canActivateOnLongPress: function(e) { var activate = this.getActivateOnLongPress(); return !!(activate && (activate === true || Ext.Array.contains(activate, e.pointerType))); }, /** * Perform any cleanup after a drag. * * @private */ dragCleanup: function(info) { var me = this, cls = me.getActiveCls(), proxy = me.getProxy(), el = me.getElement(), proxyEl = info ? info.proxy.element : null; if (cls) { el.removeCls(cls); } if (proxyEl) { proxyEl.removeCls(me.draggingCls); } proxy.cleanup(info); me.dragging = false; me.initialEvent = me.info = null; }, /** * @method getElListeners * @inheritdoc */ getElListeners: function() { var o = { touchstart: 'handleTouchStart', dragstart: 'handleDragStart', drag: 'handleDragMove', dragend: 'handleDragEnd', dragcancel: 'handleDragCancel' }, handle = this.getHandle(); if (handle) { o.dragstart = { fn: o.dragstart, delegate: handle }; } if (this.getActivateOnLongPress()) { o.longpress = 'handleLongPress'; } return o; }, /** * Called when a drag is cancelled. * @param {Ext.event.Event} e The event. * * @private */ handleDragCancel: function(e) { var me = this, info = me.info, manager = me.manager; if (manager) { manager.onDragCancel(info, e); } me.onDragCancel(info); if (me.hasListeners.dragcancel) { me.fireEvent('dragcancel', me, info, e); } Ext.fireEvent('dragcancel', me, info, e); me.dragCleanup(info); }, /** * Called when a drag is ended. * @param {Ext.event.Event} e The event. * * @private */ handleDragEnd: function(e) { if (!this.dragging) { return; } var me = this, manager = me.manager, revert = me.getRevert(), info = me.info, proxy = info.proxy; info.update(e); if (manager) { manager.onDragEnd(info, e); } me.onDragEnd(info); if (me.hasListeners.dragend) { me.fireEvent('dragend', me, info, e); } Ext.fireEvent('dragend', me, info, e); proxy = proxy.instance; if (revert && proxy) { proxy.dragRevert(info, me.revertCls, revert, function () { me.dragCleanup(info); }); } else { me.dragCleanup(info); } }, /** * Called for each drag movement. * @param {Ext.event.Event} e The event. * * @private */ handleDragMove: function(e) { var me = this, info = me.info, manager = me.manager; if (!me.dragging) { return; } e.stopPropagation(); e.claimGesture(); info.update(e); if (manager) { manager.onDragMove(info, e); } me.onDragMove(info); if (me.hasListeners.dragmove) { me.fireEvent('dragmove', me, info, e); } }, /** * Called when a drag is started. * @param {Ext.event.Event} e The event. * * @private */ handleDragStart: function(e) { var me = this, hasListeners = me.hasListeners, manager = me.manager, constrain = me.getConstrain(), initialEvent = me.initialEvent, el, cls, info, cancel, proxyEl; if (me.preventStart(e)) { return false; } if (hasListeners.initdragconstraints) { // This (private) event allows drag constraints to be adjusted "JIT" // (used by modern sliders) me.fireEvent('initdragconstraints', me, e); } me.info = info = new Ext.drag.Info(me, initialEvent); me.setup(info); if (constrain) { constrain.onDragStart(info); } info.update(e, true); cancel = me.beforeDragStart(info) === false; if (!cancel && hasListeners.beforedragstart) { cancel = me.fireEvent('beforedragstart', me, info, e) === false; } if (cancel) { me.dragCleanup(); return false; } e.claimGesture(); me.dragging = true; cls = me.getActiveCls(); el = me.getElement(); if (cls) { el.addCls(cls); } proxyEl = info.proxy.element; if (proxyEl) { proxyEl.addCls(me.draggingCls); } info.update(e); if (manager) { manager.onDragStart(info, e); } me.onDragStart(info); if (hasListeners.dragstart) { me.fireEvent('dragstart', me, info, e); } Ext.fireEvent('dragstart', me, info, e); }, /** * Called when a longpress is started on this target (which may lead to a drag) * @param {Ext.event.Event} e The event. * * @private */ handleLongPress: function(e) { if (!this.isDisabled() && this.canActivateOnLongPress(e)) { this.initialEvent = e; e.startDrag(); } }, /** * Called when a touch starts on this target (which may lead to a drag). * @param {Ext.event.Event} e The event. * * @private */ handleTouchStart: function(e) { if (!this.isDisabled()) { this.initialEvent = e; } }, preventStart: function(e) { return this.isDisabled() || (!e.longpress && this.canActivateOnLongPress(e)); }, /** * Allow for any setup as soon as the info object is created. * * @private */ setup: Ext.privateFn, stopNativeDrag: function(e) { e.preventDefault(); } }});