Search code examples
javascriptjquerydrag-and-dropdraggable

js Drag and Drop with snapping


In this example, the workplace consists of 26 tables. When the employee is dragged from the top box, I want him to snapped to the red dot and only on the red dot can be dropped. Can someone help, thanks...

const workplace = document.getElementById('workplace');
const employeesContainer = document.getElementById('employees-container');
// Layout based on the provided configuration
const columnWidth = 225;
const rowHeight = 125;

// Greate Office Tables
function createTable(x, y, number) {
  const table = document.createElement('div');
  table.className = 'table';
  table.setAttribute('draggable', false);
  table.style.left = `${x}px`;
  table.style.top = `${y}px`;
  table.textContent = number;

  if (number % 2 === 0) {
    const dots = ['right'];
    dots.forEach((position) => {
      const dot = document.createElement('div');
      dot.className = 'drop-dot';
      dot.dataset.position = position;
      table.appendChild(dot);
    });
  } else {
    const dots = ['left'];
    dots.forEach((position) => {
      const dot = document.createElement('div');
      dot.className = 'drop-dot';
      dot.dataset.position = position;
      table.appendChild(dot);
    });
  }


  workplace.appendChild(table);
}

//Create Employees
function createEmployee(x, y, firstName, lastName, initials) {
  const employee = document.createElement('span');
  employee.className = 'employee';
  employee.style.left = `${x}px`;
  employee.style.top = `${y}px`;
  employee.textContent = initials;
  employee.draggable = true;
  // Add drag start event listener
  employee.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', 'employee'); // Custom data for identification
    employee.classList.add('dragging');
  });

  workplace.appendChild(employee);
}

// Create Employee Avatar
function createEmployeeBox(firstName, lastName) {
  const employeeBox = document.createElement('div');
  employeeBox.className = 'employee-box';
  const initials = `${firstName.charAt(0).toUpperCase()}${lastName.charAt(0).toUpperCase()}`;
  employeeBox.textContent = initials;
  employeeBox.draggable = true;
  employeeBox.dataset.initials = initials; // Add custom data attribute

  // Add drag start event listener
  employeeBox.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', 'employee-box'); // Custom data for identification
    employeeBox.classList.add('dragging');
  });

  employeesContainer.appendChild(employeeBox);
}


// First row
for (let i = 0; i < 10; i += 2) {
  createTable(i * (columnWidth / 3), 0, i + 1);
  createTable((i + 1) * (columnWidth / 3) - 20, 0, i + 2); // Adjusted spacing
}
// Second row
for (let i = 4; i < 10; i += 2) {
  createTable(i * (columnWidth / 3), rowHeight, i + 7);
  createTable((i + 1) * (columnWidth / 3) - 20, rowHeight, i + 8); // Adjusted spacing
}
// Third row (same as the first)
for (let i = 0; i < 10; i += 2) {
  createTable(i * (columnWidth / 3), 2 * rowHeight, i + 17);
  createTable((i + 1) * (columnWidth / 3) - 20, 2 * rowHeight, i + 18); // Adjusted spacing
}



// Add employees
createEmployeeBox('sofia', 'Dhomas');
createEmployeeBox('Cofia', 'Whomas');
createEmployeeBox('Mofia', 'Lhomas');
createEmployeeBox('Cofia', 'Nhomas');

function createEmployee(x, y, firstName, lastName, initials) {
  const employee = document.createElement('span');
  employee.className = 'employee';
  employee.style.left = `${x}px`;
  employee.style.top = `${y}px`;
  employee.textContent = initials;
  employee.draggable = true;
  // Add drag start event listener
  employee.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', 'employee'); // Custom data for identification
    employee.classList.add('dragging');
  });
  workplace.appendChild(employee);
}


// Event listeners for drag-and-drop
workplace.addEventListener('dragover', (event) => {
  event.preventDefault();
});

workplace.addEventListener('drop', (event) => {
  event.preventDefault();
  const dataType = event.dataTransfer.getData('text/plain');
  const offsetX = event.clientX - workplace.getBoundingClientRect().left;
  const offsetY = event.clientY - workplace.getBoundingClientRect().top;
  if (dataType === 'employee') {
    const employee = document.querySelector('.employee.dragging');
    if (employee) {
      console.log('employee')
      const nearestTable = findNearestTable(offsetX, offsetY);
      if (nearestTable) {
        console.log('nearestTable')
        const tableRect = nearestTable.getBoundingClientRect();
        const snappedX = tableRect.left + tableRect.width / 2 - employee.clientWidth / 2;
        const snappedY = tableRect.top + tableRect.height / 2 - employee.clientHeight / 2;
        // Remove the corresponding employee box
        const initials = employee.textContent;
        const employeeBox = document.querySelector(`.employee-box[data-initials='${initials}']`);
        if (employeeBox) {
          employeesContainer.removeChild(employeeBox);
        }
        employee.style.left = `${snappedX}px`;
        employee.style.top = `${snappedY}px`;
        employee.classList.remove('dragging');
      }

      // Move employee to new position
      //employee.style.left = `${offsetX - 12.5}px`; // Adjust for centering
      //employee.style.top = `${offsetY - 12.5}px`; // Adjust for centering

    }
  } else if (dataType === 'employee-box') {
    console.log('employee-box')
    // Remove the dragged employee box
    const nearestTable = findNearestTable(offsetX, offsetY);
    if (nearestTable) {
      console.log('nearestTable');
    }

    const employeeBox = document.querySelector('.employee-box.dragging');
    if (employeeBox) {
      employeesContainer.removeChild(employeeBox);
    }
    // Create new employee at the dropped position
    const initials = employeeBox.textContent;
    createEmployee(offsetX - 12.5, offsetY - 12.5, 'New', 'Employee', initials);
  }
}); // Orginal


// Double-click to return employee to the employee box
workplace.addEventListener('dblclick', (event) => {
  if (event.target.classList.contains('employee')) {
    const employee = event.target;
    const initials = employee.textContent;

    // Remove the employee from the workplace
    workplace.removeChild(employee);

    // Create a new employee box with the correct initials
    createEmployeeBox(initials.charAt(0), initials.charAt(1));
  }
});


// FN
// Function to find the nearest table
function findNearestTable(x, y) {
  const tables = document.querySelectorAll('.table');
  let nearestTable = null;
  let minDistance = Infinity;

  tables.forEach((table) => {
    const tableRect = table.getBoundingClientRect();
    console.log(tableRect)
    const distance = Math.sqrt((x - tableRect.left - tableRect.width / 2) ** 2 +
      (y - tableRect.top - tableRect.height / 2) ** 2);

    if (distance < minDistance) {
      console.log('tableRect')
      minDistance = distance;
      nearestTable = table;
    }
  });

  return nearestTable;
}
body {
  font-family: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
  font-size: 0.9375rem;
  font-wiight: 400;
  line-height: 1.47;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.workplace {
  position: relative;
  width: 740px;
  height: 375px;
  border: 2px solid #cfd3ec;
  margin: 20px;
  background-color: #434968;
  border-radius: 0.5rem;
}

.table {
  position: absolute;
  width: 40px;
  height: 80px;
  background-color: #424659;
  border: 2px solid #cfd3ec;
  border-radius: 0.2rem;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 20px;
  color: #cfd3ec;
}

.table.highlighted-table {
  border: 2px solid yellow;
  /* Change the color as needed */
}

.employee {
  position: absolute;
  width: 25px;
  height: 25px;
  border: 1px solid black;
  border-radius: 50%;
  background-color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 500;
  cursor: move;
  user-select: none;
}

.employees-container {
  display: flex;
  width: 740px;
  height: 100px;
  background-color: #434968;
  border: 2px solid #cfd3ec;
  border-radius: 0.3rem;
  box-sizing: border-box;
  align-items: center;
  margin-top: 20px;
  flex-wrap: wrap;
}

.employee-box {
  width: 25px;
  height: 25px;
  border: 1px solid #cfd3ec;
  border-radius: 50%;
  background-color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 500;
  cursor: move;
  user-select: none;
  margin: .5rem;
}

.drop-dot {
  position: absolute;
  width: 10px;
  height: 10px;
  background-color: red;
  border-radius: 50%;
  cursor: pointer;
}

.drop-dot[data-position="left"] {
  top: 50%;
  left: -5px;
  transform: translateY(-50%);
}

.drop-dot[data-position="right"] {
  top: 50%;
  right: -5px;
  transform: translateY(-50%);
}
<div class="employees-container" id="employees-container"></div>
<div class="workplace" id="workplace"></div>


Solution

  • On dragstart you need information about what employee is being dragged. On dragover you can change the style of the "drop zone" by setting a className. On the drop event the dragged element must be moved/appended to the new drop zone. And then the dragged element must be styled so that is in place.

    The already object is used for testing if an employee is already at the table.

    const workplace = document.getElementById('workplace');
    const employeesContainer = document.getElementById('employees-container');
    
    employeesContainer.addEventListener('dragstart', e => {
      e.dataTransfer.setData('text/plain', e.target.dataset.initials);
    });
    
    workplace.addEventListener('dragstart', e => {
      e.dataTransfer.setData('text/plain', e.target.dataset.initials);
    });
    
    workplace.addEventListener('dragover', e => {
      e.preventDefault();
      workplace.querySelectorAll('.drop-dot').forEach(dot => dot.classList.remove('over'));
      let table = e.target.closest('.table');
      if (table) {
        let already = table.querySelector('.employee-box');
        let dot = table.querySelector('.drop-dot');
        if (dot && !already) {
          dot.classList.add('over');
        }
      }
    });
    
    workplace.addEventListener('drop', e => {
      e.preventDefault();
      let table = e.target.closest('.table');
      if (table) {
        let already = table.querySelector('.employee-box');
        if (!already) {
          let dot = table.querySelector('.drop-dot');
          let initials = e.dataTransfer.getData('text/plain');
          let employee = document.querySelector(`div[data-initials="${initials}"]`);
          employee.classList.add(dot.dataset.position);
          table.appendChild(employee);
        }
      }
      workplace.querySelectorAll('.drop-dot').forEach(dot => dot.classList.remove('over'));
    });
    body {
      font-family: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
      font-size: 0.9375rem;
      font-wiight: 400;
      line-height: 1.47;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    
    .workplace {
      position: relative;
      width: 740px;
      height: 375px;
      border: 2px solid #cfd3ec;
      margin: 20px;
      background-color: #434968;
      border-radius: 0.5rem;
    }
    
    .table {
      position: absolute;
      width: 40px;
      height: 80px;
      background-color: #424659;
      border: 2px solid #cfd3ec;
      border-radius: 0.2rem;
      box-sizing: border-box;
      display: flex;
      align-items: center;
      justify-content: center;
      margin: 20px;
      color: #cfd3ec;
    }
    
    .table.highlighted-table {
      border: 2px solid yellow;
      /* Change the color as needed */
    }
    
    .employee {
      position: absolute;
      width: 25px;
      height: 25px;
      border: 1px solid black;
      border-radius: 50%;
      background-color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: 500;
      cursor: move;
      user-select: none;
    }
    
    .employees-container {
      display: flex;
      width: 740px;
      height: 100px;
      background-color: #434968;
      border: 2px solid #cfd3ec;
      border-radius: 0.3rem;
      box-sizing: border-box;
      align-items: center;
      margin-top: 20px;
      flex-wrap: wrap;
    }
    
    .employee-box {
      width: 25px;
      height: 25px;
      border: 1px solid #cfd3ec;
      border-radius: 50%;
      background-color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: 500;
      cursor: move;
      user-select: none;
      margin: .5rem;
    }
    
    .drop-dot {
      position: absolute;
      width: 10px;
      height: 10px;
      background-color: red;
      border-radius: 50%;
      cursor: pointer;
    }
    
    .drop-dot.over {
      width: 30px;
      height: 30px;
    }
    
    .drop-dot[data-position="left"] {
      top: 50%;
      left: -5px;
      transform: translateY(-50%);
    }
    
    .drop-dot[data-position="right"] {
      top: 50%;
      right: -5px;
      transform: translateY(-50%);
    }
    
    .table .employee-box.right {
      position: absolute;
      right: -25px;
    }
    
    .table .employee-box.left {
      position: absolute;
      left: -25px;
    }
    <div class="employees-container" id="employees-container">
      <div class="employee-box" draggable="true" data-initials="SD">SD</div>
      <div class="employee-box" draggable="true" data-initials="CW">CW</div>
      <div class="employee-box" draggable="true" data-initials="ML">ML</div>
      <div class="employee-box" draggable="true" data-initials="CN">CN</div>
    </div>
    <div class="workplace" id="workplace">
      <div class="table" style="left: 0px; top: 0px;">1
        <div class="drop-dot" data-position="left"></div>
      </div>
      <div class="table" style="left: 55px; top: 0px;">2
        <div class="drop-dot" data-position="right"></div>
      </div>
      <div class="table" style="left: 150px; top: 0px;">3
        <div class="drop-dot" data-position="left"></div>
      </div>
      <div class="table" style="left: 205px; top: 0px;">4
        <div class="drop-dot" data-position="right"></div>
      </div>
      <div class="table" style="left: 300px; top: 0px;">5
        <div class="drop-dot" data-position="left"></div>
      </div>
      <div class="table" style="left: 355px; top: 0px;">6
        <div class="drop-dot" data-position="right"></div>
      </div>
    </div>