Modern Drag and Drop
Introduction
Ext JS 6.2 introduced the Ext.drag API to provide a cross-toolkit solution to drag-and-drop for the Calendar package. This API provides a collection of classes that allow applications to easily add custom Drag/Drop functionality ranging from basic element manipulation to complex, aysnchronous data transfer. The Ext.drag API is modeled after and greatly expands on the HTML5 drag/drop API and is available for both Classic and Modern Toolkits.
Overview
Ext.drag keeps the element-level API simple with the appropriate hooks to combine with your components. It also allows data related actions to occur asynchronously.
Ext.drag is composed of two main classes: Ext.drag.Source and Ext.drag.Target. A source is something that can be dragged. A target is something that can receive a drop from a source. Both of these classes are attached to an Ext.dom.Element.
This namespace also includes functionality that handles element-level interactions. For components, it is often useful to wrap these classes to provide a more component friendly interface.
Ext.drag.Source
A drag source represents a movable element that can be dragged on screen. Below are some of the main features:
Constraining
Constraints limit the possible positions a drag can occur. Constraints for sources are handled by the Ext.drag.Constraint class. A configuration for this (along with some shortcuts) can be passed to the drag source. Some of the useful options include:
Limiting the drag to be only horizontal or vertical
Limiting the drag to a particular onscreen region (Ext.util.Region)
Limiting the drag to a parent element
Limiting the position of the drag by snapping to a grid in increments.
You can see an example of constraints being applied in our Kitchen Sink Example
Drag Handle
By default, a drag gesture on any portion of the source element will initiate a drag on the source. A handle allows specific portion/s of the element to be specified by using a css selector. This is useful in 2 main scenarios:
The source should only be dragged in a certain area, for example, the title bar of a window.
The source has many repeated elements that should trigger a drag with unique data, for example a dataview.
You can see an example of constraints being applied in our Kitchen Sink Example
Proxies
A drag proxy is a visual representation show on screen while a drag is in progress. The proxy element (if specified) follows the mouse cursor. There are currently 3 implementations provided in this namespace:
Original (the default) - The element of the source is moved.
Placeholder - A new element is created and the source element is left in place.
None - No proxy element is shown. This is typically used in conjunction with Source events to display drag information.
Events/Template Methods
The following events and template methods on the source are available at the specified points in the drag cycle:
beforedragstart/onBeforeDragStart
, can be cancelled by returning false.dragstart
/onDragStart
dragmove
/onDragMove
dragend
/onDragEnd
dragcancel
/onDragCancel
Expand Code
JS Run
var logger = Ext.getBody().createChild({
tag: 'textarea',
rows: 15,
cols: 80
}).dom;
function log(eventName, type) {
return function(item) {
var val = logger.value;
if (val.length) {
val += '\n';
}
val += '"' + eventName + '" fired on ' + type;
logger.value = val;
logger.scrollTop = logger.scrollHeight;
};
}
new Ext.drag.Source({
element: Ext.getBody().createChild({
html: 'Drag Me',
style: {
zIndex: 10,
width: '100px',
height: '100px',
border: '1px solid red',
position: 'absolute',
top: '50px',
left: '600px'
}
}),
listeners: {
beforedragstart: log('beforedragstart', 'Source'),
dragstart: log('dragstart', 'Source'),
dragmove: log('dragmove', 'Source'),
dragend: log('dragend', 'Source')
}
});
new Ext.drag.Target({
element: Ext.getBody().createChild({
html: 'Drop Here',
style: {
width: '300px',
height: '300px',
border: '1px solid blue',
position: 'absolute',
top: '250px',
left: '600px'
}
}),
listeners: {
dragenter: log('dragenter', 'Target'),
dragmove: log('dragmove', 'Target'),
dragleave: log('dragleave', 'Target'),
beforedrop: log('beforedrop', 'Target'),
drop: log('drop', 'Target')
}
});
Ext.drag.Target
A drag target represents an element that can receive a drop from a source. Most of its functionality will be described in the data exchange and interaction between sources/targets sections.
Events/Template Methods
The following events and template methods on the source are available at the specified points in the drag cycle:
dragenter
/onDragEnter
dragmove
/onDragMove
dragleave
/onDragLeave
beforedrop
/onBeforeDrop
, can be cancelled by returning false.drop
/onDrop
Ext.data.Info
This class acts as a mediator and information holder for the lifetime of a single drag. It holds all information regarding a particular drag. It also manages data exchange for the drag. This class is passed to all relevant events/template methods throughout the drag/drop cycle.
Describing Data
Drag/drop provides the mechanics for moving elements and receiving events, however it doesn’t describe the underlying meaning of those actions. This section discusses working with that data.
General
Keyed Data
Similar to the HTML5 drag API, data is specified as a set key/value pair. The key is used to indicate the type of data being passed. The key is is not restricted in value, but will typically refer to the data type (text/image) or the business logic objects (users/orders). There can be many key/value pairs for a single drag operation. For each key added to the setData
method, it will also be added to the types
Array on the info object to interrogate the available types.
This architecture is useful for several reasons:
It allows targets to quickly decide whether or not they have interest in a particular source. If some drag data is marked as “csv”, a drop target that is a table or grid may be able to consume that data, but it will probably not be useful to a text field.
It allows the data to differ depending on the target. Consider an image type being dragged. 2 keys could be set, to allow consumption by differing targets. When the drop occurs on a text field target, it can consume the text data. On a placeholder image target, it can read the blob data and display the image:
describe: function(info) {
// The text link to the image
info.setData('text', theImage.link);
// The binary image data
info.setData('image', toBlob(theImage));
}
Data Availability
Data is available for consumption via the getData
method on the info object. This method will throw an exception if it is called before the drop completes. Only the type of data may be interrogated beforehand. This is done for 2 reasons:
For consistency with the HTML5 drag API (the same restriction applies)
The data may be expensive to produce or need to be retrieved from a remote source, so it is useful to restrict access until it’s required.
However, it is still possible to have data on the info object that is accessible during the drag. The info object persists throughout the entire drag, so adding properties can occur at any point:
describe: function(info) {
info.userRecords = getTheRecords();
}
Specifying Data
When a drag initiates, the describe
method is called on the source. The data for the drag should be specified here. This is expected to be implemented by the user. The describe
method receives the info
object.
setData
The setData
method is called with 2 parameters, a string key that describes the data and a value. The value can be any data value data type (string, number, object, array). It can also be a function which will be executed to produce the data when a call to getData
is made. Note that calls to getData
are limited to when the drop completes.
describe: function(info) {
// Set immediately available data
info.setData('userId', user.id);
// Setup a function to retrieve the data from the server on drop
info.setData('userInfo', function() {
var options = {}; // ajax options
return Ext.Ajax.request(o);
});
}
Info Properties
Alternatively, if the data is immediately available or easy to construct it can be pushed as a property directly on the info object. This data will be available at any time during the lifetime of the drag operation.
describe: function(info) {
info.userRecords = getTheRecords();
}
Consuming Data
getData
getData should be called from the drop
listener or onDrop
template method to retrieve data. This method accepts a single argument, the key of the data specified from setData
. The value returned from getData
will always be a Promise
, regardless of the underlying type. Using the data-set from above:
listeners: {
drop: function(target, info) {
info.getData('userId').then(function(v) {
console.log(v);
});
info.getData('userInfo').then(function(response) {
// The ajax response
}).catch(function() {
// Oh no!
});
}
}
In this case, the userId
value will be available so the promise will resolve immediately. In the case of the ajax data, it will wait until the request returns.
Info Properties
For properties stored on the info object, they can be accessed using normal property access:
listeners: {
drop: function(target, info) {
console.log(info.userRecords);
}
}
Interactions Between Sources and Targets
By default, all sources can interact with all targets. This can be restricted in a number of ways. A drag on a target is considered valid once a series of conditions is met. Targets will still receive events for invalid targets, however the valid
flag on the info object will be false
.
Disabled State
If a Source is disabled, it is not possible to drag. If a Target is disabled, any source is not valid.
Groups
Both Sources and Targets can belong to groups. A group is an identifier that indicates which items can interact with each other. A group can be a single string or an array of strings. Items belonging to the same groups can interact. The following rules apply:
If neither source nor target has groups, the drag is valid.
If the source and the target have groups and the groups intersect, the drag is valid.
If the source and the target have groups but no intersection, the drag is invalid.
If the source has groups but the target does not, the drag is invalid.
If the source does not have groups but the target does, the drag is invalid.
Below are some example source group configurations:
// Can only be dropped on targets with no groups
{}
// Can only be dropped on targets that contain group1
{ groups: 'group1' }
// Can be dropped on targets that contain group1 or group2
{ groups: ['group1', 'group2'] }
Accepts
Target has a configurable method accepts
which is called when a Source first enters the target. This allows the Target to determine on a per source basis whether it will interact with it. The accepts
method is passed the info object and should return a boolean indicating whether it will accept the source. Note that the getData
method cannot be called here, however any properties on the info object may be accessed.
// Interrogating types
{
accepts: function(info) {
return info.types.indexOf('userRecord') > -1;
}
}
// Accessing property data
new Ext.drag.Source({
describe: function(info) {
info.total = 100;
}
});
new Ext.drag.Target({
accepts: function(info) {
return info.total > 75;
}
});
Component Integration
When using the drag namespace in conjunction with components, it is often useful to wrap the parts in plugins or other containers to provide a better contextual API. The following code sample implements the ability to drag rows in the Modern Grid.
The code doesn’t handle any of the mechanics of doing the drag/drop, however it does provide a better API for using with components, as well as filling in some of the lower level detail needed to make the drag function.
In this example, we’ll integrate a custom Drag and Drop plugin with a Grid. We’ll then disable drag for anything with a record name of “Bar”.
Expand Code
JS Run
Ext.define('RowDragger', {
extend: 'Ext.AbstractPlugin',
alias: 'plugin.rowdrag',
mixins: ['Ext.mixin.Observable'],
config: {
recordType: ''
},
constructor: function(config) {
this.mixins.observable.constructor.call(this, config);
},
init: function(component) {
var me = this,
type = this.getRecordType();
this.source = new Ext.drag.Source({
element: component.element,
delegate: '.x-grid-row',
describe: function(info) {
var row = Ext.Component.fromElement(info.eventTarget, component, 'gridrow');
info.record = row.getRecord();
},
proxy: {
type: 'placeholder',
getElement: function(info) {
var el = this.element;
if (!el) {
this.element = el = Ext.getBody().createChild({
style: 'padding: 10px; width: 100px; border: 1px solid gray; color: red;',
});
}
el.show().update(info.record.get('name'));
return el;
}
},
autoDestroy: false,
listeners: {
scope: me,
beforedragstart: this.makeRelayer('beforedragstart'),
dragstart: this.makeRelayer('dragstart'),
dragmove: this.makeRelayer('dragmove'),
dragend: this.makeRelayer('dragend')
}
});
},
disable: function() {
this.source.disable();
},
enable: function() {
this.source.enable();
},
doDestroy: function() {
Ext.destroy(this.source);
this.callParent();
},
makeRelayer: function(name) {
var me = this;
return function(source, info) {
return me.fireEvent(name, me, info);
};
}
});
Ext.define('User', {
extend: 'Ext.data.Model',
fields: ['name']
});
Ext.Viewport.add({
xtype: 'grid',
store: {
model: 'User',
data: [{
id: 1,
name: 'Foo'
}, {
name: 'Bar'
}, {
name: 'Baz'
}]
},
columns: [{
dataIndex: 'name',
text: 'Name',
flex: 1
}],
plugins: [{
type: 'rowdrag',
recordType: 'user',
listeners: {
beforedragstart: function(plugin, info) {
return info.record.get('name') !== 'Bar';
}
}
}]
});
Conclusion
As Ext JS continues to evolve, the Ext.drag API will be incorporated in plugins like the example above and more. Today, Ext.drag offers a convenient way to handle drag-and-drop for both Toolkits and plays nicely with the standard HTML5 API.