Search code examples
javascriptjquerydrag-and-drop

Drag&Drop feature in jquery drops element at seemingly random position


I got some problem with my code but I simply cannot put my finger on what would be at fault in that situation I am lost.

So the situation is as follows, I am making a simple drag and drop interface by using jquery, all it does is: Make a grid, add/remove "pawns" depending on a certain table in the same page and drag&drop whatever pawn you want in whatever square you wish to. Everything works as expected but almost always when I try to drop the pawn on a square it teleports in a random position, rarely it does drop where it is expected to. I am not sure it is relevant but I am using laravel 10 and this is a blade file, Any suggestions I could try?

<div class="col-md-3 gridframe">
            <h3 class="text-center">Character Grid</h3>
            <div id="grid" class="row mx-auto">
            </div>
        </div>

<style>
    .gridframe {
        width: 300px;
        border: 1px solid black;
        overflow: hidden; 
    }

    .grid-cell {
        width: 50px;
        height: 50px;
        border: 1px solid black;
        text-align: center;
        position: relative;
        float: left;
    }

    .character {
        width: 30px;
        height: 30px;
        background-color: black;
        color: white;
        border-radius: 50%;
        display: flex;
        justify-content: center;
        align-items: center;
        z-index: 999;
        position: absolute;
        top: 10px;
        left: 10px;
    }

</style>

</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<script>
   function insertCharacter(nickname, hp) {
        var cells = $('.grid-cell');
        cells.each(function() {
            if (!$(this).children('.character').length) {
                var row = $(this).data('row');
                var col = $(this).data('col');
                $(this).append('<div class="character" data-hp="' + hp + '">' + nickname + '<br>' + hp + '</div>');
                return false; 
            }
        });

        // make characters draggable 
        $('.character').draggable({
            revert: 'invalid',
            zIndex: 100,
            start: function(event, ui) {
                $(this).css('cursor', 'pointer');
            },
            stop: function(event, ui) {
                $(this).css('cursor', 'auto');
            }
        });

        // make grid cells droppable
        $('.grid-cell').droppable({
            accept: '.character',
            drop: function(event, ui) {
                var character = ui.draggable;
                // var characterHP = character.data('hp');
                character.appendTo($(this)); 
            }
        });
    }

    $(document).ready(function() {
        $('#character-table tbody').on('DOMNodeInserted', 'tr', function() {
            var nickname = $(this).find('td:eq(0)').text();
            var hp = $(this).find('td:eq(1)').text();
            insertCharacter(nickname, hp);
        });

        $('#character-table tbody').on('DOMNodeRemoved', 'tr', function() {
            var nickname = $(this).find('td:eq(0)').text();
            $('.character').each(function() {
                if ($(this).text().includes(nickname)) {
                    $(this).remove();
                }
            });
        });

        var gridSize = 10; 
        var grid = $('#grid');
        for (var i = 0; i < gridSize; i++) {
            for (var j = 0; j < gridSize; j++) {
                $('<div class="grid-cell" data-row="' + i + '" data-col="' + j + '"></div>').appendTo(grid);
            }
        }
    });
    </script>```

Solution

  • In my experience, getting draggable and droppable to work together has the same inconsistencies you mention. Unless they are required, I would suggest trying sortable instead. Sortable functions very similar to draggable + droppable and allows you to link multiple containers using the connectWith property.

    Here is a simple example:

    let gridSize = 5,
        characters = [
          { name: 'Frodo', hp: 100, type: 'hobbit' },
          { name: 'Data', hp: 105, type: 'android' },
          { name: 'Spiderman', hp: 62, type: 'human' }
        ];
    
    $('.grid')
      .css(`grid-template-columns`, `repeat(${gridSize}, 4rem)`)
      .html([...Array(gridSize**2)].map(() => '<div class="cell"></div>').join(''));
    
    $('.characters')
      .html(characters.map((character) =>
        `<div class="character"><div class="stats"><p>${character.name}</p><p>hp: ${character.hp}</p></div></div>`).join(``))
      .add('.grid .cell')
      .sortable({
        connectWith: '.characters, .grid .cell',
        cursor: 'grabbing',
        receive: (e, ui) => {
          if(ui.item.parent().hasClass('cell')) {
            let $cell = $(e.target),
                $character = ui.item,
                $characters = $cell.find('.character').not($character),
                coords = [$cell.index() % gridSize, Math.floor($cell.index() / gridSize)];
            //do stuff
          }
        }
      });
    .grid{
      border: 2px solid gray;
      display: grid;
      width: fit-content;}
      
    .grid .cell{
      align-items: center;
      aspect-ratio: 1 / 1;
      border: 1px solid lightgray;
      display: flex;
      justify-content: center;
      overflow: visible;}
      
    .grid .cell:has(.character):hover{
      border-width: 2px;
      box-sizing: border-box;}
      
    .characters{
      border: 2px solid gray;
      display: flex;
      gap: 1rem;
      margin-top: 1rem;
      min-height: 3rem;
      min-width: 6rem;
      padding: 1rem;
      width: fit-content;}
     
    .character{
      align-items: end;
      background-color: #000000;
      border-radius: 50%;
      color: #ffffff;
      display: flex;
      height: 3rem;
      justify-content: center;
      text-align: center;
      width: 3rem;}
      
    .character .stats{
      background-color: inherit;
      padding: 4px;}
    
    .character p{
      font-size: .66rem;
      margin: 0;
      white-space: nowrap;}
    <div class="grid"></div>
    <div class="characters"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>