React 拖拽功能 多个框之间的拖拽

首先,需要npm install @emotion/styled 和 react-beautiful-dnd 和 @emotion/react 这三个依赖

一、dragContent.js 这个文件:哪里需要哪里引入这个文件就行。

import React, { Component } from "react";
import styled from "@emotion/styled";
import { DragDropContext } from "react-beautiful-dnd";
import initial from "./data";
import Column from "./column";
import { mutliDragAwareReorder, multiSelectTo  } from "./utils";

const Container = styled.div`display: flex`,
    getTasks = (entities, columnId) =>
        entities.columns[columnId].taskIds.map((taskId) => entities.tasks[taskId]);
export default class TaskApp extends Component {
    state = {
        entities: initial,
        selectedTaskIds: [],
        draggingTaskId: null
    };

    componentDidMount () {
        window.addEventListener("click", this.onWindowClick);
        window.addEventListener("keydown", this.onWindowKeyDown);
        window.addEventListener("touchend", this.onWindowTouchEnd);
    }

    componentWillUnmount () {
        window.removeEventListener("click", this.onWindowClick);
        window.removeEventListener("keydown", this.onWindowKeyDown);
        window.removeEventListener("touchend", this.onWindowTouchEnd);
    }

    onDragStart = (start) => {
        const id = start.draggableId,
            selected = this.state.selectedTaskIds.find((taskId) => taskId === id);
        // if dragging an item that is not selected - unselect all items
        if (!selected) {
            this.unselectAll();
        }
        this.setState({
            draggingTaskId: start.draggableId
        });
    };

    onDragEnd = (result) => {
        const destination = result.destination,
            source = result.source;

        // nothing to do
        if (!destination || result.reason === "CANCEL") {
            this.setState({
                draggingTaskId: null
            });
            return;
        }
        const processed = mutliDragAwareReorder({
            entities: this.state.entities,
            selectedTaskIds: this.state.selectedTaskIds,
            source,
            destination
        });
        // processed.selectedTaskIds = [];
        this.setState({
            ...processed,
            draggingTaskId: null
        });
    };

    onWindowKeyDown = (event) => {
        if (event.defaultPrevented) {
            return;
        }

        if (event.key === "Escape") {
            this.unselectAll();
        }
    };

    onWindowClick = (event) => {
        if (event.defaultPrevented) {
            return;
        }
        this.unselectAll();
    };
    // touched
    onWindowTouchEnd = (event) => {
        if (event.defaultPrevented) {
            return;
        }
        this.unselectAll();
    };
    // click
    toggleSelection = (taskId) => {
        const selectedTaskIds = this.state.selectedTaskIds,
            wasSelected = selectedTaskIds.includes(taskId),

            newTaskIds = (() => {
                // Task was not previously selected
                // now will be the only selected item
                if (!wasSelected) {
                    return [taskId];
                }

                // Task was part of a selected group
                // will now become the only selected item
                if (selectedTaskIds.length > 1) {
                    return [taskId];
                }

                // task was previously selected but not in a group
                // we will now clear the selection
                return [];
            })();

        this.setState({
            selectedTaskIds: newTaskIds
        });
    };
    // ctrl
    toggleSelectionInGroup = (taskId) => {
        const selectedTaskIds = this.state.selectedTaskIds,
            index = selectedTaskIds.indexOf(taskId);
        // if not selected - add it to the selected items
        if (index === -1) {
            this.setState({
                selectedTaskIds: [...selectedTaskIds, taskId]
            });
            return;
        }

        // it was previously selected and now needs to be removed from the group
        const shallow = [...selectedTaskIds];
        shallow.splice(index, 1);
        this.setState({
            selectedTaskIds: shallow
        });
    };
    // shift
    // This behaviour matches the MacOSX finder selection
    multiSelectTo1 = (newTaskId) => {
        const updated = multiSelectTo(
            this.state.entities,
            this.state.selectedTaskIds,
            newTaskId
        );

        if (updated === null) {
            return;
        }

        this.setState({
            selectedTaskIds: updated
        });
    };

    unselect = () => {
        this.unselectAll();
    };

    unselectAll = () => {
        this.setState({
            selectedTaskIds: []
        });
    };

    render () {
        const entities = this.state.entities,
            selected = this.state.selectedTaskIds;
        return (
            <DragDropContext
                onDragStart={this.onDragStart}
                onDragEnd={this.onDragEnd}
            >
                <Container>
                    {entities.columnOrder.map((columnId) => (
                        <Column
                            column={entities.columns[columnId]}
                            tasks={getTasks(entities, columnId)}
                            selectedTaskIds={selected}
                            key={columnId}
                            draggingTaskId={this.state.draggingTaskId}
                            toggleSelection={this.toggleSelection}
                            toggleSelectionInGroup={this.toggleSelectionInGroup}
                            multiSelectTo={this.multiSelectTo1}
                        />
                    ))}
                </Container>
            </DragDropContext>
        );
    }
}

二、column.js 每一列:因为是多列互相拖拽

import React, { Component } from "react";
import styled from "@emotion/styled";
import memoizeOne from "memoize-one";
import { Droppable } from "react-beautiful-dnd";
import { grid, borderRadius } from "./constants";
import Task from "./task";


const Container = styled.div`
  width: 350px;
  height: 400px;
  overflow-y: auto;
  margin: ${grid}px;
  border-radius: ${borderRadius}px;
  border: 1px solid pink;
  background-color: lightblue;
  display: flex;
  flex-direction: column;
`,

    Title = styled.h3`
  font-weight: bold;
  padding: ${grid}px;
`,

    TaskList = styled.div`
  padding: ${grid}px;
  min-height: 200px;
  flex-grow: 1;
  transition: background-color 0.2s ease;
  ${(props) =>
        props.isDraggingOver ? "background-color: pink" : ""};
`,

    getSelectedMap = memoizeOne((selectedTaskIds) =>
        selectedTaskIds.reduce((previous, current) => {
            previous[current] = true;
            return previous;
        }, {})
    );

export default class Column extends Component {
    render () {
        const column = this.props.column,
            tasks = this.props.tasks,
            selectedTaskIds = this.props.selectedTaskIds,
            draggingTaskId = this.props.draggingTaskId;
        return (
            <Container>
                <Title>{column.title}</Title>
                <Droppable droppableId={column.id}>
                    {(provided, snapshot) => (
                        <TaskList
                            ref={provided.innerRef}
                            isDraggingOver={snapshot.isDraggingOver}
                            {...provided.droppableProps}
                        >
                            {tasks.map((task, index) => {
                                const isSelected = Boolean(getSelectedMap(selectedTaskIds)[task.id]),
                                    isGhosting = isSelected && Boolean(draggingTaskId) && draggingTaskId !== task.id;
                                return (
                                    <Task
                                        task={task}
                                        index={index}
                                        key={task.id}
                                        isSelected={isSelected}
                                        isGhosting={isGhosting}
                                        selectionCount={selectedTaskIds.length}
                                        toggleSelection={this.props.toggleSelection}
                                        toggleSelectionInGroup={this.props.toggleSelectionInGroup}
                                        multiSelectTo={this.props.multiSelectTo}
                                    />
                                );
                            })}
                            {provided.placeholder}
                        </TaskList>
                    )}
                </Droppable>
            </Container>
        );
    }
}

三、task.js

import React, { Component } from "react";
import styled from "@emotion/styled";
import { Draggable } from "react-beautiful-dnd";
import { grid, borderRadius } from "./constants";

const primaryButton = 0,

    getBackgroundColor = ({ isSelected, isGhosting }) => {
        if (isGhosting) {
            return "cyan";
        }

        if (isSelected) {
            return "pink";
        }

        return "cyan";
    },

    getColor = ({ isSelected, isGhosting }) => {
        if (isGhosting) {
            return "darkgrey";
        }
        if (isSelected) {
            return "red";
        }
        return "blue";
    },

    Container = styled.div`
    background-color: ${(props) => getBackgroundColor(props)};
    color: ${(props) => getColor(props)};
    padding: ${grid}px;
    margin-bottom: ${grid}px;
    border-radius: ${borderRadius}px;
    font-size: 18px;
    border: 3px solid yellow;
`,

    Content = styled.div``,
    size = 30,

    SelectionCount = styled.div`
    right: -${grid}px;
    top: -${grid}px;
    color: red;
    background: crimson;
    border-radius: 50%;
    height: ${size}px;
    width: ${size}px;
    line-height: ${size}px;
    position: absolute;
    text-align: center;
    font-size: 0.8rem;
`,

    keyCodes = {
        enter: 13,
        escape: 27,
        arrowDown: 40,
        arrowUp: 38,
        tab: 9
    };

export default class Task extends Component {
    onKeyDown = (event, provided, snapshot) => {
        if (event.defaultPrevented) {
            return;
        }

        if (snapshot.isDragging) {
            return;
        }

        if (event.keyCode !== keyCodes.enter) {
            return;
        }

        // we are using the event for selection
        event.preventDefault();

        this.performAction(event);
    };

    // Using onClick as it will be correctly
    // preventing if there was a drag
    onClick = (event) => {
        if (event.defaultPrevented) {
            return;
        }

        if (event.button !== primaryButton) {
            return;
        }

        // marking the event as used
        event.preventDefault();

        this.performAction(event);
    };

    onTouchEnd = (event) => {
        if (event.defaultPrevented) {
            return;
        }

        // marking the event as used
        // we would also need to add some extra logic to prevent the click
        // if this element was an anchor
        event.preventDefault();
        this.props.toggleSelectionInGroup(this.props.task.id);
    };

    // Determines if the platform specific toggle selection in group key was used
    wasToggleInSelectionGroupKeyUsed = (event) => {
        const isUsingWindows = navigator.platform.indexOf("Win") >= 0;
        return isUsingWindows ? event.ctrlKey : event.metaKey;
    };

    // Determines if the multiSelect key was used
    wasMultiSelectKeyUsed = (event) => event.shiftKey;

    performAction = (event) => {
        const {
            task,
            toggleSelection,
            toggleSelectionInGroup,
            multiSelectTo
        } = this.props;

        if (this.wasToggleInSelectionGroupKeyUsed(event)) {
            toggleSelectionInGroup(task.id);
            return;
        }

        if (this.wasMultiSelectKeyUsed(event)) {
            multiSelectTo(task.id);
            return;
        }

        toggleSelection(task.id);
    };

    render () {
        const task = this.props.task,
            index = this.props.index,
            isSelected = this.props.isSelected,
            selectionCount = this.props.selectionCount,
            isGhosting = this.props.isGhosting;
        return (
            <Draggable draggableId={task.id} index={index}>
                {(provided, snapshot) => {
                    const shouldShowSelection = snapshot.isDragging && selectionCount > 1;

                    return (
                        <Container
                            ref={provided.innerRef}
                            {...provided.draggableProps}
                            {...provided.dragHandleProps}
                            onClick={this.onClick}
                            onTouchEnd={this.onTouchEnd}
                            onKeyDown={(event) => this.onKeyDown(event, provided, snapshot)}
                            isDragging={snapshot.isDragging}
                            isSelected={isSelected}
                            isGhosting={isGhosting}
                        >
                            <Content>{task.content}</Content>
                            {shouldShowSelection ? (
                                <SelectionCount>{selectionCount}</SelectionCount>
                            ) : null}
                        </Container>
                    );
                }}
            </Draggable>
        );
    }
}

四、data.js 模拟的数据

const tasks = Array.from({ length: 20 }, (v, k) => k).map((val) => ({
        id: `task-${val}`,
        content: `TaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTaskTask ${val}`
    })),

    taskMap = tasks.reduce((previous, current) => {
        previous[current.id] = current;
        return previous;
    }, {}),

    todo = {
        id: "todo",
        title: "To do",
        taskIds: tasks.map((task) => task.id)
    },

    done = {
        id: "done",
        title: "Done",
        taskIds: []
    },

    entities = {
        columnOrder: [todo.id, done.id],
        columns: {
            [todo.id]: todo,
            [done.id]: done
        },
        tasks: taskMap
    };

export default entities;

五、utils.js 提取的方法

// import { invariant } from "react-beautiful-dnd";
import reorder from "./reorder";

const withNewTaskIds = (column, taskIds) => ({
        id: column.id,
        title: column.title,
        taskIds
    }),

    reorderSingleDrag = ({
        entities,
        selectedTaskIds,
        source,
        destination
    }) => {
    // moving in the same list
        if (source.droppableId === destination.droppableId) {
            const column = entities.columns[source.droppableId],
                reordered = reorder(column.taskIds, source.index, destination.index),

                updated = {
                    ...entities,
                    columns: {
                        ...entities.columns,
                        [column.id]: withNewTaskIds(column, reordered)
                    }
                };

            return {
                entities: updated,
                selectedTaskIds
            };
        }

        // moving to a new list
        const home = entities.columns[source.droppableId],
            foreign = entities.columns[destination.droppableId],

            // the id of the task to be moved
            taskId = home.taskIds[source.index],

            // remove from home column
            newHomeTaskIds = [...home.taskIds];
        newHomeTaskIds.splice(source.index, 1);

        // add to foreign column
        const newForeignTaskIds = [...foreign.taskIds];
        newForeignTaskIds.splice(destination.index, 0, taskId);

        const updated = {
            ...entities,
            columns: {
                ...entities.columns,
                [home.id]: withNewTaskIds(home, newHomeTaskIds),
                [foreign.id]: withNewTaskIds(foreign, newForeignTaskIds)
            }
        };

        return {
            entities: updated,
            selectedTaskIds
        };
    };

export const getHomeColumn = (entities, taskId) => {
    const columnId = entities.columnOrder.find((id) => {
        const column = entities.columns[id];
        return column.taskIds.includes(taskId);
    });

    // invariant(columnId, "Count not find column for task");

    return entities.columns[columnId];
};

const reorderMultiDrag = ({
    entities,
    selectedTaskIds,
    source,
    destination
}) => {
    const start = entities.columns[source.droppableId],
        dragged = start.taskIds[source.index],

        insertAtIndex = (() => {
            const destinationIndexOffset = selectedTaskIds.reduce(
                    (previous, current) => {
                        if (current === dragged) {
                            return previous;
                        }

                        const final = entities.columns[destination.droppableId],
                            column = getHomeColumn(entities, current);

                        if (column !== final) {
                            return previous;
                        }

                        const index = column.taskIds.indexOf(current);

                        if (index >= destination.index) {
                            return previous;
                        }

                        // the selected item is before the destination index
                        // we need to account for this when inserting into the new location
                        return previous + 1;
                    },
                    0
                ),

                result = destination.index - destinationIndexOffset;
            return result;
        })(),

        // doing the ordering now as we are required to look up columns
        // and know original ordering
        orderedSelectedTaskIds = [...selectedTaskIds],
        // 不需要重新排序
        // orderedSelectedTaskIds.sort((a, b) => {
        //     // moving the dragged item to the top of the list
        //     if (a === dragged) {
        //         return -1;
        //     }
        //     if (b === dragged) {
        //         return 1;
        //     }

        //     // sorting by their natural indexes
        //     const columnForA = getHomeColumn(entities, a),
        //         indexOfA = columnForA.taskIds.indexOf(a),
        //         columnForB = getHomeColumn(entities, b),
        //         indexOfB = columnForB.taskIds.indexOf(b);

        //     if (indexOfA !== indexOfB) {
        //         return indexOfB - indexOfA;
        //     }

        //     // sorting by their order in the selectedTaskIds list
        //     return -1;
        // });
        // we need to remove all of the selected tasks from their columns
        withRemovedTasks = entities.columnOrder.reduce((previous, columnId) => {
            const column = entities.columns[columnId],

                // remove the id's of the items that are selected
                remainingTaskIds = column.taskIds.filter(
                    (id) => !selectedTaskIds.includes(id)
                );

            previous[column.id] = withNewTaskIds(column, remainingTaskIds);
            return previous;
        }, entities.columns),

        final = withRemovedTasks[destination.droppableId],
        withInserted = (() => {
            const base = [...final.taskIds];
            base.splice(insertAtIndex, 0, ...orderedSelectedTaskIds);
            return base;
        })(),

        // insert all selected tasks into final column
        withAddedTasks = {
            ...withRemovedTasks,
            [final.id]: withNewTaskIds(final, withInserted)
        },

        updated = {
            ...entities,
            columns: withAddedTasks
        };

    return {
        entities: updated,
        selectedTaskIds: orderedSelectedTaskIds
    };
};

export const mutliDragAwareReorder = (args) => {
    if (args.selectedTaskIds.length > 1) {
        return reorderMultiDrag(args);
    }
    return reorderSingleDrag(args);
};

export const multiSelectTo = (entities, selectedTaskIds, newTaskId) => {
    // Nothing already selected
    if (!selectedTaskIds.length) {
        return [newTaskId];
    }

    const columnOfNew = getHomeColumn(entities, newTaskId),
        indexOfNew = columnOfNew.taskIds.indexOf(newTaskId),

        lastSelected = selectedTaskIds[selectedTaskIds.length - 1],
        columnOfLast = getHomeColumn(entities, lastSelected),
        indexOfLast = columnOfLast.taskIds.indexOf(lastSelected);

    // multi selecting to another column
    // select everything up to the index of the current item
    if (columnOfNew !== columnOfLast) {
        return columnOfNew.taskIds.slice(0, indexOfNew + 1);
    }

    // multi selecting in the same column
    // need to select everything between the last index and the current index inclusive

    // nothing to do here
    if (indexOfNew === indexOfLast) {
        return null;
    }

    const isSelectingForwards = indexOfNew > indexOfLast,
        start = isSelectingForwards ? indexOfLast : indexOfNew,
        end = isSelectingForwards ? indexOfNew : indexOfLast,

        inBetween = columnOfNew.taskIds.slice(start, end + 1),

        // everything inbetween needs to have it's selection toggled.
        // with the exception of the start and end values which will always be selected

        toAdd = inBetween.filter((taskId) => {
            // if already selected: then no need to select it again
            if (selectedTaskIds.includes(taskId)) {
                return false;
            }
            return true;
        }),

        sorted = isSelectingForwards ? toAdd : [...toAdd].reverse(),
        combined = [...selectedTaskIds, ...sorted];

    return combined;
};

六、reorder.js 同样也是提取的方法

const reorder = (list, startIndex, endIndex) => {
    const result = Array.from(list),
        [removed] = result.splice(startIndex, 1);
    result.splice(endIndex, 0, removed);

    return result;
};

export default reorder;

export const reorderQuoteMap = ({ quoteMap, source, destination }) => {
    const current = [...quoteMap[source.droppableId]],
        next = [...quoteMap[destination.droppableId]],
        target = current[source.index];

    // moving to same list
    if (source.droppableId === destination.droppableId) {
        const reordered = reorder(current, source.index, destination.index),
            result = {
                ...quoteMap,
                [source.droppableId]: reordered
            };
        return {
            quoteMap: result
        };
    }

    // moving to different list

    // remove from original
    current.splice(source.index, 1);
    // insert into next
    next.splice(destination.index, 0, target);

    const result = {
        ...quoteMap,
        [source.droppableId]: current,
        [destination.droppableId]: next
    };

    return {
        quoteMap: result
    };
};

export function moveBetween ({ list1, list2, source, destination }) {
    const newFirst = Array.from(list1.values),
        newSecond = Array.from(list2.values),

        moveFrom = source.droppableId === list1.id ? newFirst : newSecond,
        moveTo = moveFrom === newFirst ? newSecond : newFirst,

        [moved] = moveFrom.splice(source.index, 1);
    moveTo.splice(destination.index, 0, moved);

    return {
        list1: {
            ...list1,
            values: newFirst
        },
        list2: {
            ...list2,
            values: newSecond
        }
    };
}

七、constants.js 这个文件可以不需要,就是存了两个变量,看个人需求

export const grid = 8;
export const borderRadius = 2;

工程目录 ,在你需要的地方讲dragContent.js作为组件引入即可。

React 拖拽功能 多个框之间的拖拽_第1张图片

效果图: 大致这样,从todo 拖到 done

React 拖拽功能 多个框之间的拖拽_第2张图片

demo的地址:https://codesandbox.io/s/multi-drag-forked-vvrdwt?file=/src/constants.js:0-54 看看这个效果

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