React реализует простую сортировку изображений перетаскиванием.

React.js
React реализует простую сортировку изображений перетаскиванием.

Основное введение 📝

  • На веб-странице, если вам нужно изменить положение нескольких элементов, вы можете сделать это, перетаскивая элементы. В HTML5 добавлен глобальный атрибутdraggable, установив для этого свойства значениеtrue/falseчтобы контролировать, можно ли перетаскивать элемент. нужно знать, это:
    • Ссылки и изображения по умолчанию можно перетаскивать.draggableУстановить какfalseСделайте их неперетаскиваемыми элементами.
    • Internet Explorer 8 и более ранние версии IE не поддерживаютсяdraggableАтрибуты.
  • Реализация сортировки изображений перетаскиванием в этой статье в основном использует элементы, которые будут срабатывать в процессе перетаскивания.ondragstart,ondragendиondragoverСобытия, конкретное время запуска и другие события могут относиться кMDN, из-за недостатка места в этой статье не будет подробностей.

Демонстрация эффекта 🤩

  • Первый взгляд на эффект:

  • Идея и конкретный код реализации будут представлены ниже.

Анализ мыслей 🤔

  • Реализация сортировки изображений перетаскиванием в этой статье в основном использует элементы, которые будут срабатывать в процессе перетаскивания.ondragstart,ondragendиondragoverсобытие.
  • слушаяondragstartEvent, сохраните данные перетаскиваемого в данный момент элемента и добавьте к нему специальные стили (установите прозрачность элемента и увеличьте элемент).
  • слушаяondragendсобытие удаляет специальный стиль, установленный на предыдущем шаге, для перетаскиваемого элемента. Чтобы уменьшить потребление памяти, мы помещаем перетаскиваемый элементondragendДелегирование событий в самый внешний контейнер (делегирование событий)
  • Реализовать самую важную функцию сортировки перетаскиванием, в основном для привязки элементовondragoverсобытие. когдаondragoverКогда событие срабатывает, вам нужно получить текущую позицию мыши (event.clientX, event.clientY), рассчитать, какой элемент перетаскивается мышью, и реализовать обмен и сортировку элементов, оценивая положение текущего перетаскиваемого элемента и других элементов.
  • В следующих подразделах будет представлен конкретный код реализации.

Конкретная реализация 🧐

базовая компоновка

  • Перед реализацией функции перетаскивания выполните базовую компоновку:
/** index.tsx */
import React, { useMemo, useState } from 'react';

import styles from './index.module.scss';

//  每行多少列
const COLUMN = 4;
//  每个元素宽度
const WIDTH = 120;
//  每个元素高度
const HEIGHT = 80;
// 图片左右 padding
const IMAGE_PADDING = 5;

const showList = [
  {
    id: 2,
    name: 'osmo pocket',
    image:
      'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605703865983&di=a35a43a3b9e866f1ee0048563bfd2577&imgtype=0&src=http%3A%2F%2Fpic.rmb.bdstatic.com%2F5d8f2523322e3f4de91021701e95182c.jpeg',
  },
  {
    id: 4,
    name: 'mavic pro',
    image:
      'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=787082346,3090178555&fm=15&gp=0.jpg',
  },
  {
    id: 1,
    name: 'mavic mini2',
    image:
      'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605703111703&di=59fa621eb1e7f8f4285b95df80e11fd0&imgtype=0&src=http%3A%2F%2Fp1.itc.cn%2Fimages01%2F20201105%2F600892c32d524b99a118ea56cdf3c211.png',
  },
  {
    id: 3,
    name: '机甲大师s1',
    image:
      'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605703133913&di=a415583ce97dd0a34efe17cac24a97ab&imgtype=0&src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fimages%2F20200325%2F64ebb68f1125450f91e64bb34dc19d55.jpeg',
  },
  {
    id: 0,
    name: 'mavic 2',
    image:
      'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=4132295553,3011440949&fm=26&gp=0.jpg',
  },
];

const DragAndDropPage: React.FC = () => {
  const [list, setList] = useState(showList);

  // IMPORTANT: 动画需要,需要保持一定的渲染顺序
  const sortedList = useMemo(() => {
    return list.slice().sort((a, b) => {
      return a.id - b.id;
    });
  }, [list]);

  const listHeight = useMemo(() => {
    const size = list.length;
    return Math.ceil(size / COLUMN) * HEIGHT;
  }, [list]);

  return (
    <div 
      className={styles.wrapper} 
      style={{ width: COLUMN * (WIDTH + IMAGE_PADDING) + IMAGE_PADDING }}
    >
      <ul className={styles.list} style={{ height: listHeight }}>
        {sortedList.map((item) => {
          const index = list.findIndex((i) => i === item);
          const row = Math.floor(index / COLUMN);
          const col = index % COLUMN;
          return (
            <li
              key={item.id}
              className={styles.item}
              style={{
                height: HEIGHT,
                left: col * (WIDTH + IMAGE_PADDING),
                top: row * HEIGHT,
                padding: `0 ${IMAGE_PADDING}px`
              }}
              data-id={item.id}
            >
              <img src={item.image} alt={item.name} width={WIDTH} />
            </li>
          );
        })}
      </ul>
    </div>
  );
};

export default React.memo(DragAndDropPage);
  • Содержимое файла стиля следующее:
/** index.module.scss */
.wrapper {
  overflow: hidden;
  margin: 100px auto;
  padding: 10px 0;
  background: #fff;
  border-radius: 5px;
}

.list {
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: 0;

  position: relative;
}

.item {
  position: absolute;
  display: flex;
  align-items: center;
  transition: all 0.2s ease-in-out;
}

Эффект следующий:

  • Как видно из приведенного выше кода, хотя отображаемыйliэлемент выглядит какlistОтображается исходный порядок, но фактический порядок рендеринга определяется отсортированным списком.sortedListрешил.

Причина, по которой он ведет себя какlistПервоначальный порядок показанsortedList.mapвремя по текущемуitemсуществуетlistиндекс массиваindexРассчитать его CSSleftиtopАтрибуты.

Перетащите реализацию

Перетащите, чтобы добавить специальные стили

  • Во-первых, для самого внешнего контейнераwrapperсвязыватьrefатрибут, который удобно потом получить в методе событияDOMузел, и поместите каждыйliэлементальdraggableЗначение свойства установлено вtrue/falseСделайте его перетаскиваемым элементом при привязкеondragstartсобытие:

  • handleDragStartМетод в основном состоит в том, чтобы сохранить данные текущего перетаскиваемого элемента и добавить к нему специальные стили (установить прозрачность элемента и увеличить элемент) Соответствующий код выглядит следующим образом:

  • На данный момент эффект следующий:

Перетащите конец, чтобы удалить специальные стили

  • Как видите, когда вы начинаете перетаскивать элемент, к элементу применяется особый стиль, но элемент не восстанавливает исходный стиль после отпускания мыши. В это время вам нужно удалить специальный стиль, добавленный ранее после завершения перетаскивания.
  • Чтобы уменьшить потребление памяти, мы помещаем перетаскиваемый элементondragendСобытие делегируется самому внешнему контейнеру (делегирование события), и соответствующий код выглядит следующим образом:

  • На данный момент эффект следующий:

перетащите, чтобы отсортировать

  • Далее мы реализуем самую важную функцию перетаскивания, в основном для связывания элементов.ondragoverсобытие.
  • По умолчанию данные/элементы не могут быть помещены в другие элементы. Если мы хотим добиться этого, нам нужно запретить обработку элементов по умолчанию. Мы можем позвонитьevent.preventDefault()метод для реализации события ondragover.
  • когдаondragoverКогда событие срабатывает, вам нужно получить текущую позицию мыши (event.clientX, event.clientY), рассчитать, к какому элементу перетаскивается текущая мышь. Оценивая положение текущего перетаскиваемого элемента и других элементов, осуществляется обмен и сортировка элементов. Ключ в том, чтобы реализоватьupdateListметод, соответствующий код выглядит следующим образом:
/** 将某元素插入到数组中的某位置 */
export function insertBefore<T>(list: T[], from: T, to?: T): T[] {
  const copy = [...list];
  const fromIndex = copy.indexOf(from);
  if (from === to) {
    return copy;
  }
  copy.splice(fromIndex, 1);
  const newToIndex = to ? copy.indexOf(to) : -1;
  if (to && newToIndex >= 0) {
    copy.splice(newToIndex, 0, from);
  } else {
    // 没有 To 或 To 不在序列里,将元素移动到末尾
    copy.push(from);
  }
  return copy;
}

/** 判断是否数组相等 */
export function isEqualBy<T>(a: T[], b: T[], key: keyof T) {
  const aList = a.map((item) => item[key]);
  const bList = b.map((item) => item[key]);
  
  let flag = true;
  aList.forEach((i, idx) => {
    if (i !== bList[idx]) {
      flag = false
    }
  })
  return flag;
}

const DragAndDropPage: React.FC = () => {
  const [list, setList] = useState(showList);
  const dragItemRef = useRef<ListItem>();
  const dropAreaRef = useRef<HTMLDivElement>(null);

  ...

  const updateList = useCallback(
    (clientX: number, clientY: number) => {
      const dropRect = dropAreaRef.current?.getBoundingClientRect();
      if (dropRect) {
        const offsetX = clientX - dropRect.left;
        const offsetY = clientY - dropRect.top;
        const dragItem = dragItemRef.current;
        // 超出拖动区域
        if (
          !dragItem ||
          offsetX < 0 ||
          offsetX > dropRect.width ||
          offsetY < 0 ||
          offsetY > dropRect.height
        ) {
          return;
        }

        const col = Math.floor(offsetX / WIDTH);
        const row = Math.floor(offsetY / HEIGHT);
        let currentIndex = row * COLUMN + col;
        const fromIndex = list.indexOf(dragItem);
        if (fromIndex < currentIndex) {
          // 从前往后移动
          currentIndex++;
        }
        const currentItem = list[currentIndex];

        const ordered = insertBefore(list, dragItem, currentItem);
        if (isEqualBy(ordered, list, 'id')) {
          return;
        }
        setList(ordered);
      }
    },
    [list]
  );

  const handleDragOver = useCallback(
    (e: React.DragEvent<HTMLDivElement>) => {
      e.preventDefault();
      updateList(e.clientX, e.clientY);
    },
    [updateList]
  );

  return (
    <div
      className={styles.wrapper}
      ref={dropAreaRef}
      style={{ width: COLUMN * (WIDTH + IMAGE_PADDING) + IMAGE_PADDING }}
      onDragEnd={handleDragEnd}
      onDragOver={handleDragOver}
    >
      <ul className={styles.list} style={{ height: listHeight }}>
        {sortedList.map((item) => {
          const index = list.findIndex((i) => i === item);
          const row = Math.floor(index / COLUMN);
          const col = index % COLUMN;
          return (
            <li
              draggable
              key={item.id}
              className={styles.item}
              style={{
                height: HEIGHT,
                left: col * (WIDTH + IMAGE_PADDING),
                top: row * HEIGHT,
                padding: `0 ${IMAGE_PADDING}px`,
              }}
              data-id={item.id}
              onDragStart={(e) => handleDragStart(e, item)}
            >
              <img src={item.image} alt={item.name} width={WIDTH} />
            </li>
          );
        })}
      </ul>
    </div>
  );
};

export default React.memo(DragAndDropPage);
  • Эффект следующий:

полный код

  • index.tsxСодержимое файла:

  • Содержимое файла стиля:

Резюме 👀

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

Если есть какие-либо упущения или ошибки в вышеуказанном содержании, пожалуйста, оставьте сообщение ✍️Укажите и продвигайтесь вперед вместе💪💪💪

Если вы считаете, что эта статья полезна для вас, 🏀🏀 оставьте свой драгоценный 👍

использованная литература

  1. React hook
  2. API перетаскивания HTML
  3. Используйте два разных жеста для реализации сортировки перетаскиванием