Drag&Drop table columns tutorial
Intro
Have you already tried to implement a drag and drop mechanism with JavaScript? If yes, then you should know that it is mainly a matter of moving a DOM element, and all its children, from one place to another. But did you try to move a table’s column from one place to another?
If you answered “no” to at least one of the previous questions then you may be interested by next parts of this article. Especially for the second question where you clearly won’t find as much resource over Internet as for tables’ rows drag and drop.
But first of all, let me explain why I had to solve this problem for Esker.
Our main solution intends to give our customers a way to treat their business documents smarter and faster. It means that it has to be fully customizable. Few weeks ago, I decided to redesign the way that one can design its business reporting in our web interface and I was thinking about a way to design the table’s part of reporting as easily as I could. At this step, we want the user to provide its choice of data on which he wants to report what has the most importance for him. This question of priority implies to sort table’s columns and that’s where arises the matter of this article.
Disclaimer on browsers’ compatibility
Developing a web client interface always implies the same question: will it work on all browsers and all versions?
When you do that for a software editor as I do, answer has to be “yes” but for a personal purpose you may choose to ignore this point so I will try to explain where you can ignore some of my choices.
I also made the choice to use some jQuery helpers, mainly to manipulate DOM easily. You can rewrite those parts in Vanilla style if required.
The lowest requirement of used browser is to support HTML5 and especially the drag and drop events. This key point may be checked using for example the useful library Modernizer.
Let’s begin
The first step is fairly the simplest one because all you have to do is to define a draggable DOM element. This is achieved by adding to the former the attribute draggable=true. Isn’t it easy?
Well if you try it directly you might notice that some CSS tricks might be required. At least you should change the cursor with the following directive cursor: move.
By now you could say something like “that’s funny but completely useless”. And you would be right but remember that I wrote about JavaScript so there might be some events listener to add, right?
Listen to me
I thought that it made sense to begin with dragstart event but do not hesitate to refer to the list of the whole drag and drop process’ events.
To detect that the dragging action is in progress, I suggest to change the anchor element’s opacity but you are free to choose the visual aspect that makes the most sense for you.
function listenerDragStart(e) {
e.target.style.opacity = '0.4'; // e.target = this
}
Now we should just add an event listener on our draggable objects. As our main purpose is to drag and drop table’s columns, the following should do the job:
var cols = document.querySelectorAll('th');
[].forEach.call(cols, function(col) {
col.addEventListener('dragstart', listenerDragStart, false);
});
Symmetrically, once dragging event is finished, you should revert every visual aspect change you made. You might have already guessed that this can be done through a dragend event listener:
function listenerDragEnd(e) {
e.target.style.opacity = '1.0'; // e.target = this
}
Help your users
Drag and drop became a natural action but it came with visual codes that need to be respected in order not to lose users. One of this code is to always see where you are about to drop your element. Without that, users start to wonder if their dragging is well considered. This is where dragenter, dragover and dragleave events will help.
The dragenter event will let you show that the targeted element is a drop zone. Once again you can change the style directly or just add a CSS class depending on how much styling you want to do.
The dragover event will be the one where you’ll transfer DOM source element to its destination. We will come back on it later in the dataTransfer section. Meanwhile, if you wonder why you do not change style here, the answer is to reduce unnecessary rendering. Indeed, the dragover event is called as long as the column is hovered.
The dragleave event, like the dragend event for the dragstart one, is used to revert visual effects added during dragenter.
function listenerDragOver(e) {
if (e.preventDefault) {
e.preventDefault(); // Necessary. Allows us to drop.
}
return false;
}
function listenerDragEnter(e) {
e.target.classList.add('dropZone');
}
function listenerDragLeave(e) {
e.target.classList.remove('dropZone');
}
Drag is now pretty cool but how can I drop correctly?
As said by Alfred Lanning: this is the right question. Indeed, we have talked about how to manage dragging event but nothing about how to drop dragged data. This is where the well-named object dataTransfer plays its role.
If you have a look at its documentation you’ll see that this object comes with lots of properties and methods but not all supported by all browsers. As disclaimed earlier, I will only focus on those required for this subject and supported by “almost” all HTML5 browsers.
First, we have to define what we want to transfer from the beginning of the drag to the beginning of the drop. Here I have decided to use the title of my dragged column as it is a simple way to explain the mechanism. To do so, we will use the method setData which takes two arguments: the type of the data and the data itself. Unfortunately, the only data type supported by Internet Explorer is “text” so we’ll do our best with it.
Second, we have to retrieve those data and you should have guessed that we will use the getData method. This one takes only one argument which is the data type we want to retrieve.
Lastly (for this example), we may define the effects allowed during dragging. Here we will use only the “move” effect
Here is the result for me:
function listenerDragStart(e) {
e.dataTransfer.effectAllowed = 'move';
var srcTxt = $(this).text();
e.dataTransfer.setData('text', $(this).text());
}
function listenerDrop(e) {
if (e.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation) {
e.stopPropagation();
} var srcTxt = e.dataTransfer.getData('text');
var destTxt = $(this).text();
if (srcTxt != destTxt) {
//Search for the source column in order to move it at the destination index
var dragSrcEl = $(".table th:contains(" + srcTxt + ")");
var srcIndex = dragSrcEl.index() + 1;
var destIndex = $("th:contains(" + destTxt + ")").index() + 1;
dragSrcEl.insertAfter($(this));
$.each($('.table tr td:nth-child(' + srcIndex + ')'), function (i, val) {
var index = i + 1;
$(this).insertAfter($('.table tr:nth-child(' + index + ') td:nth-child(' + destIndex + ')'));
});
}
return false;
}
One step further?
Sure we can! Another way to help user in its action is to show her not only the column’s header but also all rows. Unfortunately, the following code won’t be supported by IE but it will be cool in all other browsers.
To display the whole column, we will use the method setDragImage of the DataTransfer object. You have to notice that the DOM object given to this method has to be visible in your DOM or to come from an external source. Here we will clone our table column but this clone has to be rendered. It means that all tricks based on visibility or display properties won’t work.
Here is how I created a ghost of my column in my dragstart handler:
//The only IE version supported in our product is 11 but there are many other ways to detect this browser
var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
if (!isIE11) {
var srcIndex = $("th:contains(" + srcTxt + ")").index() + 1;
//Create column's container
var dragGhost = document.createElement("table");
dragGhost.classList.add("tableGhost");
dragGhost.classList.add("table-bordered");
//in order tor etrieve the column's original width
var srcStyle = document.defaultView.getComputedStyle(this);
dragGhost.style.width = srcStyle.getPropertyValue("width");
//Create head's clone
var theadGhost = document.createElement("thead");
var thisGhost = this.cloneNode(true);
thisGhost.style.backgroundColor = "red";
theadGhost.appendChild(thisGhost);
dragGhost.appendChild(theadGhost); //Create body's clone
var tbodyGhist = document.createElement("tbody");
$.each($('.table tr td:nth-child(' + srcIndex + ')'), function (i, val) {
var currentTR = document.createElement("tr");
var currentTD = document.createElement("td");
currentTD.innerText = $(this).text();
currentTR.appendChild(currentTD);
tbodyGhist.appendChild(currentTR);
});
dragGhost.appendChild(tbodyGhist);
//Hide ghost
dragGhost.style.position = "absolute";
dragGhost.style.top = "-1500px";
document.body.appendChild(dragGhost);
e.dataTransfer.setDragImage(dragGhost, 0, 0);
}
Not enough?
You can go further in styling and other stuff like this (displaying the zone where the column would be added, do not always drop after the hovered column, …) but in my case that seemed enough. Do not hesitate to improve this sample but at least I hope it helped you to better understand drag and drop of table columns.
You can retrieve a demo of my code in this fiddle.