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 项目里,通过实现右边栏和底栏,掌握了如何实现水平、垂直的拖动交互操作。

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

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

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

相关推荐
空中海7 分钟前
04 工程化、质量体系与 React 生态
前端·ubuntu·react.js
空中海16 分钟前
03 性能、动画与 React Native 新架构
react native·react.js·架构
好运的阿财38 分钟前
OpenClaw工具拆解之host_workspace_write+host_workspace_edit
前端·javascript·人工智能·机器学习·ai编程·openclaw·openclaw工具
XiYang-DING1 小时前
JavaScript
开发语言·javascript·ecmascript
空中海2 小时前
02 React Native状态、导航、数据流与设备能力
javascript·react native·react.js
空中海2 小时前
02 状态、Hooks、副作用与数据流
开发语言·javascript·ecmascript
空中海3 小时前
04 React Native工程化、质量、发布与生态选型
javascript·react native·react.js
杨超凡3 小时前
豆包收费了?我特么自己用“意念”搓了一个!
javascript
龙猫里的小梅啊4 小时前
CSS(七)CSS列表控制
前端·css