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:

  1. The source should only be dragged in a certain area, for example, the title bar of a window.

  2. 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:

  1. Original (the default) - The element of the source is moved.

  2. Placeholder - A new element is created and the source element is left in place.

  3. 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

  1. var logger = Ext.getBody().createChild({
  2. tag: 'textarea',
  3. rows: 15,
  4. cols: 80
  5. }).dom;
  6. function log(eventName, type) {
  7. return function(item) {
  8. var val = logger.value;
  9. if (val.length) {
  10. val += '\n';
  11. }
  12. val += '"' + eventName + '" fired on ' + type;
  13. logger.value = val;
  14. logger.scrollTop = logger.scrollHeight;
  15. };
  16. }
  17. new Ext.drag.Source({
  18. element: Ext.getBody().createChild({
  19. html: 'Drag Me',
  20. style: {
  21. zIndex: 10,
  22. width: '100px',
  23. height: '100px',
  24. border: '1px solid red',
  25. position: 'absolute',
  26. top: '50px',
  27. left: '600px'
  28. }
  29. }),
  30. listeners: {
  31. beforedragstart: log('beforedragstart', 'Source'),
  32. dragstart: log('dragstart', 'Source'),
  33. dragmove: log('dragmove', 'Source'),
  34. dragend: log('dragend', 'Source')
  35. }
  36. });
  37. new Ext.drag.Target({
  38. element: Ext.getBody().createChild({
  39. html: 'Drop Here',
  40. style: {
  41. width: '300px',
  42. height: '300px',
  43. border: '1px solid blue',
  44. position: 'absolute',
  45. top: '250px',
  46. left: '600px'
  47. }
  48. }),
  49. listeners: {
  50. dragenter: log('dragenter', 'Target'),
  51. dragmove: log('dragmove', 'Target'),
  52. dragleave: log('dragleave', 'Target'),
  53. beforedrop: log('beforedrop', 'Target'),
  54. drop: log('drop', 'Target')
  55. }
  56. });

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) {

    1. // The text link to the image
    2. info.setData('text', theImage.link);
    3. // The binary image data
    4. 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:

  1. describe: function(info) {
  2. info.userRecords = getTheRecords();
  3. }

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.

  1. describe: function(info) {
  2. // Set immediately available data
  3. info.setData('userId', user.id);
  4. // Setup a function to retrieve the data from the server on drop
  5. info.setData('userInfo', function() {
  6. var options = {}; // ajax options
  7. return Ext.Ajax.request(o);
  8. });
  9. }

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.

  1. describe: function(info) {
  2. info.userRecords = getTheRecords();
  3. }

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:

  1. listeners: {
  2. drop: function(target, info) {
  3. info.getData('userId').then(function(v) {
  4. console.log(v);
  5. });
  6. info.getData('userInfo').then(function(response) {
  7. // The ajax response
  8. }).catch(function() {
  9. // Oh no!
  10. });
  11. }
  12. }

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:

  1. listeners: {
  2. drop: function(target, info) {
  3. console.log(info.userRecords);
  4. }
  5. }

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:

  1. // Can only be dropped on targets with no groups
  2. {}
  3. // Can only be dropped on targets that contain group1
  4. { groups: 'group1' }
  5. // Can be dropped on targets that contain group1 or group2
  6. { 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.

  1. // Interrogating types
  2. {
  3. accepts: function(info) {
  4. return info.types.indexOf('userRecord') > -1;
  5. }
  6. }
  7. // Accessing property data
  8. new Ext.drag.Source({
  9. describe: function(info) {
  10. info.total = 100;
  11. }
  12. });
  13. new Ext.drag.Target({
  14. accepts: function(info) {
  15. return info.total > 75;
  16. }
  17. });

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

  1. Ext.define('RowDragger', {
  2. extend: 'Ext.AbstractPlugin',
  3. alias: 'plugin.rowdrag',
  4. mixins: ['Ext.mixin.Observable'],
  5. config: {
  6. recordType: ''
  7. },
  8. constructor: function(config) {
  9. this.mixins.observable.constructor.call(this, config);
  10. },
  11. init: function(component) {
  12. var me = this,
  13. type = this.getRecordType();
  14. this.source = new Ext.drag.Source({
  15. element: component.element,
  16. delegate: '.x-grid-row',
  17. describe: function(info) {
  18. var row = Ext.Component.fromElement(info.eventTarget, component, 'gridrow');
  19. info.record = row.getRecord();
  20. },
  21. proxy: {
  22. type: 'placeholder',
  23. getElement: function(info) {
  24. var el = this.element;
  25. if (!el) {
  26. this.element = el = Ext.getBody().createChild({
  27. style: 'padding: 10px; width: 100px; border: 1px solid gray; color: red;',
  28. });
  29. }
  30. el.show().update(info.record.get('name'));
  31. return el;
  32. }
  33. },
  34. autoDestroy: false,
  35. listeners: {
  36. scope: me,
  37. beforedragstart: this.makeRelayer('beforedragstart'),
  38. dragstart: this.makeRelayer('dragstart'),
  39. dragmove: this.makeRelayer('dragmove'),
  40. dragend: this.makeRelayer('dragend')
  41. }
  42. });
  43. },
  44. disable: function() {
  45. this.source.disable();
  46. },
  47. enable: function() {
  48. this.source.enable();
  49. },
  50. doDestroy: function() {
  51. Ext.destroy(this.source);
  52. this.callParent();
  53. },
  54. makeRelayer: function(name) {
  55. var me = this;
  56. return function(source, info) {
  57. return me.fireEvent(name, me, info);
  58. };
  59. }
  60. });
  61. Ext.define('User', {
  62. extend: 'Ext.data.Model',
  63. fields: ['name']
  64. });
  65. Ext.Viewport.add({
  66. xtype: 'grid',
  67. store: {
  68. model: 'User',
  69. data: [{
  70. id: 1,
  71. name: 'Foo'
  72. }, {
  73. name: 'Bar'
  74. }, {
  75. name: 'Baz'
  76. }]
  77. },
  78. columns: [{
  79. dataIndex: 'name',
  80. text: 'Name',
  81. flex: 1
  82. }],
  83. plugins: [{
  84. type: 'rowdrag',
  85. recordType: 'user',
  86. listeners: {
  87. beforedragstart: function(plugin, info) {
  88. return info.record.get('name') !== 'Bar';
  89. }
  90. }
  91. }]
  92. });

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.