react列表拖拽排序组件

 

通过这篇文章修改在react中使用

React实现一个拖拽排序组件 - 支持多行多列、支持TypeScript、支持Flip动画、可自定义拖拽区域_react拖拽组件-CSDN博客

1、封装的拖拽排序组件

import React, { useEffect, useRef } from 'react';
import type { DragEventHandler } from 'react';
import type { DragSortProps } from './type';
import { FlipList } from './Filp';

import styles from './index.less';

/**查找符合条件的父节点
 * @param node 当前节点。如果当前节点就符合条件,就会返回当前节点
 * @param target 参数是当前找到的节点,返回一个布尔值,为true代表找到想要的父节点
 * @returns 没找到则返回null,找到了返回Element
 */
/* eslint-disable no-param-reassign */
export function findParent(node: Element, target: (nowNode: Element) => boolean) {
  while (node && !target(node)) {
    if (node.parentElement) {
      node = node.parentElement;
    } else {
      return null;
    }
  }
  return node;
}

/**拖拽时,留在原位置的元素的样式 */
const movingClass = [styles.background]; //使用数组是为了方便以后添加其他类名
/**拖拽时,留在原位置的子元素的样式 */
const opacityClass = ['opacity-0']; //使用数组是为了方便以后添加其他类名

/**拖拽排序组件 */
const DragSort = function ({
  list,
  render,
  afterDrag,
  keyName,
  cols = 1,
  marginX = 16,
  marginY = 10,
  flipWithListChange = true,
}: DragSortProps) {
  const listRef = useRef(null);
  /**记录当前正在拖拽哪个元素 */
  const nowDragItem = useRef();
  /**存储flipList动画实例 */
  const flipListRef = useRef();
  // const [dragOpen, setDragOpen] = useState(false); //是否开启拖拽 (鼠标进入指定区域开启)

  /**创建记录新的动画记录,并立即记录当前位置 */
  const createNewFlipList = (exceptTarget?: Element) => {
    if (!listRef.current) return;
    //记录动画
    // @ts-ignore
    const listenChildren = [...listRef?.current?.children].filter((k) => k !== exceptTarget); //除了指定元素,其它的都动画
    flipListRef.current = new FlipList(listenChildren, 300);
    flipListRef.current.recordFirst();
  };

  //下面这两个是用于,当列表变化时,进行动画
  useEffect(() => {
    if (!flipWithListChange) return;
    createNewFlipList();
  }, [list, flipWithListChange]);
  useEffect(() => {
    if (!flipWithListChange) return;
    createNewFlipList();
    return () => {
      flipListRef.current?.play(() => flipListRef.current?.recordFirst());
    };
  }, [list.length, flipWithListChange]);

  /**事件委托- 监听 拖拽开始 事件,添加样式 */
  const onDragStart: DragEventHandler = (e) => {
    if (!listRef.current) return;
    e.stopPropagation(); //阻止冒泡

    /**这是当前正在被拖拽的元素 */
    const target = e.target as HTMLDivElement;

    //设置被拖拽元素“留在原地”的样式。为了防止设置正在拖拽的元素样式,所以用定时器,宏任务更晚执行
    setTimeout(() => {
      target.classList.add(...movingClass); //设置正被拖动的元素样式
      target.childNodes.forEach((k) => (k as HTMLDivElement).classList?.add(...opacityClass)); //把子元素都设置为透明,避免影响
    }, 0);

    //记录元素的位置,用于Flip动画
    createNewFlipList(target);

    //记录当前拖拽的元素
    nowDragItem.current = target;

    //设置鼠标样式
    e.dataTransfer.effectAllowed = 'move';
  };

  /**事件委托- 监听 拖拽进入某个元素 事件,在这里只是DOM变化,数据顺序没有变化 */
  const onDragEnter: DragEventHandler = (e) => {
    e.preventDefault(); //阻止默认行为,默认是不允许元素拖动到人家身上的
    if (!listRef.current || !nowDragItem.current) return;

    /**孩子数组,每次都会获取最新的 */
    // @ts-ignore
    const children = [...listRef.current.children];
    /**真正会被挪动的元素(当前正悬浮在哪个元素上面) */ //找到符合条件的父节点
    const realTarget = findParent(e.target as Element, (now) => children.indexOf(now) !== -1);

    //边界判断
    if (realTarget === listRef.current || realTarget === nowDragItem.current || !realTarget) {
      // console.log("拖到自身或者拖到外面");
      return;
    }
    if (realTarget.className.includes(FlipList.movingClass)) {
      // console.log("这是正在动画的元素,跳过");
      return;
    }

    //拿到两个元素的索引,用来判断这俩元素应该怎么移动
    /**被拖拽元素在孩子数组中的索引 */
    const nowDragtItemIndex = children.indexOf(nowDragItem.current);
    /**被进入元素在孩子数组中的索引 */
    const enterItemIndex = children.indexOf(realTarget);

    //当用户选中文字,然后去拖动这个文字时,就会触发 (可以通过禁止选中文字来避免)
    if (enterItemIndex === -1 || nowDragtItemIndex === -1) {
      // console.log("若第二个数为-1,说明拖动的不是元素,而是“文字”", enterItemIndex, nowDragtItemIndex);
      return;
    }

    //Flip动画 - 记录原始位置
    flipListRef.current?.recordFirst();

    if (nowDragtItemIndex < enterItemIndex) {
      // console.log("向下移动");
      listRef.current.insertBefore(nowDragItem.current, realTarget.nextElementSibling);
    } else {
      // console.log("向上移动");
      listRef.current.insertBefore(nowDragItem.current, realTarget);
    }

    //Flip动画 - 播放
    flipListRef.current?.play();
  };

  /**事件委托- 监听 拖拽结束 事件,删除样式,设置当前列表 */
  const onDragEnd: DragEventHandler = (e) => {
    if (!listRef.current) return;
    /**当前正在被拖拽的元素 */
    const target = e.target as Element;

    target.classList.remove(...movingClass); //删除前面添加的 被拖拽元素的样式,回归原样式
    target.childNodes.forEach((k) => (k as Element).classList?.remove(...opacityClass)); //删除所有子元素的透明样式

    /**拿到当前DOM的id顺序信息 */
    // @ts-ignore
    const ids = [...listRef.current.children].map((k) => String(k.id)); //根据id,判断到时候应该怎么排序

    //把列表按照id排序
    const newList = [...list].sort(function (a, b) {
      const aIndex = ids.indexOf(String(a[keyName]));
      const bIndex = ids.indexOf(String(b[keyName]));
      if (aIndex === -1 && bIndex === -1) return 0;
      else if (aIndex === -1) return 1;
      else if (bIndex === -1) return -1;
      else return aIndex - bIndex;
    });

    afterDrag(newList); //触发外界传入的回调函数
    // setDragOpen(false); //拖拽完成后,再次禁止拖拽
  };

  return (
    
e.preventDefault()} //被拖动的对象被拖到其它容器时(因为默认不能拖到其它元素上) onDragEnd={onDragEnd} /**拖拽按钮组件 */ //只有鼠标悬浮在这上面的时候,才开启拖拽,做到“指定区域拖拽” // onMouseEnter={(e: any) => { // e.stopPropagation(); // setDragOpen(true); // }} // onMouseLeave={(e: any) => { // e.stopPropagation(); // setDragOpen(false) // }} > {list.map((item, index) => { const key = item[keyName] as string; const width = `calc((100% - ${(cols - 1) * marginX}px) / ${cols})`; return (
{render?.({ data: item, index })}
); })}
); }; export default DragSort;

2、Flip动画

/**位置的类型 */
interface position {
  x: number;
  y: number;
}

/**Flip动画 */
export class Flip {
  /**dom元素 */
  private dom: Element;
  /**原位置 */
  private firstPosition: position | null = null;
  /**动画时间 */
  private duration: number;
  /**正在移动的动画会有一个专属的class类名,可以用于标识 */
  static movingClass = '__flipMoving__';
  constructor(dom: Element, duration = 500) {
    this.dom = dom;
    this.duration = duration;
  }
  /**获得元素的当前位置信息 */
  private getDomPosition(): position {
    const rect = this.dom.getBoundingClientRect();
    return {
      x: rect.left,
      y: rect.top,
    };
  }
  /**给原始位置赋值 */
  /* eslint-disable no-param-reassign */
  recordFirst(firstPosition?: position) {
    if (!firstPosition) firstPosition = this.getDomPosition();
    this.firstPosition = { ...firstPosition };
  }
  /**播放动画 */
  play(callback?: () => any) {
    if (!this.firstPosition) {
      // console.warn('请先记录原始位置');
      return;
    }
    const lastPositon = this.getDomPosition();
    const dif: position = {
      x: lastPositon.x - this.firstPosition.x,
      y: lastPositon.y - this.firstPosition.y,
    };
    // console.log(this, dif);
    if (!dif.x && !dif.y) return;
    this.dom.classList.add(Flip.movingClass);
    this.dom.animate(
      [{ transform: `translate(${-dif.x}px, ${-dif.y}px)` }, { transform: `translate(0px, 0px)` }],
      { duration: this.duration },
    );
    setTimeout(() => {
      this.dom.classList.remove(Flip.movingClass);
      callback?.();
    }, this.duration);
  }
}
/**Flip多元素同时触发 */
export class FlipList {
  /**Flip列表 */
  private flips: Flip[];
  /**正在移动的动画会有一个专属的class类名,可以用于标识 */
  static movingClass = Flip.movingClass;
  /**Flip多元素同时触发 - 构造函数
   * @param domList 要监听的DOM列表
   * @param duration 动画时长,默认500ms
   */
  constructor(domList: Element[], duration?: number) {
    this.flips = domList.map((k) => new Flip(k, duration));
  }
  /**记录全部初始位置 */
  recordFirst() {
    this.flips.forEach((flip) => flip.recordFirst());
  }
  /**播放全部动画 */
  play(callback?: () => any) {
    this.flips.forEach((flip) => flip.play(callback));
  }
}

3、组件类型定义

/**有孩子的,基础的组件props,包含className style children */
interface baseChildrenProps {
  /**组件最外层的className */
  className?: string;
  /**组件最外层的style */
  style?: React.CSSProperties;
  /**孩子 */
  children?: React.ReactNode;
}
/**ItemRender渲染函数的参数 */
type itemProps = {
  /**当前元素 */
  data: T;
  /**当前索引 */
  index: number;
  /**父元素宽度 */
  width?: number;
  /**可拖拽的盒子,只有在这上面才能拖拽。自由放置位置。提供了一个默认的拖拽图标。可以作为包围盒,将某块内容作为拖拽区域 */
  DragBox?: (props: baseChildrenProps) => React.ReactNode;
};
/**拖拽排序组件的props */
export interface DragSortProps {
  /**组件最外层的className */
  className?: string;
  /**组件最外层的style */
  style?: React.CSSProperties;
  /**列表,拖拽后会改变里面的顺序 */
  list: T[];
  /**用作唯一key,在list的元素中的属性名,比如id。必须传递 */
  keyName: keyof T;
  /**一行个数,默认1 */
  cols?: number;
  /**元素水平间距,单位px,默认0 (因为一行默认1) */
  marginX?: number;
  /**元素垂直间距,单位px,默认10 */
  marginY?: number;
  /**当列表长度变化时,是否需要Flip动画,默认开启 (可能有点略微的动画bug) */
  flipWithListChange?: boolean;
  /**每个元素的渲染函数 */
  render?: (props: itemProps) => React.ReactNode;
  /**拖拽结束事件,返回排序好的新数组,在里面自己调用setList */
  afterDrag: (list?: T[]) => any;
}

4、样式

@import (reference) '~antd/es/style/themes/index';

.background {
  background: linear-gradient(
    45deg,
    rgba(0, 0, 0, 0.1) 0,
    rgba(0, 0, 0, 0.1) 25%,
    transparent 25%,
    transparent 50%,
    rgba(0, 0, 0, 0.1) 50%,
    rgba(0, 0, 0, 0.1) 75%,
    transparent 75%,
    transparent
  );
  background-size: 20px 20px;
  border-radius: 5px;
}

.list {
  display: flex;
  flex-wrap: wrap;
  row-gap: 10px;

  .dragBox {
    width: 100%;
  }

  .item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 6px 10px;
    overflow: hidden;
    background-color: #fff;
    border-radius: 2px;
    cursor: pointer;

    > span:first-child {
      flex: 1;
      width: 0;
    }
  }
}

你可能感兴趣的:(react.js,前端,前端框架)