Перейти к содержанию

Drag&Drop

  • dataTransfer - объект класса DataTransfer, который нужен чтоб хранить перетаскиваемые данные. Сквозное хранилище
    • dataTransfer.effectAllowed - поддерживаемые эффекты (copy, move, ...) Drag'n'Drop. Навешивается на источнике в dragstart
    • dataTransfer.dropEffect - используемый эффект (copy, move, ...) Drag'n'Drop. Навешивается на цели в dragover. Меняет вид указателя

Механизм

  • На перетаскиваемый элемент надо добавит атрибут draggable="true"
  • На перетаскиваемый элемент навесить dragstart, прописать данные в DataTransfer
  • На те ноды, куда можно бросить, навесить dragover, вызвать event.preventDefault() чтоб drop начал срабатывать
  • На ноду, на которую можно бросить (target), навесить событие drop. В нём прописать логику успешности перетаскивания

Моменты

draggable

Не обязательно навешивать draggable="true" на весь элемент перетаскивания. Навешивание draggable отключит возможность выделять текст в элементе. Например, если навесить draggable="true" на строку tr, то пропадёт возможность выделять текст во всех ячейках этой строки. Лучше добавить дополнительную ячейку, которую пометить как перемещаемую, а в качестве перетаскиваемого изображения указать родительский tr

Пример
<table>
  <tr>
    <td draggable="true" class="draggable-cell"></td>
    <td>Содержимое ячейки</td>
  </tr>
</table>

<script>
  const dragStartHandler = event => {
    // установка изображения, которое показывается при перетаскивании
    const image = event.target.closest('tr');
    event.dataTransfer.setDragImage(image, 0, 0);
  }
  document.querySelectorAll('.draggable-cell')
    .forEach(item => item.addEventListener('dragstart', dragStartHandler));
</script>

dragend

Событие срабатывает в момент окончания перетаскивания. Причем неважно, успешно перетащили или нет. Если например в dragstart перетаскиваемый элемент положили в отдельную переменную, то в dragend эту переменную можно очистить.

dragover

Обработчик dragover по дефолту отменяет срабатывание drop. Если в dragover не вызвать event.preventDefault(), событие drop не сработает.

Пример
const dragOverHandler = event => {
  if (event) { // какие-то условия, при которых не надо давать перетаскивать
    return;
  }
  event.preventDefault();
  event.dataTransfer.dropEffect = 'move';
}

drop

Обработчик drop — это логика, которая происходит в тот момент, когда перетаскиваемый элемент успешно отпущен. Сюда можно прописать логику перемещения данных из одного массива в другой, или логику пересортировки массива.

Примеры

Перемещение данных из одного массива в другой на Vue2

<body>
<style>
  .main {
    width: 800px;
    margin: 0 auto;
  }

  .source {
    border: 1px solid coral;
    padding: 20px 5px;
  }

  .target {
    border: 1px solid darkorange;
    margin-top: 40px;
    padding: 20px 0;
  }

  .car__item {
    border: 1px solid tomato;
    padding: 15px;
    margin: 5px;
    box-sizing: border-box;
  }
</style>
<div class="main">
  <div id="app">
    <div class="source">
      <div
        v-for="car in cars"
        :key="'car_' + car.id"
        @dragstart="dragStartHandler($event, car)"
        draggable="true"
        class="car__item"
      >
        {{ car.id }}: {{ car.name }} ({{ car.city }})
      </div>
    </div>

    <div
      @dragover="dragOverHandler"
      @drop="dropHandler"
      class="target"
    >
      <div v-for="car in movedCars" :key="'moved_car_' + car.id" class="car__item">
        {{ car.id }}: {{ car.title }} ({{ car.city }})
      </div>
    </div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
  new Vue({
    el: '#app',
    data () {
      return {
        cars: [
          { id: 1, title: 'Lada',   city: 'Russia' },
          { id: 2, title: 'Skoda',  city: 'Czech' },
          { id: 3, title: 'Audi',   city: 'Germany' },
          { id: 4, title: 'Toyota', city: 'Japan' },
        ],
        movedCars: [],
      }
    },

    methods: {
      dragStartHandler (event, car) {
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData('carId', car.id);
      },

      dragOverHandler (event) {
        event.preventDefault();
        event.dataTransfer.dropEffect = 'move';
      },

      dropHandler (event) {
        event.preventDefault();
        const carId = event.dataTransfer.getData('carId');
        this.switchCar(carId);
      },

      switchCar (carId) {
        const carIndex = this.cars.findIndex(item => item.id == carId);
        const car = this.cars.splice(carIndex, 1)[0];
        this.movedCars.push(car);
      }
    },
  });
</script>
</body>

Пересортировка списка

Полезная статья на htmlacademy

Основная идея — каждый элемент списка пометить как возможный для drop. В событии drop смотрим, на какой элемент бросили. Получая его индекс — получим место в массиве, куда надо переместить запись. Если хотим перемещать не только после, но и перед, то надо получить координаты, смотреть, в нижней или верхней части элемента сработал drop.

<body>
<style>
  .main {
    width: 800px;
    margin: 0 auto;
  }
  .source {
    border: 1px solid coral;
    padding: 20px 5px;
  }
  .car__item {
    border: 1px solid tomato;
    padding: 15px;
    margin: 5px;
    box-sizing: border-box;
  }
</style>
<div class="main">
  <div id="app">
    <div class="source">
      <div
        v-for="car in cars"
        :key="'car_' + car.id"
        @dragover="dragOverHandler($event, car)"
        @drop="dropHandler($event, car)"
        class="car__item"
      >
        <span
          draggable="true"
          @dragstart="dragStartHandler($event, car)"
          @dragend="dragEndHandler"
        ></span>
        {{ car.id }}: {{ car.title }} ({{ car.city }})
      </div>
    </div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
  new Vue({
    el: '#app',
    data () {
      return {
        cars: [
          { id: 1, title: 'Lada',   city: 'Russia' },
          { id: 2, title: 'Skoda',  city: 'Czech' },
          { id: 3, title: 'Audi',   city: 'Germany' },
          { id: 4, title: 'Toyota', city: 'Japan' },
        ],
        draggableCar: null,
      }
    },

    methods: {
      dragStartHandler (event, car) {
        this.draggableCar = car;
        const image = event.target.closest('div');
        event.dataTransfer.setDragImage(image, 0, 0);
        event.dataTransfer.effectAllowed = 'move';
      },

      dragEndHandler () {
        this.draggableCar = null;
      },

      dragOverHandler (event, car) {
        if (car === this.draggableCar) {
          // бросать на себя не имеет смысла
          return;
        }
        event.preventDefault();
        event.dataTransfer.dropEffect = 'move';
      },

      dropHandler (event, car) {
        event.preventDefault();
        // приземлять перед или после элемента в зависимости от того, на какой части элемента отпустили
        const domRect = event.target.getBoundingClientRect();
        const offset = event.clientY < (domRect.y + (domRect.height / 2)) ? 0 : 1;
        this.cars.splice(this.cars.indexOf(this.draggableCar), 1);
        const targetIndex = this.cars.indexOf(car);
        this.cars.splice(targetIndex + offset, 0, this.draggableCar);
      },
    },
  });
</script>
</body>