首页 > 其他分享 >Figma数值输入框支持拖拽调整功能实现

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

时间:2024-07-18 21:08:02浏览次数:6  
标签:const 鼠标 拖动 Figma newSnapshot 输入框 document 拖拽

最近再研究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
      onm ouseDown={(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" 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端, 也是一样的.

标签:const,鼠标,拖动,Figma,newSnapshot,输入框,document,拖拽
From: https://www.cnblogs.com/cmen/p/18310452

相关文章

  • input输入框 防抖
    1什么是防抖防抖:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始计时。为了避免用户在输入过程中,还没输入完,下面搜索框就一直频繁的在搜索,体验很差。2防抖解决方案首先需要把input的双向绑定v-mode拆......
  • 转 | element-ui组件table去除下方滚动条,实现鼠标左右拖拽移动表格
    看起来是vue的语法,我在vue3下面初步试了,还没成功url  https://mp.weixin.qq.com/s/5_EQjUGMrom7Da-T-gzlKQ  element-ui组件table去除下方滚动条,实现鼠标左右拖拽移动表格原创 wsh华仔 懒人wang 2024年07月11日17:44 山东时隔多日,再次遇到值得记录的问题。需求......
  • WPF 透明窗口可以调整尺寸(通过拖拽窗口边缘)
    通过这篇文章,我们可以实现:让WPF的透明窗口可以通过拖拽窗口的边缘,自由的调整尺寸。我们可以通过拖拽窗口的上方,来移动窗口。我们可以禁止窗口拖到屏幕边缘时自动最大化。还可以等比例的改变窗口的大小。还可以等比例的改变窗口中控件的大小参考文章AllowsTransp......
  • 别小瞧它,提高效率可了解可拖拽的工作流引擎
    当前,社会发展程度越来越高,很多企业都希望寻求更优的平台产品实现提质增效的目的。低代码技术平台、可拖拽的工作流引擎具有可视化操作界面、更灵活、好操作等多个优势特点,在提升办公效率方面具有事半功倍的效果。提升效率,可以随时来了解低代码技术平台、可拖拽的工作流引擎更多特......
  • VTK-自定义交互器、可拖拽坐标轴、视图定向立方体
    源代码:https://github.com/qianqiu10000/mySWInteractorStyle1.0.git仿照SolidWorks的操作习惯自定义的VTK交互器:1.左键单击Actor,可以选择Actor,并显示红色2.左键双击Actor,可以在Actor位置弹出拖拽坐标轴,可以移动、旋转3.单击空格键,可以弹出立方体视图定向工具4.按住鼠标......
  • Vue3+Element Plus 使用sortablejs对el-table表格进行拖拽
    sortablejs官网:点击跳转一、安装sortablejsnpminstallsortablejs--save二、 页面按需引入importSortablefrom'sortablejs';三、组件方法1.temlate:<template><el-tableref="tableHeader":data="tableData"row-key="id"style=&quo......
  • Sortable.js板块拖拽示例
    图例代码在图片后面点赞❤️+关注......
  • 实现一个 AI 聊天输入框:从基础到高级的指南
    引言随着人工智能和自然语言处理(NLP)技术的快速发展,基于AI的聊天系统变得越来越普遍。无论是简单的问答系统还是复杂的客服聊天机器人,聊天输入框都是用户与系统交互的关键组件。本文将详细介绍如何实现一个功能丰富的AI聊天输入框,从基础组件到高级功能,包括用户输入处理、界......
  • 提质增效,还看拖拽式报表设计器
    随着业务量的增大,传统的报表已经无法满足发展需要了,借助于低代码技术平台、拖拽式报表设计器的优势特点,可以助力摆脱信息孤岛、部门之间协作沟通不畅的弊端,实现高效增值的市场价值。如果想实现提质、降本、增效等发展目标,可以随时来了解拖拽式报表设计器的相关优势特点。其实,低代......
  • react hooks实现对元素拖拽及鼠标滚轮缩放
    page.jsximport'./index.less';import{useDrag,useZoom}from'./hooks';constDragZoom=()=>{const{handleMouseDown,handleMouseMove,handleMouseUp}=useDrag();const{handleWheel,scale}=useZoom();re......