推荐一个react拖拽排序的库,@dnd

编程入门 行业动态 更新时间:2024-10-24 03:24:49

推荐一个react<a href=https://www.elefans.com/category/jswz/34/1765858.html style=拖拽排序的库,@dnd"/>

推荐一个react拖拽排序的库,@dnd

@dnd-kit

  • @dnd-kit
    • 基于这个库封装了一个组件
      • 效果图
      • 代码
        • index
        • less
        • components
    • 一些基本使用总结

@dnd-kit

官网
GitHub

基于这个库封装了一个组件

效果图

代码

index
import { forwardRef, ForwardedRef, useState, useRef, useCallback, useMemo } from 'react';
import { Checkbox } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { Resizable, NumberSize, ResizeDirection } from 're-resizable';
import type { ERPTransferProps, ERPTransferRef } from './type';
import { LeftHeaderLeft, LeftHeaderRight, RightHeaderLeft, RightHeaderRight } from './components/headers';
import DraggableList from './components/draggable';
import { LeftSearch } from './components/search';
import './index.less';
import { debounce, isUndefined } from 'lodash-es';type ResizeCallbackParams = [MouseEvent | TouchEvent, ResizeDirection, HTMLElement, NumberSize];function Index<T>(props: ERPTransferProps<T>, ref: ForwardedRef<ERPTransferRef<T>>) {const {width = 688,height = 376,transferWidth = 336,allowResizable = false,dataSource,keyCode,nameCode,leftHeaderLeft,leftHeaderRight,rightHeaderLeft,rightHeaderRight,onChange,} = props;const [leftWidth, setLeftWidth] = useState(transferWidth);const [rightWidth, setRightWidth] = useState(transferWidth);const leftWidthRef = useRef(transferWidth);const rightWidthRef = useRef(transferWidth);const [selectedKeys, setSelectedKeys] = useState<(string | number)[]>([]);const [searchLeftData, setSearchLeftData] = useState<T[]>();const [searchRightData, setSearchRightData] = useState<T[]>();// 用map对象存一下dataSourceconst dataSourceMapMemo = useMemo(() => {return new Map(dataSource.map((i) => [i[keyCode], i]));}, [dataSource]);/* —————————————————————————拖拽的回调处理—————————————————————————————————— */const onLeftResize = useCallback((...rest: ResizeCallbackParams) => {const { width } = rest[3];setLeftWidth(leftWidthRef.current + width);setRightWidth(rightWidthRef.current - width);}, []);const onRightResize = useCallback((...rest: ResizeCallbackParams) => {const { width } = rest[3];setRightWidth(rightWidthRef.current + width);setLeftWidth(leftWidthRef.current - width);}, []);const onLeftResizeStop = useCallback((...rest: ResizeCallbackParams) => {const { width } = rest[3];leftWidthRef.current = leftWidthRef.current + width;rightWidthRef.current = rightWidthRef.current - width;}, []);const onRightResizeStop = useCallback((...rest: ResizeCallbackParams) => {const { width } = rest[3];leftWidthRef.current = leftWidthRef.current - width;rightWidthRef.current = rightWidthRef.current + width;}, []);/* —————————————————————————左侧—————————————————————————————————— */const handleClickItem = (e: CheckboxChangeEvent, keyCode: string) => {const checked = e.target.checked;if (checked) {const selectedKeysTemp = [...selectedKeys, keyCode];setSelectedKeys(selectedKeysTemp);handleOnchange(selectedKeysTemp);} else {const selectedKeysTemp = selectedKeys.filter((item) => item !== keyCode);setSelectedKeys(selectedKeysTemp);handleOnchange(selectedKeysTemp);}};const handleAllSelected = (all: boolean) => {if (all) {const selectedKeysTemp = dataSource.map((item) => item[keyCode]);setSelectedKeys(selectedKeysTemp);handleOnchange(selectedKeysTemp);} else {setSelectedKeys([]);handleOnchange([]);}};const handleLeftSearch = (searchText: string) => {if (searchText === '') {setSearchLeftData(undefined);} else {const searchResult = dataSource.filter((i) => i[nameCode].includes(searchText));setSearchLeftData(searchResult);}};/* —————————————————————————右侧—————————————————————————————————— */const handleOnClean = () => {setSelectedKeys([]);handleOnchange([]);};const handleRightSearch = (searchText: string) => {if (searchText === '') {setSearchRightData(undefined);} else {const searchResult = dataSource.filter((i) => i[nameCode].includes(searchText));setSearchRightData(searchResult);}};const handleDraggableListData = () => {if (!searchRightData) return selectedKeys;else {return searchRightData.filter((i) => selectedKeys.includes(i[keyCode])).map((i) => i[keyCode]);}};/* —————————————————————————onChange—————————————————————————————————— */const handleOnchange = (keys: (string | number)[]) => {let result: T[] = [];for (const key of keys) {if (dataSourceMapMemo.has(key)) {result.push(dataSourceMapMemo.get(key)!);}}onChange?.(result);};return (<div style={{ width, height }} className="panui-fi-transfer-erp-container"><Resizablesize={{ width: leftWidth, height }}onResize={onLeftResize}onResizeStop={onLeftResizeStop}enable={{right: allowResizable,}}><div className="left-wrapper"><div className="left-header"><LeftHeaderLeftnumerator={selectedKeys.length}denominator={dataSource.length}leftHeaderLeft={leftHeaderLeft}onChange={handleAllSelected}/><LeftHeaderRight leftHeaderRight={leftHeaderRight} /></div><LeftSearch onChange={debounce(handleLeftSearch, 300)} /><ul className="list">{(searchLeftData ?? dataSource).map((i) => (<li key={i[keyCode]} className="li-item"><Checkbox checked={selectedKeys.includes(i[keyCode])} onChange={(e) => handleClickItem(e, i[keyCode])}>{i[nameCode]}</Checkbox></li>))}{searchLeftData?.length === 0 && <li className="li-item-no">无搜索结果</li>}</ul></div></Resizable><Resizablesize={{ width: rightWidth, height }}onResize={onRightResize}onResizeStop={onRightResizeStop}enable={{left: allowResizable,}}><div className="right-wrapper"><div className="right-header"><RightHeaderLeft selected={selectedKeys.length} rightHeaderLeft={rightHeaderLeft} /><RightHeaderRight rightHeaderRight={rightHeaderRight} onClean={handleOnClean} /></div><LeftSearch onChange={debounce(handleRightSearch, 300)} /><div className="list"><DraggableListdata={handleDraggableListData()}setSortData={setSelectedKeys}dataSourceMap={dataSourceMapMemo}nameCode={nameCode}handleOnchange={handleOnchange}disabledDraggable={!isUndefined(searchRightData)}/></div></div></Resizable></div>);
}export default forwardRef(Index);
less
.panui-fi-transfer-erp-container {position: relative;display: flex;justify-content: space-between;.left-wrapper {width: 100%;height: 100%;border: 1px solid rgba(227, 231, 237, 1);border-radius: 4px;display: flex;flex-direction: column;.left-header {border-bottom: 1px solid #e3e7ed;color: #86909c;display: flex;justify-content: space-between;.left-header-left {height: 38px;line-height: 38px;padding-left: 12px;.ant-checkbox + span {color: #86909c;}}.left-header-right {line-height: 38px;padding-right: 12px;}}.list {margin: 0;padding: 0;list-style: none;flex: 1;overflow: hidden auto;.li-item {line-height: 32px;padding-left: 12px;cursor: pointer;&:hover {background-color: #e6ecfa;}.panui-base-checkbox-container {width: 100%;margin-right: 4px;.ant-checkbox {.ant-checkbox-inner {border-radius: 4px;}& + span {width: 100%;}}}}.li-item-no {text-align: center;color: #86909c;line-height: 64px;}}}.right-wrapper {width: 100%;height: 100%;border: 1px solid rgba(227, 231, 237, 1);border-radius: 4px;display: flex;flex-direction: column;.right-header {border-bottom: 1px solid #e3e7ed;color: #86909c;display: flex;justify-content: space-between;.right-header-left {line-height: 38px;padding-left: 12px;}.right-header-right {line-height: 38px;padding-right: 12px;color: #0e42d2;cursor: pointer;}}.list {padding: 0 12px;flex: 1;overflow: hidden auto;.li-item {width: 100%;line-height: 30px;display: flex;justify-content: space-between;border: 1px solid transparent;align-items: center;.li-item-name {height: 30px;display: flex;align-items: center;span {height: inherit;}.panui-icon-anticon {display: flex;align-items: center;cursor: grab;}}.panui-icon-anticon {cursor: pointer;}}.li-item[aria-pressed='true'] {background: rgba(223, 0, 36, 0.04);border: 1px solid rgba(223, 0, 36, 0.3);border-radius: 4px;.li-item-name {visibility: hidden;}.li-item-name + span {visibility: hidden;}}.li-item.li-item-overlay {background: rgba(255, 255, 255, 0.9);border: 1px solid rgba(223, 0, 36, 0.3);box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1);border-radius: 4px;cursor: grabbing;.panui-icon-anticon {cursor: grabbing;}.li-item-name {color: #86909c;}.li-item-name + span {visibility: hidden;}}.li-item.li-item-disabled-draggable {.panui-icon-anticon {cursor: default;}}}}.search-container {padding: 8px 12px;}
}
components
// header
import React from 'react';
import { Checkbox } from 'antd';
interface LeftHeaderLeftProps {leftHeaderLeft?: (numerator: number, denominator: number) => React.ReactNode;numerator: number;denominator: number;onChange: (all: boolean) => void;
}export const LeftHeaderLeft = (props: LeftHeaderLeftProps) => {const { leftHeaderLeft, numerator, denominator, onChange } = props;return (<div className="left-header-left"><Checkboxindeterminate={numerator > 0 && denominator !== numerator}checked={numerator === denominator}onChange={(e) => onChange(e.target.checked)}>{leftHeaderLeft ? leftHeaderLeft(numerator, denominator) : `${numerator}/${denominator}`}</Checkbox></div>);
};export const LeftHeaderRight = ({ leftHeaderRight }: { leftHeaderRight?: React.ReactNode }) => {return leftHeaderRight ? <div>{leftHeaderRight}</div> : <span className="left-header-right">可选择</span>;
};export const RightHeaderLeft = ({selected,rightHeaderLeft,
}: {selected: number;rightHeaderLeft?: (selected: number) => React.ReactNode;
}) => {return rightHeaderLeft ? (<div>{rightHeaderLeft(selected)}</div>) : (<span className="right-header-left">已选择 <span style={{ color: '#1D2129' }}>{selected}</span></span>);
};export const RightHeaderRight = ({rightHeaderRight,onClean,
}: {rightHeaderRight?: React.ReactNode;onClean: () => void;
}) => {return rightHeaderRight ? (<div>{rightHeaderRight}</div>) : (<span className="right-header-right" onClick={onClean}>清空</span>);
};
// Search
import { Input } from 'antd';
export function LeftSearch({ onChange }: { onChange: (searchText: string) => void }) {return (<div className="search-container"><Input placeholder="搜索" suffix="放大镜" onChange={(e) => onChange(e.target.value)} /></div>);
}
// draggable
import React, { useState } from 'react';
import {DndContext,closestCenter,KeyboardSensor,PointerSensor,useSensor,useSensors,DragEndEvent,DragOverlay,DragStartEvent,
} from '@dnd-kit/core';
import {arrayMove,SortableContext,sortableKeyboardCoordinates,verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { restrictToVerticalAxis, restrictToParentElement } from '@dnd-kit/modifiers';import { SortableItem } from './sortItem';
import { isEmpty } from 'lodash-es';interface DraggableListProps<T> {data: (string | number)[];setSortData: React.Dispatch<React.SetStateAction<(string | number)[]>>;dataSourceMap: Map<string | number, T>;nameCode: string;handleOnchange: (keys: (string | number)[]) => void;disabledDraggable?: boolean;
}export default function Index<T>(props: DraggableListProps<T>) {const { data, setSortData, dataSourceMap, nameCode, handleOnchange, disabledDraggable } = props;const sensors = useSensors(useSensor(PointerSensor),useSensor(KeyboardSensor, {coordinateGetter: sortableKeyboardCoordinates,}),);const handleDeleteItem = (id: string | number) => {setSortData((items) => {const keys = items.filter((item) => item !== id);handleOnchange(keys);return keys;});};const [activeId, setActiveId] = useState<string | number | null>(null);return (<DndContextsensors={sensors}collisionDetection={closestCenter}onDragStart={handleDragStart}onDragEnd={handleDragEnd}modifiers={[restrictToParentElement]}><SortableContext items={data} strategy={verticalListSortingStrategy} disabled={disabledDraggable}>{data.map((i) => (<SortableItemkey={i}id={i}name={dataSourceMap.get(i)?.[nameCode]}deleteItem={handleDeleteItem}className={disabledDraggable ? 'li-item-disabled-draggable' : ''}/>))}{isEmpty(data) && disabledDraggable && (<spanstyle={{display: 'inline-block',width: '100%',textAlign: 'center',color: '#86909c',lineHeight: '64px',}}>无搜索结果</span>)}</SortableContext><DragOverlay>{activeId ? (<SortableItemkey={activeId}id={activeId}name={dataSourceMap.get(activeId)?.[nameCode]}className="li-item-overlay"/>) : null}</DragOverlay></DndContext>);function handleDragStart(event: DragStartEvent) {setActiveId(event.active.id);}function handleDragEnd(event: DragEndEvent) {const { active, over } = event;if (active.id !== over?.id && over) {setSortData((items) => {const oldIndex = items.indexOf(active.id);const newIndex = items.indexOf(over?.id);const keys = arrayMove(items, oldIndex, newIndex);handleOnchange(keys);return keys;});}}
}
// SortableItem
import { useSortable } from '@dnd-kit/sortable';
import classNames from 'classnames';
import { CSS } from '@dnd-kit/utilities';
import { PWrong1Outlined, PDrag2Filled } from '@panui/icons';interface SortableItemProps {id: string | number;name: string;deleteItem?: (id: string | number) => void;className?: string;
}export function SortableItem(props: SortableItemProps) {const { id, name, deleteItem, className } = props;const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });const style = {transform: CSS.Transform.toString(transform),transition,};return (<div ref={setNodeRef} style={style} className={classNames(['li-item', className])} {...attributes}><div className="li-item-name"><span {...listeners}><PDrag2Filled /></span><span style={{ marginLeft: '8px' }}>{name}</span></div><span onClick={() => deleteItem?.(id)}><PWrong1Outlined /></span></div>);
}

PS:

  1. 线性的复选框是又封装了一层,贴的代码里换成了antd的

一些基本使用总结

  1. 基本demo地址
  2. 内容要嵌套在DndContext组件中
  3. DndContext的modifiers属性控制运动检测坐标,作用如:锁定x、y等
  4. listeners属性可以用来指定可拖拽的dom,setNodeRef给要拖拽的dom
  5. DragOverlay组件是拖拽覆盖的组件,通过handleDragStart来配合拖拽的是哪个
  6. 可以通过属性选择器[aria-pressed=‘true’]来判断当前拖拽的是哪个

更多推荐

推荐一个react拖拽排序的库,@dnd

本文发布于:2024-02-27 08:53:46,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1705938.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:拖拽   react   dnd

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!