jqueryjquery-ui

jQuery UI sortable/draggable nesting example


I've been playing with jQuery UI sortable and draggable and I'm looking for a way to nest list items (only first and second level) depending on where they are dropped on the list.

Simple JSFiddle example of what I am trying to do: http://jsfiddle.net/picitelli/XZ4Tw/

$(".items").sortable({
    connectWith: ".items",
    placeholder: "placeholder",
    update: function(event, ui) {
        // update
    },
    sort: function(event, ui) {
        // sort
    }
});
$(".item").droppable({
    accept: ".item",
    hoverClass: "dragHover",
    drop: function( event, ui ) {
        console.log('drop');
        if (ui.position.left >= 20) {
            $(this).removeClass("first-level");
            $(this).addClass("second-level");
        } else {
            $(this).removeClass("second-level");
            $(this).addClass("first-level");
        }
    },
    over: function( event, ui ) {
        // over
    },
    activate: function( event, ui ) {
        // activate
    }
});

I want to be able to drag a list item and drop it before/after another list item, and depending on how far I am from the left (x) of the list, and how far I am from the bottom/top (y) of the list item I am dropping near, it will make that list item either a first and second level item. For instance, if I have a list item dragged on top of another, and I am closer to the bottom of that list item that I am hovering over, than if I was farther away from the bottom, if I drop it it will snap to the bottom of said list item and be a first-level item. If I was to move that list item further away and drop it, it would snap to the bottom and be a second-level item. The placeholder would update as well, invoking a class, just on hover, to show that the dropped list item would be either first or second level before it is actually dropped.

Per the fiddle above, in the drop method, I am checking to see how far the dragged list item is from the left. If it is greater than or equal to 20px, I am adding a "second-level" class that indents the list item. If it is less than 20px, I am adding a "first-level" class that keeps the list item flush left. Just for the left (x) calculation, this is working but it is not applying the classes to the dropped list item, it applies them to the list item that it has "jumped" over. Am I not invoking the correct method to perform this? Also, how would I go about performing such a calculation to determine where the list item would snap depending on how far (y) it is from the list item I am hovering over?

Any feedback/direction would be greatly appreciated.


Solution

  • Well I thought you question looked like a fun challenge.. And I had some idea of how to do it but was not sure.. So I tried it out. Hopefully this will be useful.. I'm not sure if it does everything your looking for so if something is missing let me know. Thanks.

    Here is an example that I came up with: Using the sortable events.

    CSS

    .placeholder-sub {
        background: #fff;
        border: 1px dashed #ccc;
        height: 50px;
        width: 480px;
        margin-left: 20px;
    }
    

    jQuery

    $(".items").sortable({
        connectWith: ".items",
        placeholder: "placeholder",
        update: function(event, ui) {
            // update
        },
        start: function(event, ui) {
            if(ui.helper.hasClass('second-level')){
                ui.placeholder.removeClass('placeholder');
                ui.placeholder.addClass('placeholder-sub');
            }
            else{ 
                ui.placeholder.removeClass('placeholder-sub');
                ui.placeholder.addClass('placeholder');
            }
        },
        sort: function(event, ui) {
            var pos;
            if(ui.helper.hasClass('second-level')){
                pos = ui.position.left+20; 
                $('#cursor').text(ui.position.left+20);
            }
            else{
                pos = ui.position.left; 
                $('#cursor').text(ui.position.left);    
            }
            if(pos >= 32 && !ui.helper.hasClass('second-level')){
                ui.placeholder.removeClass('placeholder');
                ui.placeholder.addClass('placeholder-sub');
                ui.helper.addClass('second-level');
            }
            else if(pos < 25 && ui.helper.hasClass('second-level')){
                ui.placeholder.removeClass('placeholder-sub');
                ui.placeholder.addClass('placeholder');
                ui.helper.removeClass('second-level');
            }
        }
    });
    

    $(".items").sortable({
      connectWith: ".items",
      placeholder: "placeholder",
      update: function(event, ui) {
        // update
      },
      start: function(event, ui) {
        if (ui.helper.hasClass('second-level')) {
          ui.placeholder.removeClass('placeholder');
          ui.placeholder.addClass('placeholder-sub');
        } else {
          ui.placeholder.removeClass('placeholder-sub');
          ui.placeholder.addClass('placeholder');
        }
      },
      sort: function(event, ui) {
        var pos;
        if (ui.helper.hasClass('second-level')) {
          pos = ui.position.left + 20;
          $('#cursor').text(ui.position.left + 20);
        } else {
          pos = ui.position.left;
          $('#cursor').text(ui.position.left);
        }
        if (pos >= 32 && !ui.helper.hasClass('second-level')) {
          ui.placeholder.removeClass('placeholder');
          ui.placeholder.addClass('placeholder-sub');
          ui.helper.addClass('second-level');
        } else if (pos < 25 && ui.helper.hasClass('second-level')) {
          ui.placeholder.removeClass('placeholder-sub');
          ui.placeholder.addClass('placeholder');
          ui.helper.removeClass('second-level');
        }
      }
    });
    $(".item").droppable({
      accept: ".item",
      hoverClass: "dragHover",
      drop: function(event, ui) {
        /*console.log('drop');
        if (ui.position.left >= 20) {
            $(this).removeClass("first-level");
            $(this).addClass("second-level");
        } else {
            $(this).removeClass("second-level");
            $(this).addClass("first-level");
        }*/
      },
      over: function(event, ui) {
        // over
      },
      activate: function(event, ui) {
        // activate
      }
    });
    .items {
      list-style: none outside none;
      margin-bottom: 20px;
      padding: 0;
      width: 480;
    }
    
    .item {
      background: #fff;
      border: 1px solid #ddd;
      cursor: move;
      height: 50px;
      line-height: 50px;
      padding: 0 10px;
      width: 480px;
    }
    
    .second-level {
      margin-left: 20px;
      width: 460px;
    }
    
    .dragHover {
      border-color: blue;
      color: blue;
    }
    
    .placeholder {
      background: #fff;
      border: 1px dashed #ccc;
      height: 50px;
      width: 500px;
    }
    
    .placeholder-sub {
      background: #fff;
      border: 1px dashed #ccc;
      height: 50px;
      width: 480px;
      margin-left: 20px;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.14.1/jquery-ui.min.js" integrity="sha512-MSOo1aY+3pXCOCdGAYoBZ6YGI0aragoQsg1mKKBHXCYPIWxamwOE7Drh+N5CPgGI5SA9IEKJiPjdfqWFWmZtRA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    
    
    <label>Cursor At:</label><label id="cursor"></label>
    
    <ul class="items">
      <li class="item first-level">Item #1</li>
      <li class="item second-level">Item #2</li>
      <li class="item second-level">Item #3</li>
      <li class="item first-level">Item #4</li>
      <li class="item first-level">Item #5</li>
    </ul>
    
    <ul class="items">
      <li class="item">Add new item</li>
    </ul>

    Fiddle