Вложенные контейнеры

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

Основные принципы работы

Вложенные контейнеры позволяют:

  • Организовывать элементы в иерархическую структуру.
  • Ограничивать движения дочерних элементов в пределах родительского контейнера.
  • Обрабатывать события drag, resize и gesture на разных уровнях иерархии независимо.

Interact.js использует концепцию dropzones и draggable elements, где контейнеры могут быть одновременно зоной сброса для одного уровня и родителем для другого.

Настройка вложенных draggable элементов

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

interact('.draggable')
  .draggable({
    modifiers: [
      interact.modifiers.restrictRect({
        restriction: 'parent',
        endOnly: true
      })
    ],
    listeners: {
      move(event) {
        const target = event.target;
        target.style.transform = `translate(${event.pageX}px, ${event.pageY}px)`;
      }
    }
  });
  • modifiers.restrictRect обеспечивает ограничение движения внутри родительского контейнера.
  • restriction: 'parent' автоматически связывает границы с ближайшим контейнером-родителем.
  • endOnly: true предотвращает сдвиг до конца перетаскивания, сохраняя плавность интерфейса.

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

Dropzones внутри вложенных контейнеров

Dropzone — это элемент, в который можно «сбрасывать» другие элементы. Для вложенных контейнеров настройка выглядит следующим образом:

interact('.dropzone')
  .dropzone({
    accept: '.draggable',
    overlap: 0.5,
    ondropactivate(event) {
      event.target.classList.add('active-dropzone');
    },
    ondrop(event) {
      event.target.appendChild(event.relatedTarget);
      event.relatedTarget.style.transform = 'translate(0, 0)';
    },
    ondragleave(event) {
      event.target.classList.remove('active-dropzone');
    }
  });
  • accept задаёт, какие элементы могут быть сброшены.
  • overlap определяет минимальное перекрытие для срабатывания события drop.
  • ondrop позволяет переместить элемент внутрь нового контейнера, корректно сбрасывая предыдущие трансформации.

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

Управление событиями на разных уровнях

Interact.js позволяет фильтровать события по иерархии элементов, предотвращая конфликт между родительским и дочерним контейнерами. Например:

interact('.nested-draggable')
  .draggable({
    listeners: {
      move(event) {
        const target = event.target;
        let x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
        let y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
        target.style.transform = `translate(${x}px, ${y}px)`;
        target.setAttribute('data-x', x);
        target.setAttribute('data-y', y);
      }
    }
  });
  • Использование data-x и data-y позволяет хранить абсолютные координаты элемента относительно родителя.
  • Это особенно важно для вложенных элементов, где простое translate по event.pageX/Y может привести к смещению за пределы родительского контейнера.

Масштабирование и вложенные контейнеры

Interact.js поддерживает масштабирование (resize) элементов с сохранением ограничений родительских контейнеров:

interact('.resizable')
  .resizable({
    edges: { left: true, right: true, bottom: true, top: true },
    modifiers: [
      interact.modifiers.restrictEdges({
        outer: 'parent',
      }),
      interact.modifiers.restrictSize({
        min: { width: 50, height: 50 },
        max: { width: 500, height: 500 }
      })
    ],
    listeners: {
      move(event) {
        let { x, y } = event.target.dataset;
        x = parseFloat(x) || 0;
        y = parseFloat(y) || 0;

        Object.assign(event.target.style, {
          width: `${event.rect.width}px`,
          height: `${event.rect.height}px`,
          transform: `translate(${x}px, ${y}px)`
        });
      }
    }
  });
  • restrictEdges гарантирует, что элемент не выйдет за пределы родителя.
  • restrictSize предотвращает чрезмерное увеличение или уменьшение.
  • Совместное использование drag и resize требует аккуратного управления координатами через dataset, чтобы изменения одного взаимодействия не ломали другое.

Практические рекомендации

  • Всегда явно указывать родительский контейнер в restriction и restrictEdges.
  • Использовать отдельные классы для разных уровней вложенности.
  • Хранить координаты через атрибуты data-x/data-y или через state, чтобы избежать накопления ошибок трансформации.
  • Обрабатывать события drop и drag отдельно для каждого уровня, чтобы избежать конфликта событий.

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