10

I'm using jQuery UI sortable to sort connected lists. The update event appears to be running twice.

Here is the full sortable call:

$(".pageContent").sortable({
    handle: ".quesText",
    connectWith: ".pageContent",
    containment: "section",
    start: function(e, ui){
        ui.placeholder.height(ui.item.height());
    },
    placeholder: "sortable-placeholder",
    opacity: 0.5,
    cursor: "move",
    cancel: "input, select, button, a, .openClose",
    update: function(e, ui){
        var thisItem = ui.item;
        var next = ui.item.next();
        var prev = ui.item.prev();

        var thisNumber = thisItem.find(".quesNumb");
        var nextNumber = next.find(".quesNumb");
        var prevNumber = prev.find(".quesNumb");

        var tn = parseInt(thisNumber.text());
        var nn = parseInt(nextNumber.text());
        var pn = parseInt(prevNumber.text());

        var quesNumbs = $(".quesNumb");

        var newItemId = thisItem.attr("id").replace(/_\d+$/, "_");

        //test if we are dragging top down
        if(ui.position.top > ui.originalPosition.top){
            quesNumbs.each(function(i){
                var thisVal = parseInt($(this).text());
                var grandparent = $(this).parent().parent();
                var grandId = grandparent.attr("id").replace(/_\d+$/, "_");
                if(thisVal > tn && (thisVal <= pn || thisVal <= (nn - 1))){
                    $(this).text(thisVal - 1 + ".");
                    grandparent.attr("id",grandId + (thisVal - 1));
                }
            });
            if(!isNaN(pn) || !isNaN(nn)){
                if(!isNaN(pn)){
                    //for some reason when there is a sender pn gets updated, so we check if sender exists
                    //only occurs sorting top down
                    if($.type(ui.sender) !== "null"){
                        var senderId = ui.sender.attr("id");
                        thisNumber.text(pn + 1 + ".");
                        thisItem.attr("id",senderId + "_" + (pn + 1));
                        alert(thisItem.attr("id"));
                    }
                    else {
                        thisNumber.text(pn + ".");
                        thisItem.attr("id",newItemId + pn);
                        alert(thisItem.attr("id"));
                    }
                }
                else {
                    thisNumber.text(nn - 1 + ".");
                }
            }
            else {
                   //something will happen here
            }
        }
        //otherwise we are dragging bottom up
        else {
            quesNumbs.each(function(i){
                var thisVal = parseInt($(this).text());
                if(thisVal < tn && (thisVal >= nn || thisVal >= (pn + 1))){
                    $(this).text(thisVal + 1 + ".");
                }
            });
            if(!isNaN(pn) || !isNaN(nn)){
                if(!isNaN(pn)){
                    thisNumber.text(pn + 1 + ".");
                }
                else {
                    thisNumber.text(nn + ".");
                }
            }
            else {
               //something will happen here
            }
        }
    }
});

Here is the part that seems to run twice:

                if($.type(ui.sender) !== "null"){
                    var senderId = ui.sender.attr("id");
                    thisNumber.text(pn + 1 + ".");
                    thisItem.attr("id",senderId + "_" + (pn + 1));
                    alert(thisItem.attr("id"));
                }
                else {
                    thisNumber.text(pn + ".");
                    thisItem.attr("id",newItemId + pn);
                    alert(thisItem.attr("id"));
                }

I am expecting to only get one alert as ui.sender is null when the sorting stays inside the same list. When an item leaves a list to go to another then ui.sender will no longer be null.

The problem is I will get 2 alert messages when I move an item to a new list. Its as if ui.sender is set after the update function runs once then it runs through the update function again. Obviously this is no good because I am overwriting data that should not necessarily be overwritten.

If that is the case how should I rewrite my code to avoid overwriting data?

EDIT

I believe the update event is called each time a list DOM is changed, not just the DOM in general. So for each list that has a DOM change the update runs. When I move one item to a new list I am updating two lists.

So I guess the new question is: how do I rewrite this code knowing it will fire two times? Is there a combination of Receive and Remove events that could achieve this?

Jake Zeitz
  • 2,429
  • 3
  • 23
  • 43

2 Answers2

47

I did some investigating into each sortable event. Here are my findings, listed in the order that they take place:

$(".pageContent").sortable({
    start: function(e,ui){
        //Before all other events
        //Only occurs once each time sorting begins
    },
    activate: function(e,ui){
        //After the start event and before all other events
        //Occurs once for each connected list each time sorting begins
    },
    change: function(e,ui){
        //After start/active but before over/sort/out events
        //Occurs every time the item position changes
        //Does not occur when item is outside of a connected list
    },
    over: function(e,ui){
        //After change but before sort/out events
        //Occurs while the item is hovering over a connected list
    },
    sort: function(e,ui){
        //After over but before out event
        //Occurs during sorting
        //Does not matter if the item is outside of a connected list or not
    },
    out: function(e,ui){
        //This one is unique
        //After sort event before all drop/ending events unless **see NOTE
        //Occurs, only once, the moment an item leaves a connected list
        //NOTE: Occurs again when the item is dropped/sorting stops 
        //--> EVEN IF the item never left the list
        //--> Called just before the stop event but after all other ending events
    },
    beforeStop: function(e,ui){
        //Before all other ending events: update,remove,receive,deactivate,stop
        //Occurs only once at the last possible moment before sorting stops
    },
    remove: function(e,ui){
        //After beforeStop and before all other ending events
        //Occurs only once when an item is removed from a list
    },
    receive: function(e,ui){
        //After remove and before all other ending events
        //Occurs only once when an item is added to a list
    },
    update: function(e,ui){
        //After receive and before all other ending events
        //Occurs when the DOM changes for each connected list
        //This can fire twice because two lists can change (remove from one
        //list but add to another)
    },
    deactivate: function(e,ui){
        //After all other events but before out (kinda) and stop
        //Occurs once for each connected list each time sorting ends
    },
    stop: function(e,ui){
        //After all other events
        //Occurs only once when sorting ends
    }
});

In solving my problem I just force the contents of my update function to only run once by wrapping it in an if else statement that checks for ui.sender. Basically it says if ui.sender does not exist then this is the first time through the update function and we should do the function, otherwise do nothing.

Jake Zeitz
  • 2,429
  • 3
  • 23
  • 43
  • 1
    Sorry but I think there is at least a glitch in the order of your events: the `stop` isn't the last event, it's the `update`. If I look at the [Dan Wellman's book «jQuery UI 1.8»](http://my.safaribooksonline.com/book/-/9781849516525/the-sortables-component/ch13lvl1sec03) as reference to back me up, it is clear that the `stop` event occurs at the sort ends while the `update` event occurs when the sort has already ended and the DOM has changed... So `update` would be happening after `stop` and that's quite logical that way, don't you think? – Cholesterol Jan 23 '14 at 16:09
  • 3
    @Cholesterol thanks for the input that does make more sense. But the information I ended up with here was through my testing of each event. I tested by calling console.log("whatever-this-event-is") when that event is fired. My findings were the update event sent out it's log before the stop event. – Jake Zeitz Jan 27 '14 at 04:23
  • 6
    The source code never lies: [sortable.js](https://github.com/jquery/jquery-ui/blob/master/ui/sortable.js#L1281) - the stop event is the last event – tokarev Apr 17 '14 at 13:50
  • 1
    Thank you so much , i tried found why this is happening and finally solved it with your suggested solution . – Den Pat Dec 16 '16 at 19:37
1

I have posted the answer here: jquery Sortable connectWith calls the update method twice

You can combine the remove and receive and create an array which will hold changes and than post it to the server as JSON.

Demo: http://jsfiddle.net/r2d3/p3J8z/

HTML:

<div class="container">
    <div class="step" id="step_1">
        <h2 class="title">Step 1</h2>
        <div class="image" id="image_10">Image 10</div>
        <div class="image" id="image_11">Image 11</div>
        <div class="image" id="image_12">Image 12</div>
    </div>
    <div class="step" id="step_2">
        <h2 class="title">Step 2</h2>
        <div class="image" id="image_21">Image 21</div>
        <div class="image" id="image_22">Image 22</div>
        <div class="image" id="image_23">Image 23</div>
    </div>

JS:

   $(function(){

        /* Here we will store all data */
        var myArguments = {};   

        function assembleData(object,arguments)
        {       
            var data = $(object).sortable('toArray'); // Get array data 
            var step_id = $(object).attr("id"); // Get step_id and we will use it as property name
            var arrayLength = data.length; // no need to explain

            /* Create step_id property if it does not exist */
            if(!arguments.hasOwnProperty(step_id)) 
            { 
                arguments[step_id] = new Array();
            }   

            /* Loop through all items */
            for (var i = 0; i < arrayLength; i++) 
            {
                var image_id = data[i]; 
                /* push all image_id onto property step_id (which is an array) */
                arguments[step_id].push(image_id);          
            }
            return arguments;
        }   

        /* Sort images */
        $('.step').sortable({
            connectWith: '.step',
            items : ':not(.title)',
            /* That's fired first */    
            start : function( event, ui ) {
                myArguments = {}; /* Reset the array*/  
            },      
            /* That's fired second */
            remove : function( event, ui ) {
                /* Get array of items in the list where we removed the item */          
                myArguments = assembleData(this,myArguments);
            },      
            /* That's fired thrird */       
            receive : function( event, ui ) {
                /* Get array of items where we added a new item */  
                myArguments = assembleData(this,myArguments);       
            },
            update: function(e,ui) {
                if (this === ui.item.parent()[0]) {
                     /* In case the change occures in the same container */ 
                     if (ui.sender == null) {
                        myArguments = assembleData(this,myArguments);       
                    } 
                }
            },      
            /* That's fired last */         
            stop : function( event, ui ) {                  
                /* Send JSON to the server */
                $("#result").html("Send JSON to the server:<pre>"+JSON.stringify(myArguments)+"</pre>");        
            },  
        });
    });

Full explanation of the solution: http://r2d2.cc/2014/07/22/jquery-sortable-connectwith-how-to-save-all-changes-to-the-database/

Community
  • 1
  • 1
Artur Kedzior
  • 3,994
  • 1
  • 36
  • 58