drag and drop, cross browser support in asp.net
part of a project involved allowing a user to select from a list of 200+ items, and it needed to be easy. So, we decided to provide users with a drag and drop facility
Drag and drop has been around for quite some time, and its a little tricky to get it right. I’m going to show you the basics that i used to enable a drag and drop between lists
What are we after?
we want draggable lists that will do the following
- Two lists
- drag and drop
- support multi select
- support re order of list items
The end result
this is what the HTML will look like once the page has rendered, go ahead, give it a try. Try selecting multiple items then dragging them over
as you play around with the drag and drop, monitor your browser console to view the events that are firing
notice that when you change the selected fields the text area displays a comma separated list of selected items
Filter list |
||
Choice fields
|
Selected fields
|
|
This drag and drop is tied together with just 4 javascript functions, tied to the browser drag events
- handleStartDrag()
- handleDragEnter()
- handleDragOver()
- handleDropped()
there are ancillary functions that bind events and respond to the filter
- fltrChanged()
- UpdatehiddenFields()
- bindUIactions()
How its done
In a nutshell
The rendered HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<table> <tr> <td>Choice fields <ul class="ctlDrop" id="lstleft" > <li class="ctlDrag" draggable="True" id="uniqueID1" data-fieldName="FieldName1"><span>Name 1</span></li> <li class="ctlDrag" draggable="True" id="uniqueID2" data-fieldName="FieldName2"><span>Name 2</span></li> <li class="ctlDrag" draggable="True" id="uniqueID3" data-fieldName="FieldName3"><span>Name 3</span></li> <li class="ctlDrag" draggable="True" id="uniqueID4" data-fieldName="FieldName4" ><span>Name 4</span></li> <li class="ctlDrag" draggable="True" id="uniqueID5" data-fieldName="FieldName5" ><span>Name 5</span></li> <li class="ctlDrag" draggable="True" id="uniqueID6" data-fieldName="FieldName6" ><span>Name 6</span></li> <li class="ctlDrag" draggable="True" id="uniqueID7" data-fieldName="FieldName7" ><span>Name 7</span></li> </ul> </td> <td style="width: 4em;"> </td> <td>Report fields <ul class="ctlDrop" id="lstright"> <li class="ctlDrag" draggable="True" id="uniqueID8" data-fieldName="FieldName8" ><span>Name 1</span></li> <li class="ctlDrag" draggable="True" id="uniqueID9" data-fieldName="FieldName9" ><span>Name 2</span></li> <li class="ctlDrag" draggable="True" id="uniqueID10" data-fieldName="FieldName10" ><span>Name 3</span></li> <li class="ctlDrag" draggable="True" id="uniqueID11" data-fieldName="FieldName11" ><span>Name 4</span></li> <li class="ctlDrag" draggable="True" id="uniqueID12" data-fieldName="FieldName12" ><span>Name 5</span></li> <li class="ctlDrag" draggable="True" id="uniqueID13" data-fieldName="FieldName13" ><span>Name 6</span></li> <li class="ctlDrag" draggable="True" id="uniqueID14" data-fieldName="FieldName14" ><span>Name 7</span></li> </ul> </td> </tr> <tr> <td colspan="3"> <input type="text" ID="hdnSelectedfields" /> </td> </tr> </table> |
So whats going on ?
Well a few things are happening here, CSS is styling our stuff. ASP.NET is loading the repeater and generating our “li” elements for the un-ordered lists. And jQuery is doing the rest
the keys points to note here are:
- the classes used {ctlDrop, ctlDrag} (for CSS and more importantly jQuery hooks)
- the draggable attribute on the items tell the browser these are allowed to be dragged
- the use if ID‘s and Data- attributes as hooks for our functionality
how it works
first up i will say you have to do a few inexplicable things to get drag and drop working, but its a given, just do it and don’t ask, or you’ll simply lose your mind like many other techies who ask the “why did they do that?” question. like me
1. enable jQuery to support passing a payload across drag n drop.
2. declare a variable in your class to hold the last entered item that the drag action has entered.
1 2 3 4 |
<!--Enable jQuery to support payload--> jQuery.event.props.push('dataTransfer'); <!--class wide storage variable for the last entered item while dragging--> var curEnterElementID; |
The singleton script layout looks like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
jQuery(document).ready(function ($) { function ctlDragDrop() { "use strict"; jQuery.event.props.push('dataTransfer'); var curEnterElementID; function handleStartDrag(e) { }; function handleDropped(e) { }; function handleDragOver(e) { }; function handleFltrChanged(e){ }; function handleDragEnter(e) { }; function UpdateHiddenFields() { } ; this.bindUIactions = function () { }; <!--Public function initialise--> this.init = function () { }; }; ctlDragDrop = new ctlDragDrop(); <!--initialise on a seperate thread--> setTimeout(function () { ctlDragDrop.init(); }, 0); }); |
The Code
Loading the form
The code infront
jumping straight in, we need two lists, driven by some data source. Here is how we load the lists, using .net repeaters
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<table> <tr> <td>Choice fields <ul class="ctlDrop" id="lstleft" > <asp:Repeater ID="rptfieldsNotChoose" runat="server"> <ItemTemplate> </ItemTemplate> </asp:Repeater> </ul> </td> <td style="width: 4em;"> </td> <td>Selected fields <asp:HiddenField ID="hdnSelectedfields" runat="server" ClientIDMode="Static" /> <ul class="ctlDrop" id="lstright"> <asp:Repeater ID="rptfieldsChosen" runat="server"> <ItemTemplate> </ItemTemplate> </asp:Repeater> </ul> </td> </tr> </table> |
Notice we have a hidden field in the right hand column, this will be used to stored the selected values, as the selection changes
Code Behind – the repeater code
On our data bind we add list items to the list, and some information to the each list item. Class, draggable, data-fieldName, data-ordinal.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Private Sub myPage_Init(sender As Object, e As EventArgs) Handles Me.Init AddHandler rptfieldsChosen.ItemDataBound, AddressOf repeater_ItemDataBound AddHandler rptfieldsNotChoose.ItemDataBound, AddressOf repeater_ItemDataBound End Sub Private Sub repeater_ItemDataBound(sender As Object, e As RepeaterItemEventArgs) If {ListItemType.AlternatingItem, ListItemType.Item}.Contains(e.Item.ItemType) Then Dim di As Base.adoEntity.Model.vwlist_All_Field_Definitions = e.Item.DataItem Dim li As New HtmlControls.HtmlGenericControl("li") With {.ID = di.GUID.ToString} li.Attributes.Add("class", "ctlDrag") li.Attributes.Add("data-ordinal", di.ordinal) li.Attributes.Add("data-fieldName", di.column_name) li.Attributes.Add("draggable", "True") Dim span As New HtmlGenericControl("span") span.InnerHtml = di.column_name li.Controls.Add(span) e.Item.Controls.Add(li) End If End Sub |
jQuery – the ancillary functions
bindUIactions()
We will bind to the dragover and the drop event on the elements with class ctlDrop
We will bind to the dragstart, dragenter, drop and click events of the elements with class ctlDrag
Finally we will bind to the txtfltr element so we can filter our list
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
this.bindUIactions = function () { <!--bind events--> $('.ctlDrop') .on("dragover", handleDragOver) .on("drop", handleDropped); $('.ctlDrag') .on("dragstart", handleStartDrag) .on("drop", handleDropped) .on("dragenter", handleDragEnter) .on("click", function (e) { $(e.target).closest("li").toggleClass("selected"); }); $('#txtfltr').on("keyup blur change", handleFltrChanged); }; |
UpdateHiddenFields()
handling the collecting of the selected items into a comma separated list is relatively simple, and this function will be called from the end of the drop event
1 2 3 4 5 6 7 8 9 |
function UpdateHiddenFields() { var hdn = $('#hdnSelectedfields'); var flds = []; $("#lstright > li").each(function () { flds.push('[' + $(this).attr("Data-fieldName") + ']') }); hdn.val(flds.join(",")); } |
jQuery – The Drag and Drop routines
handleStartDrag()
The start drag routine, a must have to set the sllowed effect property, this is searchable on the drop if you need to.
1 2 3 4 5 6 |
function handleStartDrag(e) { console.log("dragstart"); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData("text", e.target.id); }; |
handleDragOver()
The drag over routine, this is another must have, without this, obscurely no draggable item will drop. Don’t ask just do
1 2 3 4 5 6 7 |
function handleDragOver(e) { if (e.preventDefault) { e.preventDefault(); // Necessary. Allows us to drop. } e.dataTransfer.dropEffect = 'move'; return false; }; |
handleDragEnter()
The drag enter routine, this is required because internet explorer doesn’t correctly record which element the drop occurs over, so here we record the last entered element before the drop. And thus we have cross browser compatibility
1 2 3 4 |
function handleDragEnter(e) { console.log("enter " + $(e.currentTarget).prop("tagName") + ' ' + e.currentTarget.id); curEnterElementID = e.currentTarget.id; }; |
handleDropped()
The drop routine, this is where all of the hard work is done. If you are a public facing website you might want to add screening of the mime type that is dropped, just incase its not what you are expecting. I’m not doing that here, our project was internal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
function handleDropped(e) { console.log("drop " + $(e.currentTarget).prop("tagName") + ' ' + e.currentTarget.id); e.stopPropagation(); e.preventDefault(); var droppedElemID = e.dataTransfer.getData("text"); var droppedElem = document.getElementById(droppedElemID); if (droppedElemID != undefined) { var ulTGT = $(e.target).closest(".ctlDrop")[0]; var ulSRC = $('#' + droppedElemID).closest(".ctlDrop")[0]; //how many selected items are there var currSelectedItems = $(ulSRC).find('li.selected'); //get the index of the items, the source item and the item dropped over var indexofDropTGT = parseInt($('#' + ulTGT.id + ' li').index($('#'+ curEnterElementID).closest("li")[0])) var indexofDropSRC = parseInt($('#' + ulTGT.id + ' li').index($(droppedElem).closest("li")[0])) //For one way dropping uncomment this line //if (ulTGT.id === 'lstleft') { return false;} //if we are in the same drop object then we must be changing order if (ulSRC === ulTGT) { //if moving a selection around if (currSelectedItems.length > 1) { $(currSelectedItems).each(function (i, e) { $($(ulTGT).children()[indexofDropTGT]).before(e); }) $(ulTGT).find('li.selected').removeClass("selected"); } else { //if moving a single item if (indexofDropSRC > indexofDropTGT) { //moving down $($(ulTGT).children()[indexofDropTGT]).before(droppedElem); } if (indexofDropSRC == 0 && indexofDropTGT == 0) { //moving first or last $($(ulTGT).children()[0]).before(droppedElem); } if (indexofDropSRC < indexofDropTGT) { //moving up $($(ulTGT).children()[indexofDropTGT]).after(droppedElem); } } } else { if (currSelectedItems.length > 1) { $(currSelectedItems).each(function (i, e) { ulTGT.appendChild(e); $('#' + e.id).closest("li").removeClass("selected"); }) } else { ulTGT.appendChild(droppedElem); $('#' + droppedElemID).closest("li").removeClass("selected"); } } UpdateHiddenFields(); } }; |
the javascript file is available for download here dragndrop
the CSS is available for download here dragndrop CSS