react拖拽功能实现

因项目中有拖拽功能需求,于是乎在github上找到了react-beautiful-dnd这个react列表拖拽库帮助我们实现甬道间拖拽,下面介绍一下react-beautiful-dnd基本的几个API和实现方法。

DragDropContext

拖拽上下文。可拖拽的内容需包裹在DragDropContext中,DragDropContext不支持嵌套。

Props
type Hooks = {|
  // optional
  onDragBeforeStart?: OnDragBeforeStartHook,
  onDragStart?: OnDragStartHook,
  onDragUpdate?: OnDragUpdateHook,
  // required
  onDragEnd: OnDragEndHook,
|};

type OnBeforeDragStartHook = (start: DragStart) => mixed;
type OnDragStartHook = (start: DragStart, provided: HookProvided) => mixed;
type OnDragUpdateHook = (update: DragUpdate, provided: HookProvided) => mixed;
type OnDragEndHook = (result: DropResult, provided: HookProvided) => mixed;
  
type Props = {|
  ...Hooks,
  children: ?Node,
|};
基本用法
import { DragDropContext } from 'react-beautiful-dnd';

class App extends React.Component {
  onDragStart = () => {
    /*...*/
  };
  onDragUpdate = () => {
    /*...*/
  }
  onDragEnd = () => {
    // the only one that is required
  };

  render() {
    return (
      <DragDropContext
        onDragStart={this.onDragStart}
        onDragUpdate={this.onDragUpdate}
        onDragEnd={this.onDragEnd}
      >
        <div>Hello world</div>
      </DragDropContext>
    );
  }
}

Droppable

Droppable为放置拖拽元素的甬道,< Draggable/>必须包裹在<Droppable/>中。

Props
import type { Node } from 'react';

type Props = {|
  // required
  droppableId: DroppableId, // 必需,可拖动甬道的唯一标识
  // optional
  type?: TypeId, // string,用来简单的接受某一类draggable,当两个droppable的type值一样时,甬道内的draggable才能互相拖动
  mode?: DroppableMode, // 拖动模式,默认为standard(标准)模式,另一种模式为处理大量数据的virtual(虚拟)模式
  isDropDisabled?: boolean, // 用于控制拖动起来的draggable是否允许放到当前Droppable,默认为false(允许)
  isCombineEnabled?: boolean, // 是否允许draggable合并,默认false
  direction?: Direction,  // 可拖拽块在droppable上的移动方向,甬道为垂直的就为vertical(默认),水平的为horizontal
  ignoreContainerClipping?: boolean,
  renderClone?: DraggableChildrenFn, // virtual模式中需使用
  getContainerForClone?: () => HTMLElement,
  children: (DroppableProvided, DroppableStateSnapshot) => Node,
|};

type DroppableMode = 'standard' | 'virtual';
type Direction = 'horizontal' | 'vertical';

// DraggableChildrenFn: 需返回一个ReactElement
<Droppable droppableId="droppable-1">
  {(provided, snapshot) => ({
    /*...*/
  })}
</Droppable>;
placeholder

通常,我们需要将placeholder(<Droppable /> | DroppableProvided | placeholder)放入列表中,以便在拖动过程中根据需要在列表中插入空格。

<Droppable droppableId="droppable">
  {(provided, snapshot) => (
    <div ref={provided.innerRef} {...provided.droppableProps}>
      {/* Usually needed. But not for virtual lists! */}
      {provided.placeholder}
    </div>
  )}
</Droppable>

Tips: 在虚拟列表中我们不需要加入placeholder占位符,因为虚拟列表中我们不是基于可视项的集合大小来确定列表的尺寸,而是根据itemCount来计算的。(height = itemSize*itemCount)。对于虚拟列表,将我们自己的节点插入其中不会增加列表的大小。

Draggable

Draggable 为可拖拽的块,<Draggable/>必须放在<Droppable/>里。

Props
import type { Node } from 'react';

type Props = {|
  // required
  draggableId: DraggableId, // 可拖拽块的唯一标识id
  index: number, // index索引
  children: DraggableChildrenFn,
  // optional
  isDragDisabled: ?boolean, // 是否允许该draggable被拖动
  disableInteractiveElementBlocking: ?boolean,
  shouldRespectForcePress: ?boolean,
|};
基本用法
const getItems = count =>
  Array.from({ length: count }, (v, k) => k).map(k => ({
  id: `item-${k}`,
  content: `item-${k}`
}))

const grid = 8;

const getItemStyle = (isDragging, draggableStyle) => ({
  // some basic styles to make the items look a bit nicer
  userSelect: "none",
  padding: grid * 2,
  margin: `0 0 ${grid}px 0`,

  // change background color if dragging
  background: isDragging ? "lightgreen" : "grey",

  // styles we need to apply on draggables
  ...draggableStyle
});

const getListStyle = isDraggingOver => ({
  background: isDraggingOver ? "lightblue" : "lightgrey",
  padding: grid,
  width: 250
});

<DragDropContext onDragEnd={onDragEnd}>
   <Droppable droppableId="drop">
      {(provided, snapshot) => (
         <div
           {...provided.droppableProps}
           ref={provided.innerRef}
           style={getListStyle(snapshot.isDraggingOver)}
          >
           {getItems(10).map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
                {(provided, snapshot) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                    style={getItemStyle(
                      snapshot.isDragging,
                      provided.draggableProps.style
                    )}
                  >
                    {item.content}
                  </div>
                )}
              </Draggable>
            ))}
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>

拖拽图示

拖拽图示

virtual模式

当数据量足够大的时候,相应地渲染出来的dom也会足够的多, react-virtualized便是一个react长列表解决方案。

react-beattiful-dnd@12.0版本也增加了对虚拟列表的支持。

虚拟列表原理

虚拟列表通过判断并只加载当前视窗内的列表元素来解决海量数据列表。

  1. 自行引入 react-virtualizedreact-window,使用他们的一些支持虚拟列表的组件。

  2. 首先要把<Droppable/>的mode属性设为virtual(参考上文Droppable的props),告诉DragDropContext当前甬道为虚拟列表模式。

  3. 使用<Droppable/>renderCloneAPI 。在虚拟列表模式下,拖动时原始<Draggable/>会被删除,然后用renderClone克隆个新的放到容器元素中。

renderClone用法:

function List(props) {
  const items = props.items;

  return (
    <Droppable
      droppableId="droppable"
      renderClone={(provided, snapshot, rubric) => (
        <div
          {...provided.draggableProps}
          {...provided.dragHandleProps}
          ref={provided.innerRef}
        >
          Item id: {items[rubric.source.index].id}
        </div>
      )}
    >
      {provided => (
        <div ref={provided.innerRef} {...provided.droppableProps}>
          {items.map(item) => (
            <Draggable draggableId={item.id} index={item.index}>
              {(provided, snapshot) => (
                <div
                  {...provided.draggableProps}
                  {...provided.dragHandleProps}
                  ref={provided.innerRef}
                >
                  Item id: {item.id}
                </div>
              )}
            </Draggable>
          )}
        </div>
      )}
    </Droppable>
  );
}

const getRenderItem = (items) => (provided, snapshot, rubric) => (
  <div
    {...provided.draggableProps}
    {...provided.dragHandleProps}
    ref={provided.innerRef}
  >
    Item id: {items[rubric.source.index].id}
  </div>
);

function List(props) {
  const items = props.items;
  const renderItem = getRenderItem(items);

  return (
    <Droppable
      droppableId="droppable"
      renderClone={renderItem}
    >
      <div ref={provided.innerRef} {...provided.droppableProps}>
        {items.map(item) => (
          <Draggable draggableId={item.id} index={item.index}>
            {renderItem}
          </Draggable>
        )}
      </div>
    </Droppable>
  );
}

Tips: 在使用react-virtualized时,稍不注意会出现滚动出第一屏后页面闪烁的问题。

react-virtualized使用注意事项

拖动的原理——数组的重排

onDragEnd

该钩子是拖拽过程中最重要的一个函数,也是必需的,该函数必须导致列表数据的重新排序。它也提供来有关拖动的所有信息。

result:DropResult
type DropResult = {|
  ...DragUpdate,
  reason: DropReason,
|}

type DropReason = 'DROP' | 'CANCEL';
  • result.draggableId: 拖动的draggabledraggableId
  • result.type: 拖动的draggable的类型(type),droppable上设置的type值
  • result.source: draggable的起始位置(包含起始位置的index索引和droppableId)
  • result.destination: draggable完成的位置,如果用户在超过<Droppable/>的情况下掉落,则目标将为null(如果不为null,则包含结束位置的index索引和droppableId)
  • result.reason: 下降的原因
你需要做的
  • 如果result.destination为null,直接return;
  • 如果source.droppableIddestination.droppableId相等,则需要从列表中删除该项目并放置到正确的位置;
  • 如果source.droppableIddestination.droppableId不相等,则需要source.droppableId列表中删除该项目并放置到destination.droppableId正确的位置;

附上我的demo代码库,有兴趣的可以看看。demo代码库
另附上react-beautiful-dnd官方地址。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容