5分钟,用 React-Draggable 做一个可拖动的 IDE 面板

前言

大家好,我是馋嘴的猫。在在线编辑器越来越流行的趋势下,开发者已经对在浏览器操作 IDE 非常轻车熟路了。比如常见的VSCode 网页版,开发者就可以熟悉地在浏览器上,实现代码的开发。

让我们回想一下,使用 IDE 的时候,是不是有个非常高频的操作:手动拖动面板?通过改变编辑器的显示区域大小,来方便编程、调试等需要。这个熟悉得不能再熟悉的动态效果,在前端网页,又该如何实现呢?

今天,我们将会借助 React-Draggable 插件,在页面实现上述效果,请跟随下文,一起学起来吧~

需求

通过 React-Draggable,在 Next.js 项目,实现一个可拖动的 IDE 面板。

使用框架

  1. React-Draggable
  2. Next.js
  3. Tailwindcss

仓库地址

点此查看

在线演示地址

点此查看

实现步骤

  1. 在终端里初始化一个 Next.js 项目,并在 CLI 中选择使用 Tailwindcss 作为样式书写工具。
bash 复制代码
pnpm create next-app

## 接下来的安装选项
## 省略其他的。。。
Would you like to use Tailwind CSS? No / Yes (此项选择yes)
  1. 在新建立的项目,修改app/page.tsx,为页面添加画布,底栏与右侧边栏,代码如下所示:
tsx 复制代码
import RightSidebar from '@/app/components/right_sidebar'
import FooterBar from '@/app/components/footer_bar'
import CanvasContainer from '@/app/components/canvas_container'

export default function Home() {
  return (
    <div className={'w-[100vw] h-[100vh] flex overflow-hidden bg-[#EDEFF3]'}>
      <div className={'flex flex-1 flex-col'}>
        <CanvasContainer />
        <FooterBar />
      </div>
      <RightSidebar />
    </div>
  )
}
  1. 在 app 目录下新建 components 目录,并添加三个文件,canvas_container.tsxfooter_bar.tsxright_sidebar.tsx
  1. 在 app 目录下新建 utils 目录,在其下新建 constant.ts 并填入以下代码:
tsx 复制代码
export const DRAGGABLE_BORDER_WIDTH = 4
  1. canvas_container.tsx 不是今天的重点,可直接参考源代码填写。
  2. 现在,开始我们今天的重点,打开 right_sidebar.tsx 并准备编辑,开始实现可拖动的 IDE 面板。
  3. 为项目安装react-draggable依赖,执行以下命令:
bash 复制代码
pnpm add react-draggable
  1. 我们先来实现无拖动功能的右边栏的样式,添加以下代码:
tsx 复制代码
'use client'
import Draggable from 'react-draggable'
import { useState, useRef } from 'react'
import { DRAGGABLE_BORDER_WIDTH } from '@/app/utils/constant'

const INITIAL_SIDEBAR_WIDTH = 400
const MINIMUM_SIDEBAR_WIDTH = 300

interface IRightSidebarProps {}

export default function RightSidebar(props: IRightSidebarProps) {
  const [width, setWidth] = useState(INITIAL_SIDEBAR_WIDTH)
  const nodeRef = useRef(null)
  return (
    <div className="h-full flex flex-col bg-transparent relative">
      <div
        style={{ width: `${width}px` }}
        className={`h-full flex flex-col border-l-[1px] bg-white p-[12px]`}
      >
        This is the right sidebar.
      </div>
    </div>
  )
}

查看网页效果,右边侧边栏已经显示出来了,但此时宽度为初始化的 400 px,且不可拖动修改。

  1. 此时,让我们为其添加 react-draggable 插件,并实现拖动效果,修改 render 函数返回结果如下:
tsx 复制代码
return (
  <div className="h-full flex flex-col bg-transparent relative">
    {/*用 react-draggable 实现可拖动*/}
    <div className={`w-[4px] flex absolute right-[400px] bottom-0 top-0 bg-transparent`}>
      <Draggable
        axis="x"
        nodeRef={nodeRef}
        bounds={{ right: INITIAL_SIDEBAR_WIDTH - MINIMUM_SIDEBAR_WIDTH }}
        onDrag={(e, data) => {
          const newWidth = INITIAL_SIDEBAR_WIDTH - data.x
          setWidth(newWidth)
        }}
      >
        <div
          ref={nodeRef}
          className={`h-full cursor-ew-resize bg-transparent hover:bg-gray-400 w-[${DRAGGABLE_BORDER_WIDTH}px]`}
        ></div>
      </Draggable>
    </div>
    <div
      style={{ width: `${width}px` }}
      className={`h-full flex flex-col border-l-[1px] bg-white p-[12px]`}
    >
      This is the right sidebar.
    </div>
  </div>
)

我们在这段新添加的代码片段,实现了什么功能?

  • 引入了 React-Draggable,使其 children div 可拖动,实现拖动条的功能。
  • 通过配置 Draggable 组件的 axis 属性为 "x", 来限制其只能横向拖动
  • 通过配置 Draggable 组件的 bounds 属性来限制横向滑动的距离
  • 通过配置 Draggable 组件的 onDrag 函数,实现监听拖动条的位移,来动态修改右边栏的宽度,进而实现我们最初的目标:可拖动的 IDE 面板。
  • 为了方便用户的拖动,我们需要将其的可拖动区域尽量设置大一点,在此处,我们通过 Tailwindcss,为拖动条设置了4px的宽度,并且加上了cursor: ew-resize的样式,示意其可以横向拉动。

但是,4px的拖动条宽度,如果直接常驻显示,会显得边框非常的粗,视觉效果不够精致。因此,我们还需要加上优化:平时隐藏,仅在鼠标悬浮在上面才显示。这个也很好实现,使用tailwindcss的hover伪类实现即可。

需要注意的是,上面的代码,会将拖动条在默认状态下隐藏,所以,为了最佳的视觉体验,我们还需要加上为右边栏加上1px的常显边距,也使用 TailwindCss 实现即可。

至此,我们就实现好了一个可拖动的ide面板,完成了以下几个功能点:

  • 用户可以左右横滑拖动边框,改变ide右边栏大小
  • 左右横滑拖动有大小限制,避免面积太小影响显示
  • 只有鼠标悬浮在边框时,才会显示拖动条,提醒用户可拖动

上图!非常流畅的交互效果hhh

扩展实现

在跟随上面的步骤实践后,我们已经完整的实现了一个可拖动的 ide 面板。然而,如果还想更深入地了解,这个章节就是为你准备的!

我们将会在这个章节,以底栏为例,实现一个功能更丰富的可拖动面板,请跟着一起来实现吧!

  1. 在 public 目录下,新建 ide/footer目录,并在里面放置两张图片,collapse.svgexpand.svg。图片可以在这里下载。

  2. 打开footer_bar.tsx, 加入以下代码:

tsx 复制代码
'use client'
import Draggable from 'react-draggable'
import { useState, useRef, useMemo } from 'react'
import Image from 'next/image'
import { DRAGGABLE_BORDER_WIDTH } from '@/app/utils/constant'

const INITIAL_FOOTER_HEIHGT = 250
const MINIMUM_FOOTER_HEIGHT = 150
const TITLE_BAR_HEIGHT = 32
const EXPAND_SVG = '/ide/footer/expand.svg'
const COLLAPSE_SVG = '/ide/footer/collapse.svg'

const TITLE_NAME = 'Footer bar'

interface IFooterBarProps {}

export default function FooterBar(props: IFooterBarProps) {
  const [height, setHeight] = useState(INITIAL_FOOTER_HEIHGT)
  const [lastHeight, setLastHeight] = useState(INITIAL_FOOTER_HEIHGT)
  const [footerPosition, setFooterPosition] = useState({ x: 0, y: 0 })
  const footerNodeRef = useRef(null)

  const isMinFooterHeight = useMemo(() => {
    return height <= TITLE_BAR_HEIGHT
  }, [height])

  return (
    <div className="w-full flex flex-col bg-transparent relative">
      <div
        className={`h-[4px] flex absolute right-0 bottom-[250px] left-0 flex-col bg-transparent`}
      >
        <Draggable
          axis="y"
          nodeRef={footerNodeRef}
          position={footerPosition}
          bounds={{ bottom: INITIAL_FOOTER_HEIHGT - TITLE_BAR_HEIGHT }}
          onDrag={(e, data) => {
            const newHeight = INITIAL_FOOTER_HEIHGT - data.y
            setFooterPosition({ x: 0, y: data.y })
            setHeight(newHeight)
          }}
        >
          <div
            ref={footerNodeRef}
            className={`w-full cursor-ns-resize bg-transparent hover:bg-gray-400 h-[${DRAGGABLE_BORDER_WIDTH}px]`}
          ></div>
        </Draggable>
      </div>
      <div
        style={{ height: `${height}px` }}
        className={`w-full flex flex-col bg-white border-t-[1px]`}
      >
        <div
          className={`h-[32px] w-full flex items-center px-[12px] border-b-[1px] justify-between`}
        >
          <p className={'text-title text-[14px] font-bold select-none'}>{TITLE_NAME}</p>
          <div
            className={
              'cursor-pointer w-[32px] h-[32px] flex align-center justify-center select-none'
            }
            onClick={() => {
              if (isMinFooterHeight) {
                // 展开逻辑,在展开时恢复原有的高度
                // 为了防止上次折叠时高度太低,展开效果不佳,限制一个展开高度的最小值
                // Expand logic, restore the original height when expanding
                // To prevent the height from being too low since last collapsed, which might cause the expansion effect is not good,
                // we add a limit of minimum expansion height here.
                let finalHeight =
                  lastHeight < MINIMUM_FOOTER_HEIGHT ? MINIMUM_FOOTER_HEIGHT : lastHeight
                setLastHeight(finalHeight)
                setHeight(finalHeight)
                setFooterPosition({ x: 0, y: INITIAL_FOOTER_HEIHGT - finalHeight })
              } else {
                // 收起逻辑
                // 保存在收起前的高度,用于展开时恢复高度
                // Collapse logic
                // Save the height before collapsing, and use it to restore the height when expanding
                setLastHeight(height)
                setHeight(TITLE_BAR_HEIGHT)
                setFooterPosition({ x: 0, y: INITIAL_FOOTER_HEIHGT - TITLE_BAR_HEIGHT })
              }
            }}
          >
            {isMinFooterHeight ? (
              <Image width={20} height={20} src={EXPAND_SVG} alt={'Expand'} />
            ) : (
              <Image width={20} height={20} src={COLLAPSE_SVG} alt={'Collapse'} />
            )}
          </div>
        </div>
        <div className={'flex-1 w-full flex'}>
          <div className={'flex-1 w-full h-full p-[12px]'}>This is the footer content</div>
        </div>
      </div>
    </div>
  )
}
  1. 在这段新添加的底栏代码片段,比起之前的右侧边栏,有什么不同点?
  • 通过配置 Draggable 组件的 axis 属性为 "y", 来限制其只能纵向拖动
  • 通过配置 Draggable 组件的 bounds 属性来限制纵向滑动的距离
  • 通过配置 Draggable 组件的 onDrag 函数,实现监听拖动条的位移,来动态修改底栏的高度
  • 通过底栏加入的折叠/展开按钮,实现一键折叠或展开底栏,比拖动更方便。

    当用户点击折叠按钮时,我们会实现以下几点:

    1. 保存当前底栏的高度,以便展开时恢复;
    2. 将底栏高度设置为仅底栏标题栏高度,这样就仅会显示标题栏了;
    3. 将draggable的位置调整到标题栏高度的偏移量。

当用户点击展开按钮时,我们会实现以下几点:

  1. 取出之前折叠时保存的高度,与最小限制底栏高度比较,取其中较大值;
  2. 将底栏高度设置为上一步骤计算的高度;
  3. 将draggable的位也调整到与底栏高度适配的偏移量。
  • 拖动条在鼠标悬浮时才显示的实现,与上述右边栏教程类似,此处不再重复讲述,可直接参考源码查看~

至此,我们就实现了一个可拖动的 IDE 底栏,比右边栏更加复杂,并完成了以下几个功能点:

  • 用户可以上下竖滑拖动边框,改变 IDE 底栏大小
  • 上下竖滑拖动有大小限制,避免面积太小影响显示
  • 用户可以点击折叠按钮,对面板进行折叠操作
  • 用户可以点击展开按钮,面板将会展开为折叠前的高度(或最低高度,取决于折叠前高度是否大于最低高度)
  • 只有鼠标悬浮在边框时,才会显示拖动条,提醒用户可拖动

在此,上图!这次是底栏的交互效果演示hhh

总结

在网页版 IDE 或者低代码编辑器的平台里,用户会有在不同显示场景下(如大屏显示器、笔记本等)开发的需要,而平台又因自身需要,得提供尽量多的功能面板,这个时候,通过用户拖拉面板改变其面积的功能,就是个很重要的 feature 了。

我们在这篇文章里,借助 React-Draggable,在 Next.js 项目里,通过实现右边栏和底栏,掌握了如何实现水平、垂直的拖动交互操作。

同时,伴随着这些交互操作的触发,我们还需要通知对应的显示面板去更改大小。

另外,优化点比如限制最小拖动的宽度、高度;实现面板的快速折叠、展开;拖动条仅在鼠标悬浮时显示;加大可拖动交互的区域方便操作,也是我们在实现此功能时,需要关注并实现的,它们会给用户带来更好的用户体验,对于一个高频的交互操作,这无形中会提升很多的用户好感,让用户更加喜欢使用你的产品~

欢迎多多点赞,收藏!如果有问题,也欢迎在评论区继续交流~

相关推荐
一只搬砖的猹29 分钟前
cJson系列——常用cJson库函数
linux·前端·javascript·python·物联网·mysql·json
CodeClimb42 分钟前
【华为OD-E卷-租车骑绿道 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
"追风者"1 小时前
前端(八)js介绍(1)
前端·javascript
博客zhu虎康1 小时前
用 ElementUI 的日历组件 Calendar 自定义渲染
前端·javascript·elementui
叶浩成5201 小时前
elementUI——upload限制图片或者文件只能上传一个——公开版
前端·javascript·elementui
丁总学Java1 小时前
去除 el-input 输入框的边框(element-ui@2.15.13)
javascript·vue.js·elementui
yqcoder1 小时前
同源策略详解
xml·前端·javascript
GISer_Jing1 小时前
Vue3知识弥补漏洞——性能优化篇
javascript·vue.js·性能优化·vue
姬嘉晗-19期-河北工职大2 小时前
Ajax中的axios
前端·javascript·ajax
zhenryx2 小时前
微涉全栈(react,axios,node,mysql)
前端·mysql·react.js