Вложенные droppable области

Draggable от Shopify предоставляет мощный инструмент для организации интерактивных перетаскиваемых элементов, и поддержка вложенных droppable областей является одним из наиболее гибких и продвинутых возможностей библиотеки. Вложенные области позволяют создавать сложные иерархические структуры, где один элемент может быть перемещен не только в отдельную область, но и в область внутри другой droppable контейнерной структуры.


Основные концепции вложенных droppable областей

  1. Droppable контейнеры Каждый droppable контейнер определяется как отдельная область, в которую можно помещать draggable элементы. Вложенность достигается созданием одного контейнера внутри другого. Например:
const droppableOuter = new Draggable.Droppable(document.querySelectorAll('.outer-container'), {
  draggable: '.draggable-item'
});

const droppableInner = new Draggable.Droppable(document.querySelectorAll('.inner-container'), {
  draggable: '.draggable-item'
});

Здесь .inner-container находится внутри .outer-container, что создаёт вложенную структуру.

  1. Определение приоритетов Когда элемент перетаскивается над несколькими droppable областями, библиотека определяет активный контейнер, исходя из приоритета вложенности: элемент сначала проверяется на попадание в внутренний контейнер, затем во внешний.

  2. События на уровне контейнеров Для управления поведением вложенных областей используются стандартные события Draggable:

    • drag:start
    • drag:move
    • drag:over
    • drag:out
    • drag:stop

    Вложенные контейнеры позволяют подписываться на эти события независимо друг от друга, что дает возможность реализовать различное поведение для внутреннего и внешнего droppable.


Практическая организация вложенных областей

HTML структура:

<div class="outer-container">
  <div class="inner-container">
    <div class="draggable-item">Элемент 1</div>
    <div class="draggable-item">Элемент 2</div>
  </div>
  <div class="draggable-item">Элемент 3</div>
</div>

Инициализация Draggable:

import { Draggable } from '@shopify/draggable';

const draggable = new Draggable.Sortable(document.querySelectorAll('.outer-container'), {
  draggable: '.draggable-item',
  mirror: {
    constrainDimensions: true
  }
});

draggable.on('drag:start', (evt) => {
  console.log('Начало перетаскивания:', evt.source);
});

draggable.on('drag:over', (evt) => {
  console.log('Элемент над областью:', evt.over);
});

draggable.on('drag:stop', (evt) => {
  console.log('Элемент отпущен:', evt.source);
});

В данном примере .inner-container автоматически определяется как droppable область, так как она содержит draggable элементы. Это позволяет элементам перемещаться внутрь вложенного контейнера и обратно во внешний.


Управление конфликтами при пересечении областей

Вложенные droppable области могут создавать конфликтные ситуации, когда элемент одновременно находится над несколькими контейнерами. Для этого используется свойство overlapThreshold:

const droppableInner = new Draggable.Droppable(document.querySelectorAll('.inner-container'), {
  draggable: '.draggable-item',
  overlapThreshold: 0.5
});

overlapThreshold задаёт процент пересечения элемента с droppable областью, необходимый для того, чтобы она считалась активной. Вложенные контейнеры обычно используют более высокий порог для внутренних областей, чтобы предотвратить случайное срабатывание внешнего контейнера.


Динамическое добавление вложенных областей

Draggable поддерживает динамическое создание droppable контейнеров после инициализации. Это важно при работе с интерфейсами, где структура DOM может меняться:

const newInner = document.createElement('div');
newInner.classList.add('inner-container');
document.querySelector('.outer-container').appendChild(newInner);

const droppableNewInner = new Draggable.Droppable([newInner], {
  draggable: '.draggable-item'
});

Теперь newInner ведёт себя как полноценная вложенная droppable область без перезапуска основной Draggable и позволяет перемещать элементы как внутрь, так и наружу.


Контроль визуальной подсветки

Для вложенных droppable областей особенно важно различать внешний и внутренний контейнер визуально. Используются CSS классы draggable--over и draggable--over-inner (имя класса можно задать самостоятельно через опцию classes):

const droppableInner = new Draggable.Droppable(document.querySelectorAll('.inner-container'), {
  draggable: '.draggable-item',
  classes: {
    over: 'draggable--over-inner'
  }
});

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


События при вложенных перемещениях

Для точного управления вложенными droppable областями можно использовать цепочку событий:

draggable.on('drag:over', (evt) => {
  if(evt.over.classList.contains('inner-container')){
    console.log('Элемент над внутренней областью');
  } else {
    console.log('Элемент над внешней областью');
  }
});

draggable.on('drag:out', (evt) => {
  console.log('Элемент покинул область:', evt.over);
});

Это позволяет реализовать функционал, например: изменение состояния родителя, подсветку пути перемещения, динамическое открытие/закрытие вложенных областей и другие сложные сценарии UX.


Итоговые рекомендации

  • Всегда явно определять вложенные контейнеры как droppable, чтобы избежать конфликтов событий.
  • Использовать overlapThreshold для контроля срабатывания внутренних и внешних областей.
  • Применять разные CSS классы для визуального разделения уровней вложенности.
  • Подписываться на события drag:over и drag:out для управления динамическим поведением при вложенных перемещениях.
  • При необходимости динамически инициализировать новые внутренние контейнеры без перезапуска основной Draggable структуры.

Эти подходы позволяют создавать сложные интерактивные интерфейсы с полностью контролируемыми вложенными droppable областями, обеспечивая удобство пользователя и предсказуемость поведения перетаскиваемых элементов.