Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / jQuery-UI

Drag and Drop when jQuery-ui draggable Fails on Mobile Browsers!

5.00/5 (4 votes)
27 Oct 2016CPOL4 min read 23.2K  
This article describes yet another drag and drop approach that can be used when the jQuery-ui and touch-punch library fails (when the project uses transform-origin or other coordinate transformations)

Introduction

This is my fourth article on CodeProject and I hope that you will can appreciate it because in the following paragraphs, I would like to describe and handle a drawback that only appears a few times in the life of a web developer!

jQuery, jQuery-ui and touch-pouch library can be considered the standard de facto for drag and drop operations in desktop and mobile browser. This because these three libraries permit ignoring all the complexity of a drag and drop operation, just by calling one method: $.draggable() that informs the jQuery subsystem that an object can be dragged.

The $.draggable method can be decorated with drop: function (event,ui) {} parameter in which we can add the code for handling the drop event.

This seems perfect! But, what happens when we use some new CSS3 features?

Background

When we are working in high responsive environment, we could use transformations like transform-coordinate, transform:scale(), transform:rotate and so forth. It seems that jQuery does not like this occurrence very much and, in some cases, it can make a mistake in some actions.

For this article, I have chosen HYPE3 and TUMULT that permits the handling re-scaling and re-sizing in an automatic way. I prepared two examples in order to show the mistakes.

In the following pictures, you can view the same objects resized and rescaled on different devices by HYPE engine without any user codes.

Desktop Browser View

Image 1

Iphone Landscape and Portrait Views

Image 2Image 3

As you can see, all three views seem to be the same (rescaled referred to viewport size).

But, I would like to invite you to play demo1 on different browsers. You can experiment that the drag and drop will fail because drag revert will mistake the return coordinates in landscape and desktop browser (the original position will not be respected).

I am not sure about the recurrence of this kind of problem, but I can confirm that this issue appears when the coordinate transformation function is used. Note that DEMO1 only uses jQuery, jQuery-ui and touch-pouch libraries and not the native drag and drop support.

Using the Code

I think that the draggable revert issues will lie in jQuery-ui implementation but I'm not sure about this. So, my idea was to trash jQuery-ui and pouch-touch libraries and to use:

  • Native Desktop browser drag and drop (Chrome, Firefox, Opera, Safari and Edge support it)
  • Touch-events for mobile browser drag and drop capability

Desktop Browser Native Support

The first step for this kind of approach must be the assignment of a class or unique-id to each object which we want to made draggable. In my example, all colored squares were named c1, c2... c12 and have the class name gameObject. So, I just enable the native drag function with following code.

JavaScript
for (var c=1;c<=12;c++)
{
    var e = document.getElementById("c"+c);
    $("#c"+c).attr("draggable","true");
    $("#c"+c).attr("ondragstart","_dragStart(event)");
    $("#c"+c).attr("ondrop","_onDrop(event)");
    $("#c"+c).attr("ondragover","_onDragOver(event)");
}

For simplicity, I have reported the three functions that handle drag and drop in separated code blocks

JavaScript
function _dragStart(a)
{
    a.dataTransfer.setData("text", a.target.id);
}

function _onDrop(e)
{
  e.preventDefault();
  var src = e.dataTransfer.getData("text");
  var dst = e.target.id;
  // here you can do what you want with source and destination of drag and drop !
}

 function _onDragOver(a)
 {
   a.preventDefault();
 }

Mobile Browser Drag and Drop with Touches

Nowadays, mobile browsers do not implement a native support for drag and drop. So, the second step was to handle the three gestures that represents a drag and drop action on a touchable screen: touchstart, touchmove and touchend.

All drag and drop operations use a shadow (aka ghost) image that helps the actions. This image will be shown near the cursor in order to show the user which objects are moving. We also need to track the start position of an object in order to revert drag and drop.

The following three variables can help us with touches! You can try DEMO2 in order to try this kind of approach.

JavaScript
// touch capability
var startPos=[];
var toDrag = null;
var dragStart = false;

Then, for each touch on screen, we need to understand if the user hits a draggable object. This can be done by simple collision detection.

JavaScript
$(document).on("touchstart",function(e) {
   e.preventDefault();
   // touch coordinates
   var xPos = e.originalEvent.touches[0].pageX;
   var yPos = e.originalEvent.touches[0].pageY;

   startPos[0]=xPos;
   startPos[1]=yPos;

   $(".gameobject").each(function(index)
   {
       var jx = parseFloat($(this).offset().left);
       var jy = parseFloat($(this).offset().top);
       var jw = parseFloat($(this).width());
       var jh = parseFloat($(this).height());

       //collision detect
       if (jx <= xPos && jy <= yPos && xPos <= (jx+jw) && yPos <= (jy+jh))
       {
          if ($(this).attr("draggable")=="false")
          {
            //object touched is not draggable!
          }
          else {
               // collision detected
               toDrag = $(this).attr("id");
               dragStart=true;
               // setting up shadow image
               var shadow = document.getElementById("shadow");
               if (shadow==null)
               {
                   shadow = document.createElement("div");
                   document.getElementById("game").appendChild(shadow);
                   shadow.setAttribute("id","shadow");
                   shadow.style.position="absolute";
                   shadow.style.zIndex="9999999";
               }
                  // for the purpose of this article ghost image will be a square of
                  // the same color of touched object
                  $("#shadow").css("visibility","visible");
                  var shadowImg = $(this).css("background-image");
                  $("#shadow").css("background-image",shadowImg);
                  $("#shadow").css("background-size","100% 100%");
                  $("#shadow").css("left",xPos+"px");
                  $("#shadow").css("top",yPos+"px");
                  $("#shadow").css("width",jw+"px");
                  $("#shadow").css("height",jh+"px");
                  return;
             }
          }
        });
      });

Now, we detected a collision on draggable object and therefore we need to track the finger of the user by following any other touches. This can be done with a few lines of code.

JavaScript
$(document).on("touchmove",function(e) {
    e.preventDefault();
    if (dragStart && toDrag!=null)
    {
          // move the shadow
          var xPos = e.originalEvent.touches[0].pageX;
          var yPos = e.originalEvent.touches[0].pageY;
          var sw = parseFloat($("#shadow").width());
          var sh = parseFloat($("#shadow").height());
          $("#shadow").css("left",(xPos-sw/2)+"px");
          $("#shadow").css("top",(yPos-sh/2)+"px");
     }
});

Finally, we need to understand where the finger leaves the screen. This will be the last coordinates of our shadow images and so the drop position.

JavaScript
  $(document).on("touchend",function(e) {
   e.preventDefault();
   if (dragStart && toDrag!=null)
   {
         //here you can apply graphics effect on shadow like revert to initial pos or fade out
         $("#shadow").css("visibility","hidden");
         dragStart=false;

         var xPos = 0;
         var yPos = 0;

         if (e.changedTouched==undefined)
         {
                xPos = e.originalEvent.changedTouches[0].pageX;
                yPos = e.originalEvent.changedTouches[0].pageY;
         }
         else
         {
                xPos = e.changedTouches[0].pageX;
                yPos = e.changedTouched[0].pageY;
         }
         // looking for collision between gameObject and touchleave position
         var target = null;
         $(".gameobject").each(function(index) {
           var tx = parseFloat($(this).offset().left);
           var ty = parseFloat($(this).offset().top);
           var tw = parseFloat($(this).width());
           var th = parseFloat($(this).height());

           if (tx <= xPos && ty <= yPos && xPos <= (tx+tw) && yPos <= (ty+th))
           {
               target = $(this).attr("id");
               //here you can execute code on source and target
               return;
           }
           }).promise().done(function() { });
           }
     });
}

Points of Interest

This article could be useless if anyone knows a jQuery-ui configuration parameter that fixes this problem. I don't know any method, but I'm not sure whether one exists.

History

  • Improving English (Thx to Natascia Spada) @ 27/10/2016 - 12:18
  • Published @ 25/10/2016 - 15:00

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)