Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQueryUI sortable and drag speed issue

I have a list that is using jQueryUI sortable. I need to keep track of each step of the element while dragged, based on the "change" event of the plugin.

But it only works reliably from slow, up to normal drags. At those velocities, there is no error whatsoever. But if the user drags the element relatively fast (the larger the list the more often it will happen because the mouse has more room to gain speed), the "change" event loses track of some steps and thus, some useful information is lost along the way with no error thrown in the console.

Showing the problem:

https://jsfiddle.net/yhp3m6L8/2/

In this jsFiddle, you can drag an element and see its index change as an example. Below the list, while dragging, you will have a console emulation keeping track of the indexes in black.

If dragged reasonably fast, you'll see some of those indexes become red. This is the problem. This means their previous position is overlooked by the sortable script during the change event for a reason I just don't get. The continuity is broken and this continuity is primordial for my needs. The velocity is the only thing that breaks the continuity. But a "change" event that can't keep up with a reasonably fast mouse drag seems quite odd.

enter image description here

The script:

Half of it is about keeping track of the index because Sortable has a peculiar way of referencing the indexes which depends on the direction (up/down) but ALSO the current position of the element relative to its initial position (before/after) . So there is a minimal amount of code needed to visually make sense out of those indexes. With that script, the element gets a visual index that is intuitively the one you'd expect to see, in an orderly fashion.

However, this script is the showcase of the problem, probably not the problem (see the side note below).

The other half of the script is just about emulating a kind of console for debugging purposes.

My guesses:

I might be wrong but I end up thinking it's a tracking issue with the "change" event or that the dragged element doesn't keep up with the mouse cursor (it feels like it is not always under the cursor at relatively high velocities). Unless there is a Sortable option to use that I'm not aware of...

I think it's one of those scenarios because whatever code I try to use inside the "change" event, I always end up having this gap problem.

HTML:

<html>
    <body>
        <div class="panel">
            <ul class="panel_list">
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="ticked"></li>
                <li class="unticked"></li>
                <li class="unticked"></li>
                <li class="unticked"></li>
            </ul>
        </div>
        <div class='console'></div>
    </body>
</html>

CSS:

.console{
    width:100%;
}

.panel ul{
    list-style-type:none;
    width:30%;
}

.panel li{
    height:29px;
    border-bottom:1px solid #454d5a;
    text-align: left;
    vertical-align: middle;
    line-height:29px;
    padding-left:8px;
    background: #666;
}

.panel_highlight{
    height:29px;
    background:#1c2128 !important;
}

.unticked{
    color:#7c7a7d;  
    background:#222 !important;
}

jQuery:

// jQuery: 3.3.1

// jQueryUI: 1.11.4 (downgraded from 1.12.1, because of documented 
// performance issues but...with no effect)

 

var origValue;
var oldInfo;
var c=0;
var x=0;

// LIST RELATED

//Just to have a visual on the indexes in the list. 
$('li').each(function(){
    $(this).data('idx',c++);
    $(this).text(c);
})
c=0;

$( ".panel_list" ).sortable({
    placeholder: 'panel_highlight',
    items      : '>li.ticked',
    cancel     : '>li.unticked',
    tolerance  : 'pointer',
    axis       : 'y',
    opacity    :  0.9,
    start: function(event, ui){

    // LIST RELATED

        origValue = ui.item.data('idx');

    // CONSOLE RELATED (initialise variables)

        $('.console').html('');oldInfo='';
    },
    change: function(event, ui){

    // LIST RELATED

        var idx = ui.placeholder.prevAll().filter(':not(.ui-sortable-helper)').length;
        var a=ui.placeholder.index();
        var b=a+1;

        //detect the direction of the dragging
        if(idx - $(this).data('idx')==1)
        {//downward dragging

            //if the element is below its initial position (or x=1)
            if((a<=origValue) || (x==1)){ui.item.text(b);x=0;}
            //if the element is still above its initial position
            else                        {ui.item.text(a);};
        }
        else
        {//upward dragging

            //if the element is still below its initial position
            if(a<=origValue)            {ui.item.text(b);x=1;}
            //if the element is above its initial position
            else                        {ui.item.text(a);};
        };
        $(this).data('idx', idx);

       // Update the visual on the indexes in the list. 
        $('li').each(function(){
            if(ui.item.index() !=$(this).index()){           
                $(this).data('idx',c++);
                $(this).text(c);
            }
        })
        c=0;

    // CONSOLE RELATED (show indexes progression and gaps)

        var info=$('.console').html();
        if(oldInfo !=''){
            var valAbs= Math.abs( parseInt(ui.item.text()) - parseInt(oldInfo));
            if(valAbs==1){info=info+' + '+ui.item.text();}
            else{info=info+' + <span style="color:red">'+ui.item.text()+'</span>';};
        }
        else{info=ui.item.text();};
        $('.console').html(info);
        oldInfo = ui.item.text();
    }
});

Side note:

All the code inside the "change" event is tailored for you to SEE the problem, probably not the problem per se. I let you be the judge of it but it's only fair to mention this.

My actual script within the change section is different. It triggers the reordering of some table columns and that's how I first detected the problem as the table reordering has glitches at high velocities. So the dummy script here is just me narrowing it down to a minimum while showing you the issue in a visual way.

The point is to assess wether it's a performance issue that can be solved, a lack of a Sortable option I should add, a drag/cursor latency that can be fixed or if there is any trick to somehow workaround this tracking issue.

I think it's a fair warning to prevent you from the hassle of debugging a dummy script that is just a showcase. But considering I'm just a newbie and I might be wrong, do as you see fit.

In all cases, your help or input would be very appreciated. Thank you in advance.

EDIT: Here is my real (narrowed down) script, feeding a "dataTable.colReorder.move" event. It is more involved as it needs to know the landing index (a/b) But also the current index of the dragged element (a/b/c/d). And sortable has its own situational way to index this.

$( ".panel_list" ).sortable({
    placeholder: 'panel_highlight',
    items      : '>li.ticked',
    cancel     : '>li.unticked',
    start: function(event, ui){
        lastValue='';
        origValue=ui.item.index();
        $(this).data('idx', ui.item.index());
    },
    change: function(event, ui){
        var x;
        var idx = ui.placeholder.prevAll().filter(':not(.ui-sortable-helper)').length;
        var a= ui.placeholder.index();
        var b=a+1;var c=a+2;var d=a-1;

        if(idx - $(this).data('idx')==1)
        {//downward
            if((origValue>=a) || (x==1)){dataTable.colReorder.move(a,b);lastValue=b;x=0}
            else                        {dataTable.colReorder.move(d,a);lastValue=a;}
        }
        else
        {//upward
            if(origValue>=a)            {dataTable.colReorder.move(c,b);lastValue=b;x=1}
            else                        {dataTable.colReorder.move(b,a);lastValue=a;}
        }   

        $(this).data('idx', idx);       
    }
});
like image 921
Bachir Messaouri Avatar asked Mar 14 '19 23:03

Bachir Messaouri


2 Answers

Sadly... There is no solution to fix that issue.

I first tried using the sort event callback... And the same "skipping" issue occurs on very fast drags.

I wondered why a couple minutes...
Then I taught about what .sortable() could possibly rely on... I came with the only possible "browser" event: mousemove.

While mousemove fires really often... Quite like a "machine gun"... It does not fire fast enought for your expection here.

To verify this, I simply added this mousemove handler at the end of your unchanged Fiddle:

$( ".panel_list" ).on("mousemove",function(e){
  console.log($(e.target).index());
});

And it logs exactly the same numbers you have in your div.console.

enter image description here

So the issue is not due to .sortable() for sure.


EDIT
All I said above is still true... But since you already have the "skip detection", why not use it to fill the gaps?

You had oldInfo, which is the last .index() before this new change.
And you have the new .index()...

Depending if the movement is up or down, you need one or the other for loop to fill the gaps in the numbering. That also is you trigger point for what you have to do with those numbers...

;)

Here is the changed part:

// CONSOLE RELATED

info=$('.console').html();
if(oldInfo !=''){

  var valReal = parseInt(ui.item.text()) - parseInt(oldInfo);
  var valOld = parseInt(oldInfo);

  var valAbs= Math.abs( parseInt(ui.item.text()) - parseInt(oldInfo));
  if(valAbs==1){info=info+' + '+ui.item.text();}
  else{

    // ADDING THE SKIPPED ONES!
    // Upward (in blue)
    if(valReal>0){
      for(i=valOld+1;i<valOld+valReal;i++){
        info=info+' + <span style="color:blue">'+( i )+'</span>';
      }

    // Downward (in green)
    }else{
      for(i=valOld-1;i>valOld+valReal;i--){
        info=info+' + <span style="color:green">'+( i )+'</span>';
      }
    }

    // The one caugth after a gap (in red)
    info=info+' + <span style="color:red">'+ui.item.text()+'</span>';
  };
}


It deserves a snippet... For posterity. (Run in ful page mode.)

var origValue;
var oldInfo;
var info;
var c=0;
var x=0;

// LIST RELATED

//With or without this, the problem will occur.
$('li').each(function(){
  $(this).data('idx',c++);
  $(this).text(c);
})
c=0;

$( ".panel_list" ).sortable({
  placeholder: 'panel_highlight',
  tolerance  : 'pointer',
  axis       : 'y',
  opacity    : 0.9,
  items      : '>li.ticked',
  cancel     : '>li.unticked',
  start: function(event, ui){

    // LIST RELATED

    origValue = ui.item.data('idx');

    // CONSOLE RELATED

    $('.console').html(''); oldInfo=''; info='';
  },
  change: function(event, ui){

    // LIST RELATED

    var idx = ui.placeholder.prevAll().filter(':not(.ui-sortable-helper)').length;
    var a=ui.placeholder.index();
    var b=a+1;
    if(idx - $(this).data('idx')==1)
    {//downward dragging
      if((a<=origValue) || (x==1)){ui.item.text(b);x=0;}
      else					  				 {ui.item.text(a);};
    }
    else
    {//upward dragging
      if(a<=origValue)  			 {ui.item.text(b);x=1;}
      else					    			 {ui.item.text(a);};
    };
    $(this).data('idx', idx);

    //With or without this, the problem will occur.
    $('li').each(function(){
      if(ui.item.index() !=$(this).index()){
        $(this).data('idx',c++);
        $(this).text(c);
      }
    })
    c=0;

    // CONSOLE RELATED

    info=$('.console').html();
    if(oldInfo !=''){

      var valReal = parseInt(ui.item.text()) - parseInt(oldInfo);
      var valOld = parseInt(oldInfo);

      var valAbs= Math.abs( parseInt(ui.item.text()) - parseInt(oldInfo));
      if(valAbs==1){info=info+' + '+ui.item.text();}
      else{

				// ADDING THE SKIPPED ONES!
        if(valReal>0){
          for(i=valOld+1;i<valOld+valReal;i++){
            info=info+' + <span style="color:blue">'+( i )+'</span>';
          }
        }else{
          for(i=valOld-1;i>valOld+valReal;i--){
            info=info+' + <span style="color:green">'+( i )+'</span>';
          }
        }
        
        // The one caugth after a gap
        info=info+' + <span style="color:red">'+ui.item.text()+'</span>';
      };
    }
    else{info=ui.item.text();};
    $('.console').html(info);
    oldInfo = ui.item.text();
  }
});
.console{
  width:100%;
}

.panel ul{
	list-style-type:none;
  width:30%;
}

.panel li{
	height:29px;
	border-bottom:1px solid #454d5a;
  text-align: left;
  vertical-align: middle;
	line-height:29px;
	padding-left:8px;
	background: #666;
}


.panel_highlight{
    height:29px;
    background:#1c2128 !important;
}

.unticked{
	color:#7c7a7d;	
	background:#222 !important;
}


.ui-sortable-helper{
  background-color:red !important;
}
<link href="https://www.lexgotham.com/test5/styles/reset.css" rel="stylesheet"/>
<link href="https://www.lexgotham.com/test5/scripts/jQuery/jquery-ui-1.12.1/jquery-ui.css" rel="stylesheet"/>
<script src="https://www.lexgotham.com/test5/scripts/jQuery/jquery.3.3.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
<html>
<body>
  <div class="panel">
    <ul class="panel_list">
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="ticked"></li>
      <li class="unticked"></li>
      <li class="unticked"></li>
      <li class="unticked"></li>
    </ul>
  </div>
  <div class='console'></div>
</body>
</html>

Fiddle

like image 155
Louys Patrice Bessette Avatar answered Nov 15 '22 08:11

Louys Patrice Bessette


Some of the observations from the previous answer are correct, but here is my grain of salt. Btw, I do believe this is "fixable"..

There is in my opinion a very simple solution. Every time a "skip" occurs the sortable will not tell you all the intermediate steps, so..why don't you construct them?..

var origValue;
var oldInfo;
var info;
var c=0;
var x=0;

// LIST RELATED

//With or without this, the problem will occur.
$('li').each(function(){
    $(this).data('idx',c++);
    $(this).text(c);
})
c=0;

$( ".panel_list" ).sortable({
    placeholder: 'panel_highlight',
    tolerance  : 'pointer',
    axis       : 'y',
    opacity    : 0.9,
    items      : '>li.ticked',
    cancel     : '>li.unticked',
    start: function(event, ui){

    // LIST RELATED

                origValue = ui.item.data('idx');

        // CONSOLE RELATED

            $('.console').html(''); oldInfo=''; info='';
    },
    change: function(event, ui){

    // LIST RELATED

            var idx = ui.placeholder.prevAll().filter(':not(.ui-sortable-helper)').length;
            var a=ui.placeholder.index();
            var b=a+1;
            if(idx - $(this).data('idx')==1)
            {//downward dragging
            if((a<=origValue) || (x==1)){ui.item.text(b);x=0;}
                    else                                     {ui.item.text(a);};
            }
            else
            {//upward dragging
            if(a<=origValue)             {ui.item.text(b);x=1;}
            else                                     {ui.item.text(a);};
            };
            $(this).data('idx', idx);

            //With or without this, the problem will occur.
            $('li').each(function(){
          if(ui.item.index() !=$(this).index()){
               $(this).data('idx',c++);
               $(this).text(c);
              }
        })
            c=0;

        // CONSOLE RELATED

            info=$('.console').html();
            if(oldInfo !=''){
          oIv = parseInt(oldInfo);
          uiV = parseInt(ui.item.text());
          stepIn = (uiV > oIv ? 1 : -1);
          switch (stepIn) {
          case 1:
          for (i=oIv+1;i<uiV;i++) {info=info+' + <span style="color:green">'+i+'</span>';}
          break;
          case -1:
          for (i=uiV+1;i<=oIv;i++) {info=info+' + <span style="color:red">'+i+'</span>';}
          break;
          }
            }
            else{info=ui.item.text();};
            $('.console').html(info);
            oldInfo = ui.item.text();
    }
});

As you can see here, if a skip happens I will just add to the info var all the steps missed by the change event...however, and sadly, this still does not work..the reason is simple..the reentry of the change event.

Now, I didn't had the time to code a working solution, however this is my idea. You must rebuild the list with the missing intermediate values every time a change event ends. That way you can have a continual sequence. So, from your original code, just add to the stop event a method that based on the sequence created by change fills up the intermediate values and voila, you have your sequence.

Let me know if this satisfy your requirements.

like image 21
Merak Marey Avatar answered Nov 15 '22 10:11

Merak Marey