/**
* 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']; // eslint-disable-line dot-notation
},
/**
* @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) {
var el;
if (this.dragging) {
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();
/* eslint-disable-next-line max-len */
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 handle = this.getHandle(),
o = {
touchstart: 'handleTouchStart',
dragstart: 'handleDragStart',
drag: 'handleDragMove',
dragend: 'handleDragEnd',
dragcancel: 'handleDragCancel'
};
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;
}
/* eslint-disable-next-line vars-on-top */
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();
}
}
});