首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

Figma数值输入框支持拖拽调整功能实现

编程知识
2024年07月18日 20:05

最近再研究Figma的一些功能设计, 对其中的数值输入框可以直接鼠标拖拽的这个设计印象非常深刻.
这里用了其他网友的一张动态截图演示一下效果.

实际这个拖拽的功能不止看到的这么简单, 在深度研究使用之后, 发现这个拖拽可以无限的拖动, 当鼠标超出网页后会自动回到另一端然后继续拖动, 而且按住shift键, 可以调整单次数值变化的间隔值为10, 细节非常的丰富.

这篇文章, 我们就来尝试实现一下这个支持拖拽调整数值的输入框组件.
实现基于: typescript + react + tailwindcss + shadcn-ui
实现的功能有:

  1. 组件支持自定义Label, 鼠标悬浮Label拖拽调整输入框的值
  2. 无限拖拽, 鼠标超出网页边界后自动从另一边出现
  3. 支持自定义缩放系数: 比如鼠标拖拽1px增加多少值, 缩放系数越大, 拖动单位像素增加的值越多
  4. 支持自定义调整间隔: 最终计算的值为间隔的整数倍
    下面是已经实现好的效果 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的不太一样:

  1. 没办法固定鼠标样式, 在鼠标悬浮到其他的元素上, 样式会根据被悬浮元素的样式展示
  2. 鼠标的位置没有限制, 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" 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端, 也是一样的.

From:https://www.cnblogs.com/cmen/p/18310452
本文地址: http://shuzixingkong.net/article/168
0评论
提交 加载更多评论
其他文章 浅谈:HTTP 和 HTTPS 通信原理
1.HTTP基本概念 1.1 HTTP是什么? HTTP&#160;(超文本传输协议)协议被用于在 Web 浏览器和网站服务器之间传递信息,&#160;HTTP&#160;协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了 Web 浏览器和网站服务器之间的传输报文,就可以直接读懂其中
浅谈:HTTP 和 HTTPS 通信原理 浅谈:HTTP 和 HTTPS 通信原理 浅谈:HTTP 和 HTTPS 通信原理
接口防刷!利用redisson快速实现自定义限流注解
问题: 在日常开发中,一些重要的对外接口,需要加上访问频率限制,以免造成资��损失。 如登录接口,当用户使用手机号+验证码登录时,一般我们会生成6位数的随机验证码,并将验证码有效期设置为1-3分钟,如果对登录接口不加以限制,理论上,通过技术手段,快速重试100000次,即可将验证码穷举出来。 解决思
接口防刷!利用redisson快速实现自定义限流注解
深入理解 Vue 3 组件通信
在 Vue 3 中,组件通信是一个关键的概念,它允许我们在组件之间传递数据和事件。本文将介绍几种常见的 Vue 3 组件通信方法,包括 props、emits、provide 和 inject、事件总线以及 Vuex 状态管理。 1. 使用 props 和 emits 进行父子组件通信 props
【漏洞分析】Li.Fi攻击事件分析:缺乏关键参数检查的钻石协议
背景信息 2024 年 7 月 16日,Li.Fi 协议遭受黑客攻击,漏洞成因是钻石协议中 diamond 合约新添加的 facet 合约没有对参数进行检查,导致 call 函数任意执行。且 diamond 合约拥有用户的 approve,所以攻击者可以构造恶意参数对用户资金进行转移。 攻击交易ht
【漏洞分析】Li.Fi攻击事件分析:缺乏关键参数检查的钻石协议 【漏洞分析】Li.Fi攻击事件分析:缺乏关键参数检查的钻石协议 【漏洞分析】Li.Fi攻击事件分析:缺乏关键参数检查的钻石协议
数据仓库建模工具之一——Hive学习第四天
Hive的基本操作 1.3HIve的表操作(接着昨天的继续学习) 1.3.2 显示表 show tables; show tables like &#39;u*&#39;; desc t_person; desc formatted students; // 更加详细 1.3.3 加载数据 1、使用
适用于 .NET 的现代化、流畅、可测试的HTTP客户端库
前言 今天大姚给大家分享一个.NET开源(MIT License)、免费、现代化、流畅、可测试、可移植的URL构建器和HTTP客户端库:Flurl。 项目介绍 Flurl是一个集现代性、流畅性、异步性、可测试性、可移植性于一身的URL构建器与HTTP客户端库。它提供了简洁的API,使得HTTP请求的
适用于 .NET 的现代化、流畅、可测试的HTTP客户端库
WCF异常System.ServiceModel.ProtocolException问题处理
现象: 最近遇到了WCF 服务无法调用的错误,异常如下。 System.ServiceModel.ProtocolException, System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934
玄机-第二章日志分析-apache日志分析
玄机-第二章日志分析-apache日志分析 简介 账号密码 root apacherizhi ssh root@IP 1、提交当天访问次数最多的IP,即黑客IP: 2、黑客使用的浏览器指纹是什么,提交指纹的md5: 3、查看index.php页面被访问的次数,提交次数: 4、查看黑客IP访问了多少次
玄机-第二章日志分析-apache日志分析 玄机-第二章日志分析-apache日志分析 玄机-第二章日志分析-apache日志分析