6. drag-list-by-hook

64 阅读2分钟
  • 展示的列表数据
const list = [
    {
        title: '波妞1',
        src: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.alicdn.com%2Fi2%2F1628650609%2FO1CN01VZegdd1GMxn2LDJjm_%21%211628650609.jpg&refer=http%3A%2F%2Fimg.alicdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629511811&t=a164044af84bc6b7faa2cc109fc6b808'
    },
    {
        title: '魔女2',
        src: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg15.3lian.com%2F2015%2Fh1%2F130%2Fd%2F41.jpg&refer=http%3A%2F%2Fimg15.3lian.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629511721&t=908cba2a5319c3d2d55a9cc0955de25c'
    },
    {
        title: '龙猫3',
        src: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fgss0.baidu.com%2F-Po3dSag_xI4khGko9WTAnF6hhy%2Fzhidao%2Fpic%2Fitem%2F4034970a304e251fae75ad03a786c9177e3e534e.jpg&refer=http%3A%2F%2Fgss0.baidu.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629511755&t=27314f2d34ebb983a1c1fa6393c781e4'
    }
]
  • UI部分
// 一张卡片的背后是有可拖拽区域的 这个区域是Bar
// 每一个Draggable紧挨着一个Bar
/*
list里
Draggable
Bar
Draggable
Bar
。。。

所以DraggableList中传入的list既有draggable又有bar
*/
function App() {
    return (
        <div className="App">
            drag list by hook
            <DraggableList list={list} />
        </div>
    );
}
export default App;

// 整体拖拽列表容器 通过它循环渲染列表项和拖拽放置区,并向下面的组件传递属性
function DraggableList({ list }) {
    const { dragList, createDraggerPorps, createDropperProps } = useDraggable(list);
    return dragList.map((item, i) => {
        if (item.type === "BAR") {
            return <Bar id={i} key={item.id} {...createDropperProps(i)} />
        } else {
            return (
                <Draggable {...createDraggerPorps(i)}>
                    <Card {...item.data} />
                </Draggable>
            )
        }
    })
}

// 包装层 包装Card 
// 当被拖拽的时候 当前卡片opacity:0;position:fixed;
function Draggable({ children, eventHandlers, dragging, id }) {
    return <div
        className={cls("draggable", [[dragging === id, "dragging"]])}
        // 可拖拽
        draggable={true} 
        {...eventHandlers}
    >
        {children}
    </div>
}

// Bar间隔在列表项中间
// 里面还包含一个div元素,当拖拽起来的卡片悬浮到Bar上时,div高度变化撑起来 移走再次变为0
function Bar({ id, dragging, dragOver, eventHandlers }) {
    if (id === dragging + 1) { // 拖起的时候去掉一个bar
        return null;
    }

    return <div
        className={cls("draggable-bar", [[dragOver === id, "dragOver"]])}
        {...eventHandlers}>
        <div
            className="inner"
            style={{
                height: id === dragOver ? '80px' : '0px',
            }}
        />
    </div>
}

// 纯展示列表项的组件
function Card({ src, title }) {
    return <div className="card">
        <img src={src} />
        <span>{title}</span>
    </div>
}

function cls(def, conditions) {
    const list = [def];
    conditions.forEach(cond => {
        if (cond[0]) {
            list.push(cond[1])
        }
    })
    return list.join(" ")
}
  • 拖拽相关的逻辑全部维护在自定义hook中
    • 涉及到的拖拽事件:
      • onDragStart
      • onDragEnd
      • onDragOver
      • onDragLeave
      • onDrop
const DRAGGABLE = 'DRAGGABLE';
const BAR = 'BAR';

function draggable(item, id) {
    return {
        type: DRAGGABLE,
        id,
        data: item
    }
}

// 实现往列表项中增加Bar的方法, 将每一项都定义成一个对象,包含数据/ID
function insertBars(list) {
    let i = 0; //自增的ID

    // 用来产生一个新的bar的函数
    const newBar = () => {
        return {
            type: BAR,
            id: i++
        }
    }

    // 将 ABC => 转成 |A|B|C| 的插入bar的形式
    const arr = [newBar()].concat(
        ...list.map(item => {
            return [draggable(item, i++), newBar()]
        })
    )
    return arr;
}

// 计算最终列表项位置交换的方法
function clacChanging(list, drag, drop) {
    list = list.slice();
    const dragItem = list[drag];
    // dir > 0 从下往上 <0 从上往下
    const dir = drag > drop ? -2 : 2;
    // drop的地方是bar
    const end = dir > 0 ? drop - 1 : drop + 1;

    for (let i = drag; i != end; i += dir) {
        list[i] = list[i + dir]
    }
    list[end] = dragItem;

    return list;
}

// 传参list 就是要渲染的数据, 需要给list每条数据插上bar(insetBars方法)
function useDraggable(list) {
    // dragList是最终加完bar之后的list,也就是要渲染在页面上的list
    const [dragList, setDragList] = useState(insertBars(list));
    const [dragOver, setDragOver] = useState(null);
    const [dragging, setDragging] = useState(null);
    return {
        dragList,
        // 用来放置的属性
        createDropperProps: id => {
            return {
                // id,
                // key: id,
                dragging,
                dragOver,
                eventHandlers: {
                    onDragOver: e => {
                        e.preventDefault();
                        setDragOver(id);
                    },
                    onDragLeave: e => {
                        e.preventDefault();
                        setDragOver(null);
                    },
                    onDrop: e => {
                        console.log('zhixing ')
                        e.preventDefault();
                        setDragOver(null);
                        setDragList(list => {
                            return clacChanging(list, dragging, id)
                        })
                    }
                }
            }
        },
        // 生成用来拖拽的组件Draggable的属性
        createDraggerPorps: id => {
            return {
                id,
                key: id,
                dragging,
                eventHandlers: {
                    onDragStart: () => {
                        setDragging(id);
                    },
                    onDragEnd: () => {
                        setDragging(null);
                    }
                }
            }
        }
    };
}

  • 涉及到的样式
.card {
    display: flex;
    align-items: center;
    padding: 10px;
    box-shadow: grey 1px 2px 3px;
    cursor: pointer;
    user-select: none;
}

.card img {
    border-radius: 36px;
    width: 72px;
    height: 72px;
}

.card span {
    margin-left: 20px;
}

.draggable-bar {
    padding: 10px 0;
    transition: background-color 1s ease-out;
}
 .inner{
    transition: height 0.3s ease;
    margin: 10px 0;
    background-color: rgba(0,0,0,.1);
}

.draggable {
    background: white;
    transition: all 0.3s ease;
}

.dragging {
    opacity: 0; 
    position: fixed;
    width: 100%;
}