Сложные валидации

Shopify Draggable — это мощная библиотека для реализации drag-and-drop интерфейсов на веб-страницах. Она предоставляет высокоуровневый API для управления перетаскиваемыми элементами, контейнерами и событиями, но для построения сложной логики перемещения требуется грамотная реализация валидации.

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


Конфигурация Draggable с правилами валидации

Для начала необходимо создать экземпляр Draggable с указанием контейнеров и элементов:

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

const draggable = new Draggable(document.querySelectorAll('.container'), {
  draggable: '.item',
  delay: 200, // задержка перед началом перетаскивания
  mirror: {
    constrainDimensions: true, // зеркальное отображение с ограничением размеров
  },
});

Ключевой момент: библиотека поддерживает систему событий и хук containers, но сама по себе не накладывает строгие ограничения на перемещение между контейнерами. Валидация реализуется через события drag:start, drag:over, drag:move и drag:stop.


Использование событий для сложной валидации

drag:start

Это событие вызывается при начале перетаскивания. Здесь можно отклонять элементы, которые нельзя перемещать:

draggable.on('drag:start', (event) => {
  if (event.source.dataset.locked === 'true') {
    event.cancel(); // отмена перетаскивания заблокированного элемента
  }
});

Важно: event.cancel() полностью прекращает текущее действие, предотвращая создание зеркального элемента и перемещение.


drag:over

Событие drag:over срабатывает при наведении элемента на контейнер. Используется для валидации допустимых зон падения:

draggable.on('drag:over', (event) => {
  const allowedZones = ['zone-1', 'zone-2'];
  if (!allowedZones.includes(event.over.dataset.zone)) {
    event.cancel(); // элемент не может быть перемещен сюда
  }
});

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


drag:move

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

draggable.on('drag:move', (event) => {
  const rect = event.source.getBoundingClientRect();
  if (rect.top < 0 || rect.bottom > window.innerHeight) {
    event.cancel(); // элемент не выходит за границы экрана
  }
});

Совет: drag:move можно использовать для ограничения перемещения по оси X или Y, проверки пересечений и реализации правил сетки (grid snapping).


Валидаторы и кастомная логика

Для комплексных систем лучше вынести правила в отдельные функции:

function canDrag(source) {
  return source.dataset.locked !== 'true';
}

function canDrop(source, over) {
  const allowedPairs = {
    'item-type-a': ['zone-1'],
    'item-type-b': ['zone-2', 'zone-3'],
  };
  return allowedPairs[source.dataset.type]?.includes(over.dataset.zone);
}

draggable.on('drag:start', (event) => {
  if (!canDrag(event.source)) event.cancel();
});

draggable.on('drag:over', (event) => {
  if (!canDrop(event.source, event.over)) event.cancel();
});

Такой подход делает код чистым и расширяемым, упрощает добавление новых правил валидации и уменьшает вероятность ошибок.


Валидация с учётом вложенности и сортировки

Draggable поддерживает вложенные контейнеры, поэтому иногда требуется проверять структуру дерева элементов:

draggable.on('drag:over', (event) => {
  if (event.over.contains(event.source)) {
    event.cancel(); // запрещаем помещать элемент внутрь самого себя
  }
});

При использовании sortable контейнеров важно проверять порядок элементов и запрещать некорректные перестановки:

draggable.on('sortable:sort', (event) => {
  const minIndex = 0;
  const maxIndex = 4;
  if (event.newIndex < minIndex || event.newIndex > maxIndex) {
    event.cancel(); // ограничение по индексу
  }
});

Валидация с асинхронной логикой

Иногда необходимо проверять правила через запрос к серверу, например, разрешение на перемещение товара между категориями:

draggable.on('drag:over', async (event) => {
  const response = await fetch(`/validate-move?item=${event.source.id}&target=${event.over.id}`);
  const { allowed } = await response.json();
  if (!allowed) event.cancel();
});

Особенность: event.cancel() можно вызывать внутри асинхронной функции, но следует учитывать задержку отображения зеркального элемента. Для улучшения UX стоит отображать индикатор недопустимого перемещения.


Отслеживание и логирование отклонённых действий

Для сложных интерфейсов важно ведь логировать все попытки некорректного перемещения:

draggable.on('drag:over', (event) => {
  if (!canDrop(event.source, event.over)) {
    console.warn(`Перемещение элемента ${event.source.id} в ${event.over.id} запрещено`);
    event.cancel();
  }
});

Это помогает анализировать поведение пользователей и улучшать правила валидации со временем.


Итоговые рекомендации по сложной валидации

  • Использовать drag:start для запрета перетаскивания отдельных элементов.
  • drag:over — основной инструмент для контроля допустимых зон падения.
  • drag:move — подходит для ограничения координат, сеток и пересечений.
  • Вынесение правил в отдельные функции повышает читаемость и расширяемость.
  • Проверки асинхронно через сервер позволяют синхронизировать логику с данными.
  • Логирование отклонённых перемещений упрощает отладку сложных интерфейсов.

Эти практики позволяют создавать интерактивные и защищённые drag-and-drop интерфейсы, полностью контролируя поведение элементов и обеспечивая соблюдение всех бизнес-правил.