Figma数值输入框支持拖拽调整功能实现
最近再研究Figma的一些功能设计, 对其中的数值输入框可以直接鼠标拖拽的这个设计印象非常深刻.
这里用了其他网友的一张动态截图演示一下效果.
实际这个拖拽的功能不止看到的这么简单, 在深度研究使用之后, 发现这个拖拽可以无限的拖动, 当鼠标超出网页后会自动回到另一端然后继续拖动, 而且按住shift键, 可以调整单次数值变化的间隔值为10, 细节非常的丰富.
这篇文章, 我们就来尝试实现一下这个支持拖拽调整数值的输入框组件.
实现基于: typescript + react + tailwindcss + shadcn-ui
实现的功能有:
- 组件支持自定义Label, 鼠标悬浮Label拖拽调整输入框的值
- 无限拖拽, 鼠标超出网页边界后自动从另一边出现
- 支持自定义缩放系数: 比如鼠标拖拽1px增加多少值, 缩放系数越大, 拖动单位像素增加的值越多
- 支持自定义调整间隔: 最终计算的值为间隔的整数倍
下面是已经实现好的效果 Figma-draggable-input
下面拆解一下组件的实现逻辑
简单的拖动更新数值实现
通常的拖动更新数值, 实现是现在元素上监听 mousedown, 然后在document上监听 mousemove 和 mouseup.
在 mousemove 中处理位置的计算更新逻辑, 参考draggable-input-v1.
首先, 构建state记录的输入框值和鼠标的位置,
const [snapshot, setSnapshot] = useState(value);
const [mousePos, setMousePos] = useState<number[] | null>(null);
当鼠标在左侧标签按下的时候, 记录鼠标的初始位置
const onDragStart = useCallback(
(position: number[]) => {
setMousePos(position);
},
[]
);
给label的jsx绑定mousedown事件
return (
<div
onMouseDown={(e) => {
onDragStart([e.clientX, e.clientY]);
}}
className="cursor-ew-resize absolute top-0 left-0 h-full flex items-center"
>
{label}
</div>
);
然后在document上监听, mousemove和mouseup事件, 用于拖拽更新数值
useEffect(() => {
// Only change the value if the drag was actually started.
const onUpdate = (event: MouseEvent) => {
const { clientX } = event;
if (mousePos) {
const newSnapshot = snapshot + clientX - mousePos[0];
onChange(newSnapshot);
}
};
// Stop the drag operation now.
const onEnd = (event: MouseEvent) => {
const { clientX } = event;
if (mousePos) {
const newSnapshot = snapshot + clientX - mousePos[0];
setSnapshot(newSnapshot);
setMousePos(null);
onChange(newSnapshot);
}
};
document.addEventListener('mousemove', onUpdate);
document.addEventListener('mouseup', onEnd);
return () => {
document.removeEventListener('mousemove', onUpdate);
document.removeEventListener('mouseup', onEnd);
};
}, [mousePos, onChange, snapshot]);
这个时候, 我们已经可以通过鼠标拖拽来调整数值了.
但是, 和Figma的不太一样:
- 没办法固定鼠标样式, 在鼠标悬浮到其他的元素上, 样式会根据被悬浮元素的样式展示
- 鼠标的位置没有限制, Figma的效果是鼠标拖拽在超出屏幕空间后从另一侧出现, 而我们目前的实现方式没办法动态修改鼠标的位置, 因为mosueEvent.clientX是只读属性.
无限拖动的调整数值实现
基于这些问题, 可以猜测, Figma的这种无限拖拽, 其实是隐藏了鼠标后, 用虚拟的光标模拟鼠标操作的.
我们仔细留意一下Figma的输入框拖拽按下的瞬间, 发现鼠标样式是有变化的, 进一步证明我们的猜想.
那么下面就是考虑, 如果用JS隐藏鼠标, 并且保证鼠标不会移出页面可见区域窗口.
这让我想到了, 在浏览一下Web3D效果的页面时, 鼠标点击后进入场景交互, 此时鼠标用于控制第一人称视角相机, 不论怎么拖动都不会跑到别的屏幕上.
于是问了一下Claude, 确实有这样的一个API, Element.requestPointerLock()
可以让我们锁定鼠标在某个元素内, 默认使用esc可以推出锁定, 也可以用 document.exitPointerLock() 手动退出锁定.
那么我们要做的就是在鼠标按下(mousedown)的时候, 进入锁定, 鼠标抬起(mouseup)的时候退出锁定.
参考案例里面, v2版本中draggable-label.tsx文件中的的部分代码
const onDragStart = useCallback(
(position: number[]) => {
document.body.requestPointerLock();
setMousePos(position);
},
[]
);
const onEnd = () => {
setMousePos(null);
setCursorPosition(null);
document.exitPointerLock();
};
解决了锁定鼠标的问题, 下一步就是虚拟光标模拟鼠标移动的实现.
这一步不算复杂, 我们找一个水平resize的光标对应的svg, 在需要的时候, 控制他显示然后调整位置即可.
这里我直接封装了一个hooks, 参考 use-ew-resize-cursor.tsx 的实现
import { useEffect, useState } from 'react';
const EWResizeCursorID = 'ZMeta_ew_resize_cursor';
export const useEWResizeCursor = () => {
const [position, setPosition] = useState<number[] | null>(null);
useEffect(() => {
let ewCursorEle = document.querySelector(
`#${EWResizeCursorID}`
) as HTMLDivElement;
if (position == null) {
ewCursorEle && document.body.removeChild(ewCursorEle);
return;
}
if (ewCursorEle == null) {
ewCursorEle = document.createElement('div');
ewCursorEle.id = EWResizeCursorID;
ewCursorEle.style.cssText =
'position: fixed; top: 0; left: 0;transform: translate3d(-50%, -50%, 0);';
ewCursorEle.innerHTML = `<svg t="1721283130691" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4450" width="32" height="32"><path d="M955.976575 533.675016l-166.175122 166.644747a28.148621 28.148621 0 0 1-39.845904 0c-10.945883-11.018133-10.945883-28.900021 0-39.954279l119.465463-119.826713H160.575743l119.465462 119.826713c11.018133 11.018133 11.018133 28.900021 0 39.954279a28.148621 28.148621 0 0 1-39.845904 0l-166.102872-166.644747c-5.888379-5.852254-8.381006-13.691385-8.019756-21.422141-0.36125-7.658506 2.131377-15.461511 8.019756-21.34989l166.102872-166.608622a28.148621 28.148621 0 0 1 39.845904 0c11.018133 11.018133 11.018133 28.900021 0 39.954279L160.575743 484.075355h708.845269l-119.465463-119.826713c-10.945883-11.018133-10.945883-28.900021 0-39.954279a28.148621 28.148621 0 0 1 39.845904 0l166.175122 166.608622c5.888379 5.888379 8.381006 13.691385 7.911381 21.34989 0.4335 7.730756-2.059127 15.569886-7.911381 21.422141z" fill="#bfbfbf" p-id="4451"></path></svg>`;
document.body.appendChild(ewCursorEle);
}
const [x, y] = position;
ewCursorEle.style.top = y + 'px';
ewCursorEle.style.left = x + 'px';
}, [position]);
return { setPosition };
};
实现的内容很简单, 当传入位置时, 手动构建一个svg的光标在body下, 然后动态设置top和left的值.
那么在mousemove 的时候, 获取鼠标的位移,然后更新给 useEWResizeCursor 即可.
参考我的实现是, mousedown的时候记录初始位置, 直接展示虚拟光标并更新位置.
const { setPosition: setCursorPosition } = useEWResizeCursor();
const [mousePos, setMousePos] = useState<number[] | null>(null);
// Start the drag to change operation when the mouse button is down.
const onDragStart = useCallback(
(position: number[]) => {
document.body.requestPointerLock();
setMousePos(position);
setSnapshot(value);
setCursorPosition(position);
},
[setCursorPosition, value]
);
同时记录 mousePos 即鼠标的实际位置, 在mousemove的时候继续使用.
这么做是因为, 当我们使用 requestPointerLock()之后, 鼠标的位置已经被锁定了, 我们在拖动鼠标时, mouseEvent.clientX等属性不会更新, 但是 mouseEvent.movementX 和 mosueEvent.movementY还是可以正常使用的.
因此我们需要自己记录并计算鼠标拖动的大概位置
const onUpdate = (event: MouseEvent) => {
const { movementX, movementY } = event;
if (mousePos) {
const newSnapshot = snapshot + movementX;
const [x, y] = mousePos;
const newMousePos = [x + movementX, y + movementY];
setMousePos(newMousePos);
setCursorPosition(newMousePos);
setSnapshot(newSnapshot);
onChange(newSnapshot);
}
};
这样, 鼠标拖动, 虚拟的鼠标位置会更新, 然后输入框的值也会更新.
但是还不能做到无限拖动, Figma的效果是, 当鼠标水平超出网页边界后, 会从另一端出来, 这样就可以周而复始的拖动.
那我们要做的就是限制虚拟的光标位置在一个范围内, 这一步其实也很简单, 我们只需要保证 mousePosX 在 (0, bodyWidth)之间即可.
我们可以封装一个小的方法, 来实现这个限制的功能
function calcAbsoluteRemainder(value: number, max: number) {
value = value % max;
return value < 0 ? value + max : value;
}
这样, 当鼠标的x位置超出屏幕右侧, 则会自动跳到左侧, 反之亦然.
那么我们的代码就可以简单的改动一下
const onUpdate = (event: MouseEvent) => {
const { movementX, movementY } = event;
if (mousePos) {
const newSnapshot = snapshot + movementX;
const [x, y] = mousePos;
const bodyWidth = document.documentElement.clientWidth;
const bodyHeight = document.documentElement.clientHeight;
const newX = calcAbsoluteRemainder(x + movementX, bodyWidth);
const newY = calcAbsoluteRemainder(y + movementY, bodyHeight);
const newMousePos = [newX, newY];
setMousePos(newMousePos);
setCursorPosition(newMousePos);
setSnapshot(newSnapshot);
onChange(newSnapshot);
}
};
这样就可以做到无限拖拽修改数值了.
现在的逻辑是, 每移动一个像素, 数值就加减1, 如果觉得这个更新的频率太快, 希望做到 每10个像素加减1, 我们可以再 补充一个 scale的参数, 在计算snapshot的时候, 用movementX * scale, 然后onchange的时候取整即可.
const newSnapshot = snapshot + movementX * scale;
onChange(Math.round(snapshot));
同样, 我想每次调整的间隔是10而不是1 (Figma安装shift拖动)
那么可以再定义一个step参数, 在onchange的时候, 对其做整数倍计算
const newValue = Math.round(newSnapshot);
onChange(step === 1 ? newValue : Math.floor(newValue / step) * step);
这里step为1的时候, 就直接返回取整后的值即可.
至此, 我们仿照Figma的拖拽功能已经全部实现, 看下最终效果, 简直一模一样.
这里我们注意到一个小细节, 即拖拽松开的时候, 鼠标是会回到起始按下的位置, 这点和figma一样.
而且, 第一次按下的时候, 因为锁定鼠标, 浏览器默认会提示 按住esc显示鼠标.
看了一下Figma的web端, 也是一样的.
Figma数值输入框支持拖拽调整功能实现的更多相关文章
- 两种为wangEditor添加拖拽调整高度的方式:CSS3和jQuery UI
wangEditor是一款优秀的Web富文本编辑器,但如果能像KindEditor那样支持拖拽调整高度就更好了.有两种方式可以为wangEditor添加这一功能,这里使用的wangEditor版本为2 ...
- GMF Q&A(1): 如何让palette支持拖拽(DnD)等10则
1,如何让palette支持拖拽(DnD) 在*PaletteFactory类中,把私有类NodeToolEntry 和LinkToolEntry的基类修改为PaletteToolEntry.并在构造 ...
- 一个可以自由定制外观、支持拖拽消除的MaterialDesign风格Android BadgeView
为了尊重作者,先放上链接:https://github.com/qstumn/BadgeView BadgeView 一个可以自由定制外观.支持拖拽消除的MaterialDesign风格Android ...
- Qt无边框窗体-最大化时支持拖拽还原
目录 一.概述 二.效果展示 三.demo制作 1.设计窗体 2.双击放大 四.拖拽 五.相关文章 原文链接:Markdown模板 一.概述 用Qt进行开发界面时,既想要实现友好的用户交互又想界面漂亮 ...
- .net mvc mssql easyui treegrid 及时 编辑 ,支持拖拽
这里提到了,1个问题,怎么扩展 Easyui 参见: http://blog.csdn.net/chenkai6529/article/details/17528833 @{ ViewBag.Titl ...
- angular-dragon-drop.js 双向数据绑定拖拽的功能
在做公司后台物流的时候,涉及到34个省市分为两个部分,一部分为配送区域,另一部分为非配送区域,想利用拖拽的功能来实现,最好两部分的数组能自动更新. 刚好找到angular-dragon-drop.js ...
- 关于安装了VMware tools后仍然不支持拖拽文件的问题
我在学校机房里面的redhat4上面安装了VMware tools之后能正常支持拖拽,但是我自己电脑上的却不支持,折腾了好久,网上找了很久也还是没有解决,不过发现了一些问题,总结如下:(当然我总结的这 ...
- RecyclerViewItemTouchHelperDemo【使用ItemTouchHelper进行拖拽排序功能】
版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 记录使用ItemTouchHelper对Recyclerview进行拖拽排序功能的实现. 效果图 代码分析 ItemTouchHel ...
- 让一个view 或者控件不支持拖拽
让一个view 或者控件不支持拖拽: dragView.userInteractionEnabled = NO;
- jQuery插件之路(三)——文件上传(支持拖拽上传)
好了,这次咱一改往日的作风,就不多说废话了,哈哈.先贴上源代码地址,点击获取.然后直接进入主题啦,当然,如果你觉得我有哪里写的不对或者欠妥的地方,欢迎留言指出.在附上一些代码之前,我们还是先来了解下, ...
随机推荐
- kubernetes的三种探针startupprobe,ReadinessProbe,LivenessProbe记录
kubernetes的三种探针 startupprobe: k8s1.16版本后新加的探测方式,用于判断容器内应用程序是否已经启动,如果配置了startuprobe,就会先禁用其他的探测,直到它成功为 ...
- Mysql 5.7 及以上版本修改密码
登录数据后.选择 mysql 数据库 use mysql; 修改密码 update user set authentication_string=PASSWORD("mynewpasswor ...
- react的类组件的ts写法
react的类组件的ts写法,声明的变量,props和state的写法 import React, { PureComponent } from 'react'; interface Iprops { ...
- ElasticSearch服务Java内存异常分析和排查解决
ElasticSearch服务Java内存异常分析和排查解决 1.ElasticSearch业务微服务日志排查java.lang.IllegalStateException: Request cann ...
- Interceptor拦截器demo
Interceptor拦截器demo ##接口测试类 @RestController public class TestController { @RequestMapping(value = &qu ...
- xxlJob需要拆分开来,不用公用同一个jobHandler
xxlJob需要拆分开来,不用公用同一个jobHandler 不能使用同一个jobHandler,通过使用不同的任务参数来定义两个不同的job,实际在xxlJob中使用jobHandler来注册的.解 ...
- JSR303数据校验使用方法记录
JSR303并不对应着指定的jar包,而是一种规范,目前hibernate-validator是使用最多的是基于JSR303规范的实现 本文不适合新人观看,要求至少要知道使用方法 Springboot ...
- Imdeploy笔记
Smiling & Weeping ---- 天气不好的时候,我会小心地把自己心上的裂缝补起来.为什么?... LMDeploy 的量化和部署 1 环境配置 2 服务部署 2.1 模型转换 2 ...
- CSS 属性计算
CSS 属性计算过程 你是否了解 CSS 的属性计算过程呢? 有的同学可能会讲,CSS属性我倒是知道,例如: p{ color : red; } 上面的 CSS 代码中,p 是元素选择器,color ...
- JS弱类型语言的优势——之模板字符串
ES6中,开始支持模板字符串. 尽管Java和C#这样的高级语言有非常多吸引人的地方,但是想js这样的弱类型语言,也有独到之处. equType:有四种类型,分别是:chl.chwp.cwp.cot, ...