Skip to content

Начало работы (V3) ​

Установка ​

Введите в своем терминале:

sh
# npm
npm i effdnd

# pnpm
pnpm add effdnd

# yarn
yarn add effdnd

Быстрый старт ​

Вкратце, effdnd использует два пользовательских веб-компонента:

  • effdnd-trigger инициирует drag-and-drop,
  • effdnd-actor обозначает области верстки, которые будут участвовать в процессе drag-and-drop (играть свои роли).

Веб компонент effdnd-actor может "играть" несколько ролей:

  • item - перемещаемый элемент,
  • target - цель перемещения,
  • scope - границы перемещения.

Для задания роли нужно установить одноименнный атрибут на элемент effdnd-actor. На один элемент можно установить несколько ролей.

В начале drag-and-drop веб-компонент effdnd-trigger с помощью своих атрибутов выбирает участников процесса (актеров):

  • атрибут item позволяет выбрать перемещаемый элемент. Если атрибут не задан, будет использоваться ближайший по дереву <effdnd-actor item='...'> с заданным атрибутом item. Если задан атрибут со значением # (<effdnd-trigger item='#'>) или в DOM-дереве выше нет ни одного <effdnd-actor item='...'>, то перемещаться будет сами триггер;
  • атрибут scope позволяет выбрать доступную область перемещения. Если атрибут не задан, будет использоваться ближайший по дереву <effdnd-actor scope='...'> с заданным атрибутом scope. Если задан атрибут со значением # (<effdnd-trigger scope='#'>) или в DOM-дереве выше нет ни одного <effdnd-actor scope='...'>, то областью перемещения будет считаться document.body;
  • атрибут target позволяет выбрать доступные цели перемещения. Цели перемещения генерируют специальные события, когда drag-and-drop заканчивается над ними. Целями перемещения будут считаться все находящиеся внутри области scope элементы <effdnd-actor target='...'>, названия которых начинаются со значения атрибута target у триггера. Это значит, что если атрибут не задан, то доступны все <effdnd-actor target='...'> внутри текщуго <effdnd-actor scope='...'>.

Чтобы определить оба веб-компонента, просто вызовите функицю useDnD, а результаты вызова используйте прослушивания событий:

jsx
import { useRef } from 'react';
import { useDnD } from 'effdnd';

const { observe } = useDnD();

export const Component = () => {
    const ref = useRef();
    useEffect(() => {
      // вы можете подписаться на Drag-and-Drop события
      const unobserve = observe((e) => {
        // здесь идет обработка события
      }, ref.current);
      // и вы можете отписаться
      return () => unobserve();
    });
    return <effdnd-actor scope='top' ref={ref} >
        <div className="targets-wrapper">
            <effdnd-actor target='target-1'>...</effdnd-actor>
            <effdnd-actor target='target-2'>...</effdnd-actor>
        </div>
        <div id="items-wrapper">
            <effdnd-actor item='item-1'>
                <effdnd-trigger>Trigger #1</effdnd-trigger>
            </effdnd-actor>
            <effdnd-actor item='item-2'>
                <effdnd-trigger>Trigger #2</effdnd-trigger>
            </effdnd-actor>
        </div>
    </effdnd-actor>;
}

Активные и пассивные элементы ​

Во время drag-and-drop effdnd-trigger устанавливает динамический атрибут state на выбранных актеров effdnd-actor. То есть в самом начале триггер определяет, что перемещается (item), где может перемещаться (scope) и куда следует перемещать (target). Элементы могут быть

  • без состояния,
  • в пассивном состоянии (state="passive"),
  • в активном состоянии (state="active").

Элемент с ролью item активен во время его перемещения. Кроме того элемент с ролью item может быть в состоянии клонирован (state="cloned"), если активен его клон. Для клонирования элемента включите режим item-mode="cloned".

Элемент с ролью scope пассивен, когда внутри него перемещается item. Это состояние очерчивает границы области перемещения.

Элемент с ролью scope становится активным, если находится в пассивном состоянии и item пытается выйти за его пределы. Это состояние показывает пользователю, что курсор переместился за допустимые границы.

Элемент с ролью target пассивен, когда он находится внутри scope с активным или пассивным состоянием. Это состояние показывает, куда можно переместить item.

Элемент с ролью target становится активным, если находится в пассивном состоянии и курсор с item находится над ним. Это состояние показывает, что target готовит принять item.

Режимы взаимодействия ролей ​

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

  • item-mode определяет режим item, может устанавливать следующие значения:
    • item-mode="keep" - элемент item будет сохранять свои нове координаты после перемещения;
    • item-mode="clone" - для элемента item будет создан клон, который и будет перемещаться;
  • target-mode определяет режим target после его выбора и может устанавливать следующие значения:
    • target-mode="prepend" - элемент item будет добавлен в target в качестве первого дочернего элемента;
    • target-mode="append" - элемент item будет добавлен в target в качестве последнего дочернего элемента;
    • target-mode="remove" - элемент item будет удален из DOM-дерева;
  • scope-mode определяет режим scope в ходе drag-and-drop и может устанавливать следующие значения:
    • scope-mode="order-x" - элемент scope позволяет переставлять элементы item вдоль оси x;
    • scope-mode="order-y" - элемент scope позволяет переставлять элементы item вдоль оси y.

Стили ​

Внешний вид <effdnd-actor item='...'> во время перемещения устанавливается триггером с помощью атрибутов. Также с помощью атрибутов можно задать параметры анимации, ось перемещения и минимальную дистанцию срабатывания.

ts
/export interface ITriggerAttrs {
    /**
     * Item name
     * @description
     * If the value is "#" then it will use `effnd-trigger` as item
     */
    item?: string;
    /**
     * Bounding scope name
     * @description
     * If the value is "#" then it will use `document.body` as scope
     */
    scope?: string;
    /**
     * Target name or its prefix
     */
    target?: string;
    /**
     * Triggering distance
     */
    dist?: string;
    /**
     * Triggering mouse button
     * @description
     * Any mouse button if not specified
     */
    btn?: '0' | '1' | '2';
    /**
     * DnD axis
     */
    axis?: string;
    /**
     * Triggering event
     * @description
     * Both 'touch' and 'mouse' by default
     */
    event?: 'touch' | 'mouse';
    /**
     * Transition duration
     */
    dur?: string;
    /**
     * Transition delay
     */
    del?: string;
    /**
     * Transition timing-function
     */
    tf?: string;
}

По уомлчанию внутри пустого триггера будет использоваться SVG иконка перемещения, вид которой зависит от атрибута axis. Чтобы скрыть иконку используйте свой контент внутри effdnd-trigger.

Веб-компонент effdnd-actor уже содержит начальные стили для пассивного и активного состояний своих ролей, но также позволяет отменить их с помощью атрибута unstyled. При этом <effdnd-actor unstyled="target"> отменяет только встроенные стили target, <effdnd-actor unstyled="scope"> отменяет только встроенные стили scope, а <effdnd-actor unstyled> отменяет все встроенные стили.

ts
/**
 * DnD actor attributes
 */
export interface IActorAttrs {
    /**
     * Item name
     */
    item?: string;
    /**
     * Scope name
     */
    scope?: string;
    /**
     * Target name
     */
    target?: string;
    /**
     * Actor state
     */
    state?: 'active' | 'passive';
    /**
     * Reset target state styles
     */
    unstyled?: boolean | 'target' | 'scope';
    /**
     * Display contents
     */
    contents?: boolean;
    /**
     * Item mode
     */
    'item-mode'?: 'keep' | 'clone';
    /**
     * Target mode
     */
    'target-mode'?: 'append' | 'prepend' | 'remove';
    /**
     * Scope mode
     */
    'scope-mode'?: 'order-x' | 'order-y';
    /**
     * Scope transition duration
     */
    'scope-dur'?: number;
    /**
     * Scope transition delay
     */
    'scope-del'?: number;
    /**
     * Scope transition timing-function
     */
    'scope-tf'?: string;
}

Атрибуты scope-dur, scope-del и scope-del для элемента с ролью scope устанавливают дефолтные значения CSS свойства transition.

Атрибут contents позволяет установить стиль display:contents;.

Вы также можете задать свои CSS-стили для различных состояний:

css
effdnd-actor[target][state=passive] .custom-dnd-target {
  outline: 4px solid #646cffaa;
}
effdnd-actor[target][state=active] .custom-dnd-target {
  outline: 4px solid #646cffaa;
  background: #646cffaa;
}

Рендеринг на стороне сервера ​

Если вы используете рендеринг на стороне сервера, просто добавьте скрипт с объявлением effdnd в раздел <head> вашего HTML-кода, чтобы верстка сохранила свои стили и структуру:

html
<head>
    <script src="https://cdn.jsdelivr.net/npm/effdnd@3.1.0/dist/use.js"></script>
</head>

Этот скрипт также позволит вам использовать effdnd в качестве глобальной переменной:

ts
import { TUseDnD } from 'effdnd';

declare global {
    interface Window {
        effdnd: ReturnType<TUseDnD>;
    }
};

const unobserve = window.effdnd.observe(() => console.log('effdnd'));

useDnD ​

Функция ообъявляет веб-компоненты effdnd-trigger и effdnd-actor, а также возвращает обработчики для подписки на кастомные события:

ts
export type TEventDetail = {
    type: TEventType;
    refs: TRefs;
    keys: TKeys;
    event: MouseEvent | TouchEvent;
};

interface TDnDEvent extends CustomEvent {
    detail: TEventDetail;
}

export type TDnDCallback = (event: TDnDEvent) => void;

export type TUseDnD = () => {
    /**
     * Observe DnD events
     * @param callback - event handler
     * @param element - HTML element
     */
    observe: (callback: TDnDCallback, element?: HTMLElement) => () => void;
    /**
     * Unobserve DnD events
     * @param callback - event handler
     * @param element - HTML element
     */
    unobserve: (callback: TDnDCallback, element?: HTMLElement) => void;
};

effdnd-trigger ​

Веб-компонент триггера содержит несколько полезных методов и свойств:

ts
/**
 * DnD trigger element 
 */
export interface ITriggerElement extends HTMLElement {
    /**
     * Is trigger active
     */
    isActive: boolean;
    /**
     * DnD item y-offset
     */
    dndY: number;
    /**
     * DnD item x-offset
     */
    dndX: number;
    /**
     * DnD item
     */
    dndItem: HTMLElement;
    /**
     * DnD scope
     */
    dndScope: HTMLElement;
    /**
     * DnD targets
     */
    dndTargets: Set<HTMLElement>;
    /**
     * Reset DnD translate
     */
    resetDnD(): void;
}

Опубликовано под лицензией Apache License 2.0