javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JS可拖拽容器布局组件

基于JS实现一个可拖拽的容器布局组件

作者:Yikuns

这篇文章主要为大家详细介绍了如何基于JavaScript实现一个可拖拽的容器布局组件,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

1. 前言

某一天,产品经理给我提了这样一个需求:产品概览页是一个三列布局的结构,我希望用户能够自己拖动列与列之间的分割线,实现每列的宽度自定义,国际站用户就经常有这样的需求。效果类似这样:

就这?简单啊,不就是拖拽吗?使用开源拖拽库,回调里面给相关容器设置一下宽度即可,几行代码就搞定了。...不对,这是新同学才应该有的想法,但我是一个老前端啊,后来我又想了一下,如果我实现了上面的功能,那两列布局、三列布局、不管几列布局都应该可以拖拽啊,那页面左边的菜单,右边弹出的抽屉也可以让用户拖拽啊,嗯...那就做成一个组件吧,让我们来优雅的实现它。

2. 组件分析

我们先分析一下,不管是两列布局、三列布局、菜单、抽屉,最后拖拽的其实都是一根线,所以首先我们需要封装一个拖拽线条的组件,有了这个组件,再实现任何布局拖拽宽度自定义的功能就简单很多了:

使用开源库还是自己实现,也是我考虑的一个问题,我最终还是选择了自己实现,原因主要有两点:第一是现在的开源得三方包体积都比较大,我们的业务组件是项目必须引用的资源,资源当然是越小越好;第二是我们这个功能比较简单,自己实现代码可控,还可以实用一些新特性让性能做到最优。

3. DragLine

DragLine组件主要包括哪些能力呢?

废话不多说,直接上拖拽线条组件DragLine的代码:

// DragLine.js
import React, { useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import './index.scss';


const DragLine = ((props) => {
  const {
    gap = 16,
    onMouseMove,
    onMouseUp,
    style = {},
    tipKey,
    defaultShowTip = false,
    ...rest
  } = props;
  const [visible, setVisible] = useState(defaultShowTip);
  const ref = useRef(null);
  const eventRef = useRef({});

  const closeNavTips = () => {
    localStorage.setItem(tipKey, 'true'); // 设置标记
    setVisible(false); // 关闭弹窗
  };

  // 拖拽结束
  const handleMouseUp = (e) => {
    document.body.classList.remove('dragging');
    onMouseUp && onMouseUp(e, ref.current);
    document.removeEventListener('mousemove', eventRef.current.mouseMoveHandler, false);
    document.removeEventListener('mouseup', eventRef.current.mouseUpHandler, false);
  };

  // 拖拽中
  const handleMouseMove = (e) => {
    onMouseMove && onMouseMove(e, ref.current);
  };

  // 开始拖拽
  const handleMouseDown = () => {
    closeNavTips();// 关闭拖拽提示框
    document.body.classList.add('dragging');
    eventRef.current.mouseMoveHandler = (e) => handleMouseMove(e);
    eventRef.current.mouseUpHandler = (e) => handleMouseUp(e);
    document.addEventListener('mousemove', eventRef.current.mouseMoveHandler, false);
    document.addEventListener('mouseup', eventRef.current.mouseUpHandler, false);
  };

  const line = (
    <div
      ref={ref}
      style={{
        '--drag-gap': `${gap}px`,
        ...style,
      }}
      className={`drag-line ${visible ? 'active' : ''}`}
      onMouseDown={handleMouseDown}
      {...rest}
    />
  );

  return visible ? (
    <Tooltip
      open
      placement="rightTop"
      title={(
        <div>
          <div style={{ marginBottom: 4 }}>拖动这根线试试~</div>
          <Button size="small" onClick={closeNavTips}>关闭</Button>
        </div>
      )}
    >
      {line}
    </Tooltip>
  ) : line;
});

export default DragLine;

对应的css代码如下:

/* index.scss */
.drag-line {
  width: 2px;
  margin: 0 calc((var(--drag-gap, 16px) - 2px) / 2);
  background: transparent;
  cursor: col-resize;

  &.active, &:hover {
    background: blue;
  }
}

.dragging {
  user-select: none; // 内容不可选择
}

上述代码,拷贝后可以直接运行,我简单说明其中几点:

4. DragContainer

有了 DragLine 这个基础组件后,我们就可以很容易的去扩展任何需要拖拽的上层组件了,比如我们来实现一个可拖拽的多列布局容器组件,直接上DragContainer组件的源码:

// DragContainer.js
import React, { useRef } from 'react';
import DragLine from '../DragLine';
import classnames from 'classnames';
import './index.scss';


const DragContainer = (props) => {
  const {
    className,
    sceneKey,
    minChildWidth = 150,
    contentList = [],
    gap = 16,
  } = props;

  const cls = classnames('drag-container', className);
  const ref = useRef(null);

  // 拖拽结束时,保存宽度信息
  const onMouseUp = () => {
    const widthList = contentList.map((_, i) => {
      const child = ref.current.querySelector(`.item${i}`);
      return `${child?.offsetWidth}px`;
    });
    localStorage.setItem(sceneKey, widthList.join('#'));
  };

  const onMouseMove = (event, node) => {
    const index = parseInt(node.getAttribute('data-index'));
    const leftElement = ref.current.querySelector(`.item${index}`);
    const rightElement = ref.current.querySelector(`.item${index + 1}`);

    // 拖动距离 = 分割线的位置 - 鼠标的位置
    const dragOffset = node.getBoundingClientRect().left - event.clientX;
    const newLeftChildWidth = leftElement.offsetWidth - dragOffset;
    const newRightChildWidth = rightElement.offsetWidth + dragOffset;

    if (newLeftChildWidth >= minChildWidth && newRightChildWidth >= minChildWidth) {
      ref.current.style.setProperty(`--drag-childWidth-${sceneKey}-${index}`, `${newLeftChildWidth}px`);
      ref.current.style.setProperty(`--drag-childWidth-${sceneKey}-${index + 1}`, `${newRightChildWidth}px`);
    }
  };


  const contentData = [];
  const localWidthList = localStorage.getItem(sceneKey)?.split('#') || []; // 获取本地已经保存的宽度信息
  contentList.forEach((d, i) => {
    contentData.push(
      <div
        key={`${sceneKey}_${i}`}
        className={`container-item item${i}`}
        style={{ flexBasis: `var(--drag-childWidth-${sceneKey}-${i}, ${localWidthList[i]})` }}
      >{d}
      </div>,
    );
    if (i < contentList.length - 1) {
      contentData.push(
        <DragLine
          key={`${sceneKey}_dragline_${i}`}
          onMouseMove={onMouseMove}
          onMouseUp={onMouseUp}
          tipKey="draggableContainerFlag"
          data-index={i}
          defaultShowTip={i === 0}
          gap={gap}
        />,
      );
    }
  });

  return (
    <div ref={ref} className={cls}>
      {contentData}
    </div>
  );
};


export default DragContainer;

对应样式文件如下:

/* index.scss */
.drag-container {
  display: flex;
  align-items: stretch;
  width: 100%;

  .container-item {
    height: 100%;
    overflow: hidden;
    flex: 1; // 同比例放大缩小
  }
}

DragContainer组件的实现逻辑也比较简单,基本思路如下:

5. 使用效果

我们在业务代码中使用DragContainer组件写个例子,使用简单,效果完美:

<DragContainer
  sceneKey="overview-page"
  contentList={[
    <Card>111</Card>,
    <Card>222</Card>,
    <Card>333</Card>,
  ]}
/>

6. 总结

其实本文我最想要表达的是,当我们接到一个需求之后,先学会分析和过滤,如果是特定的业务需求,实现即可,如果是通用类需求,就要慢慢学会从组件开发的角度去思考,是否能够举一反三,通过组件开发去覆盖解决更多的场景和问题。另外是在功能的实现方面,主要总结以下几点:

到此这篇关于基于JS实现一个可拖拽的容器布局组件的文章就介绍到这了,更多相关JS可拖拽容器布局组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文