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官方地址。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343