前言
大家好,我是馋嘴的猫。在在线编辑器越来越流行的趋势下,开发者已经对在浏览器操作 IDE 非常轻车熟路了。比如常见的VSCode 网页版,开发者就可以熟悉地在浏览器上,实现代码的开发。
让我们回想一下,使用 IDE 的时候,是不是有个非常高频的操作:手动拖动面板?通过改变编辑器的显示区域大小,来方便编程、调试等需要。这个熟悉得不能再熟悉的动态效果,在前端网页,又该如何实现呢?
今天,我们将会借助 React-Draggable 插件,在页面实现上述效果,请跟随下文,一起学起来吧~
需求
通过 React-Draggable
,在 Next.js 项目,实现一个可拖动的 IDE 面板。
使用框架
仓库地址
在线演示地址
实现步骤
- 在终端里初始化一个 Next.js 项目,并在 CLI 中选择使用 Tailwindcss 作为样式书写工具。
bash
pnpm create next-app
## 接下来的安装选项
## 省略其他的。。。
Would you like to use Tailwind CSS? No / Yes (此项选择yes)
- 在新建立的项目,修改
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>
)
}
- 在 app 目录下新建 components 目录,并添加三个文件,
canvas_container.tsx
、footer_bar.tsx
与right_sidebar.tsx
。
- 在 app 目录下新建 utils 目录,在其下新建
constant.ts
并填入以下代码:
tsx
export const DRAGGABLE_BORDER_WIDTH = 4
canvas_container.tsx
不是今天的重点,可直接参考源代码填写。- 现在,开始我们今天的重点,打开
right_sidebar.tsx
并准备编辑,开始实现可拖动的 IDE 面板。 - 为项目安装
react-draggable
依赖,执行以下命令:
bash
pnpm add react-draggable
- 我们先来实现无拖动功能的右边栏的样式,添加以下代码:
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,且不可拖动修改。
- 此时,让我们为其添加 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 面板。然而,如果还想更深入地了解,这个章节就是为你准备的!
我们将会在这个章节,以底栏为例,实现一个功能更丰富的可拖动面板,请跟着一起来实现吧!
-
在 public 目录下,新建
ide/footer
目录,并在里面放置两张图片,collapse.svg
和expand.svg
。图片可以在这里下载。 -
打开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>
)
}
- 在这段新添加的底栏代码片段,比起之前的右侧边栏,有什么不同点?
- 通过配置 Draggable 组件的
axis
属性为 "y", 来限制其只能纵向拖动
- 通过配置 Draggable 组件的
bounds
属性来限制纵向滑动的距离
- 通过配置 Draggable 组件的
onDrag
函数,实现监听拖动条的位移,来动态修改底栏的高度
-
通过底栏加入的
折叠/展开
按钮,实现一键折叠或展开底栏,比拖动更方便。当用户点击折叠按钮时,我们会实现以下几点:
- 保存当前底栏的高度,以便展开时恢复;
- 将底栏高度设置为仅底栏标题栏高度,这样就仅会显示标题栏了;
- 将draggable的位置调整到标题栏高度的偏移量。
当用户点击展开按钮时,我们会实现以下几点:
- 取出之前折叠时保存的高度,与最小限制底栏高度比较,取其中较大值;
- 将底栏高度设置为上一步骤计算的高度;
- 将draggable的位也调整到与底栏高度适配的偏移量。
- 拖动条在鼠标悬浮时才显示的实现,与上述右边栏教程类似,此处不再重复讲述,可直接参考源码查看~
至此,我们就实现了一个可拖动的 IDE 底栏,比右边栏更加复杂,并完成了以下几个功能点:
- 用户可以上下竖滑拖动边框,改变 IDE 底栏大小
- 上下竖滑拖动有大小限制,避免面积太小影响显示
- 用户可以点击折叠按钮,对面板进行折叠操作
- 用户可以点击展开按钮,面板将会展开为折叠前的高度(或最低高度,取决于折叠前高度是否大于最低高度)
- 只有鼠标悬浮在边框时,才会显示拖动条,提醒用户可拖动
在此,上图!这次是底栏的交互效果演示hhh
总结
在网页版 IDE 或者低代码编辑器的平台里,用户会有在不同显示场景下(如大屏显示器、笔记本等)开发的需要,而平台又因自身需要,得提供尽量多的功能面板,这个时候,通过用户拖拉面板改变其面积的功能,就是个很重要的 feature 了。
我们在这篇文章里,借助 React-Draggable,在 Next.js 项目里,通过实现右边栏和底栏,掌握了如何实现水平、垂直的拖动交互操作。
同时,伴随着这些交互操作的触发,我们还需要通知对应的显示面板去更改大小。
另外,优化点比如限制最小拖动的宽度、高度;实现面板的快速折叠、展开;拖动条仅在鼠标悬浮时显示;加大可拖动交互的区域方便操作,也是我们在实现此功能时,需要关注并实现的,它们会给用户带来更好的用户体验,对于一个高频的交互操作,这无形中会提升很多的用户好感,让用户更加喜欢使用你的产品~
欢迎多多点赞,收藏!如果有问题,也欢迎在评论区继续交流~