ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Semi Design DragMove 组件完全指南:拖拽移动、范围约束与自定义位置处理

Semi Design DragMove 组件完全指南:拖拽移动、范围约束与自定义位置处理 Semi Design DragMove 组件完全指南拖拽移动、范围约束与自定义位置处理【免费下载链接】semi-designA modern, comprehensive, flexible design system and React UI library, AI-friendly built-in.Provide 3000 Design Tokens, easy to build your design system. Make Semi Design to Any Design.‍ Design to Code in one click项目地址: https://gitcode.com/gh_mirrors/se/semi-design本文以 Semi Designdouyinfe/semi-uiPlus 分类下的DragMove组件为核心系统讲解如何让任意元素通过拖拽改变位置涵盖基础用法、拖拽范围约束constrainer、自定义触发元素handler、自定义位置处理customMove以及完整的 API 参数说明。文中所有演示代码均取自仓库文档并经过源码级解析读者学完后可直接将 DragMove 应用于可拖拽弹窗、自由布局画布、面板移动等真实场景。DragMove是 Semi Design 从 v2.71.0 开始提供、位于 Plus 分类下的拖拽移动组件文档见 content/plus/dragMove/index-en-US.md它的作用是设置元素可被拖动改变位置并支持限制拖拽范围、自定义触发拖动的元素两大核心能力。组件源码位于 packages/semi-ui/dragMove/index.ts底层算法实现在 packages/semi-foundation/dragMove/foundation.ts并在 packages/semi-ui/index.ts 中以DragMove名义从douyinfe/semi-ui包导出。一、使用场景与引入方式什么时候使用 DragMoveDragMove 适用于以下典型场景可拖拽弹窗Modal 默认不可拖动配合DragMove可让弹窗任意移动Semi Design 官方文档中可拖拽 Modal即通过 DragMove 实现见 content/show/modal/index.md自由布局画布让卡片、控件在画布内自由摆放并通过constrainer限制其不超出容器面板/工具条移动自定义拖拽手柄让用户仅通过指定区域如图标、标题栏拖动整个面板。如何引入DragMove 从 v2.71.0 开始支持直接从douyinfe/semi-ui引入即可import { DragMove } from douyinfe/semi-ui;二、基础用法最简单的可拖拽元素被DragMove包裹的元素将能够通过拖拽鼠标或触摸改变位置。最简单的示例如下import React, { useRef, useEffect } from react; import { DragMove } from douyinfe/semi-ui; function Demo() { return ( DragMove div style{{ backgroundColor: var(--semi-color-primary), width: 80, height: 80, display: flex, alignItems: center, justifyContent: center, borderRadius: 10, fontWeight: 500, position: absolute, color: rgba(var(--semi-white), 1) }} Drag me/div /DragMove ); }这里被包裹的是一个 80×80 的圆角色块。从源码看DragMove的渲染逻辑非常轻量它通过React.cloneElement将组件自身的ref注入唯一的 childrenpackages/semi-ui/dragMove/index.ts并在componentDidMount时调用foundation.init()完成事件绑定packages/semi-ui/dragMove/index.ts。必须注意的两个前提官方文档对使用 DragMove 提出了两条明确提醒定位策略DragMove 默认会将可拖拽元素设置为absolute定位需要保留元素原有布局位置例如居中的 Modal时可设置positionStrategyrelative。该逻辑在 foundation 的updatePositionStrategy中执行packages/semi-foundation/dragMove/foundation.tspositionStrategy relative时设为relative否则一律为absolute。children 的类型约束DragMove 需要把 DOM 事件监听器应用到 children 上因此如果子元素是自定义组件必须确保它能将属性props透传到真实 DOM 节点。官方支持以下三类 childrenClass Component不强制绑定 ref但需要确保 props 可被透传至真实的 DOM 节点上使用forwardRef包裹后的函数式组件将 props 与 ref 透传到 children 内真实的 DOM 节点上真实 DOM 节点如span、div、p等。这一约束与源码中getDragElement的取值方式相呼应组件从this.elementRef.current取元素若isHTMLElement判断不通过则调用resolveDOM解析packages/semi-ui/dragMove/index.tschildren 无法透传 ref 时组件将拿不到真实 DOM 节点。三、限制拖拽范围constrainer传入constrainer该函数返回限制可拖拽范围的元素。注意constrainer 返回的元素需要为relative定位且其位置需覆盖被拖拽元素的初始所在区域。import React, { useRef, useEffect } from react; import { DragMove } from douyinfe/semi-ui; function Demo() { const containerRef React.useRef(); return ( div style{{ backgroundColor: rgba(var(--semi-grey-2), 1), width: 300, height: 300, padding: 5, position: relative, color: rgba(var(--semi-white), 1), fontWeight: 500, }} ref{containerRef} spanConstrainer/span DragMove constrainer{() containerRef.current} div style{{ backgroundColor: var(--semi-color-primary), width: 80, height: 80, borderRadius: 10, display: flex, alignItems: center, justifyContent: center, position: absolute, top: 80, left: 80, }} Drag me/div /DragMove /div ) }示例中 300×300 的灰色容器被ref捕获并通过constrainer返回蓝色色块只能在容器内移动、不会拖出边界。constrainer 的两种取值形式从DragMoveProps类型与 adapter 实现看packages/semi-ui/dragMove/index.tsconstrainer支持两种写法形式说明() HTMLElement返回一个 HTMLElement 作为边界容器parent字符串常量直接使用可拖拽元素的parentNode父节点作为边界当传入字符串parent时adapter 返回this.elementRef.current?.parentNode否则若传入函数则调用之两者都不满足时返回null表示不限制范围。底层范围算法解析范围计算在 foundation 的_calcMoveRange中完成packages/semi-foundation/dragMove/foundation.tsabsolute 定位从element.offsetParent开始向上遍历累加各层offsetLeft/offsetTop得到起点偏移然后按容器尺寸 − 元素尺寸推算出xMin/xMax/yMin/yMaxrelative 定位通过getBoundingClientRect()分别取元素与容器的矩形再结合当前style.left/top计算出相对偏移的上下界。拖拽过程中_changePos会把新坐标通过clampValueInRangeMath.min(Math.max(value, min), max)见 packages/semi-foundation/dragMove/foundation.ts钳制在范围内确保元素永不越界。四、自定义触发拖动的元素handler可通过handler自定义触发拖动的元素。如果不设置则点击元素任意位置均可拖动如果设置则仅点击 handler 返回的部分可拖动。import React, { useRef, useEffect } from react; import { IconTransparentStroked } from douyinfe/semi-icons; import { DragMove } from douyinfe/semi-ui; function Demo() { const handlerRef React.useRef(); const containerRef React.useRef(); return ( div style{{ backgroundColor: rgba(var(--semi-grey-2), 1), width: 300, height: 300, padding: 5, position: relative, color: rgba(var(--semi-white), 1), fontWeight: 500, }} ref{containerRef} spanConstrainer/span DragMove handler{() handlerRef.current} constrainer{() containerRef.current} div style{{ backgroundColor: var(--semi-color-primary), width: 80, height: 80, borderRadius: 10, position: absolute, top: 50, left: 50, display: flex, alignItems: center, justifyContent: center, }} div style{{ width: fit-content, height: fit-content }} ref{handlerRef} IconTransparentStroked size{large}//div /div /DragMove /div ) }示例中蓝色色块内的拖拽手柄IconTransparentStroked图标来自douyinfe/semi-icons被handlerRef捕获并通过handler返回只有按住该图标时才能拖动整个色块。从源码看getHandler的逻辑是若传入了handler函数则返回其返回值否则回退到被拖拽元素本身packages/semi-ui/dragMove/index.ts。事件绑定阶段foundation 只对 handler 注册mousedown/touchstartpackages/semi-foundation/dragMove/foundation.ts并在init时将 handler 的cursor设为movepackages/semi-foundation/dragMove/foundation.ts提示用户该区域可拖拽。五、自定义拖动后的位置处理customMove可通过customMove自定义拖动后的位置处理。该参数设置后DragMove 组件内部将仅通过参数返回计算后的位置element, top, left不做任何样式设置由用户按需自行设置新位置。import React, { useRef, useEffect } from react; import { DragMove } from douyinfe/semi-ui; function CustomMove() { const containerRef React.useRef(); const elementRef React.useRef(); const startPoint React.useRef(); const customMove useCallback((element, top, left) { if (left 100 containerRef.current.offsetWidth) { element.style.right ${containerRef.current.offsetWidth - left - element.offsetWidth}px element.style.left auto; } else { element.style.left left px; } element.style.top top px; }, []) const onMouseDown useCallback((e) { startPoint.current { x: e.clientX, y: e.clientY, } }, []); const onMouseUp useCallback((e) { if (startPoint.current) { const { x, y } startPoint.current; if (Math.abs(e.clientX - x) 5 Math.abs(e.clientY - y) 5) { if (elementRef.current.style.width 50px) { elementRef.current.style.width 100px; } else { elementRef.current.style.width 50px; } } } startPoint.current null; }, []); return ( spanClick on the blue color block to change the width. The blue color block will not exceed the range limit before and after the change./span br /br / div style{{ backgroundColor: rgba(var(--semi-grey-2), 1), width: 300, height: 300, position: relative, padding: 10, color: rgba(var(--semi-white), 1), fontWeight: 500, }} ref{containerRef} spanConstrainer/span DragMove constrainer{() containerRef.current} customMove{customMove} div style{{ backgroundColor: var(--semi-color-primary), width: 50, height: 50, display: flex, alignItems: center, justifyContent: center, position: absolute, top: 50, left: 50, borderRadius: 10, padding: 5 }} onMouseDown{onMouseDown} onMouseUp{onMouseUp} ref{elementRef} Drag me/div /DragMove /div / ) }该示例演示了一个进阶能力点击蓝色色块可将其宽度在 50px 与 100px 之间切换且无论宽度如何变化色块都不会超出容器边界。这里的关键在于customMove接管了位置写入当left 100超过容器宽度时改用right定位、将left置为auto实现贴着右边界的效果用户通过onMouseDown/onMouseUp自行记录点击起止坐标实现点击切换宽度位移小于 5px 判定为点击而非拖动constrainer仍然生效底层_changePos在调用customMove前已经用clampValueInRange将top/left钳制在合法范围内packages/semi-foundation/dragMove/foundation.ts。源码中_changePos的执行顺序值得注意packages/semi-foundation/dragMove/foundation.ts无论是默认行为还是自定义行为位置写入都被包裹在requestAnimationFrame中——传入customMove时回调customMove(element, newTop, newLeft)否则直接设置element.style.top/left。这也解释了测试文件 packages/semi-ui/dragMove/test/dragMove.test.js 中为何要先将global.requestAnimationFrame替换为同步回调callback callback()以确保测试断言同步生效。六、API 完整参考DragMove 属性属性说明类型默认值allowInputDrag点击原生 input/textarea 时是否允许拖动booleanfalseallowMove点击/触摸时是否允许拖动的判断函数(event: TouchEvent | MouseEvent, element: HTMLElement) boolean-constrainer返回限制可拖拽的范围的元素() HTMLElement | parent-customMove自定义拖动后的位置处理(element: HTMLElement, top: number, left: number) void-handler返回触发拖动的元素() HTMLElement-positionStrategy拖拽元素的定位策略relative 可保留元素原有布局位置absolute | relativeabsoluteonMouseDown鼠标按下时的回调(e: MouseEvent) void-onMouseMove鼠标移动时的回调(e: MouseEvent) void-onMouseUp鼠标抬起时的回调(e: MouseEvent) void-onTouchCancel触摸取消时的回调(e: TouchEvent) void-onTouchEnd触摸结束时的回调(e: TouchEvent) void-onTouchMove触摸移动时的回调(e: TouchEvent) void-onTouchStart触摸开始时的回调(e: TouchEvent) void-关键属性的源码行为说明结合 packages/semi-ui/dragMove/index.ts 与 packages/semi-foundation/dragMove/foundation.ts 的实现以下几个属性值得深入理解allowInputDrag默认 false_allowMove中默认拦截input与textarea标签的点击保证在输入框中点击不会触发拖动仅允许正常输入显式传入true后可放行packages/semi-foundation/dragMove/foundation.tsallowMove在_allowMove中作为拦截判断函数执行接收原生事件与元素本身返回true才继续注册拖动监听。该函数在 allowInputDrag 的标签检查之后执行positionStrategy默认absolute。在 foundation 初始化、以及组件的componentDidUpdate检测到该值变化时packages/semi-ui/dragMove/index.ts都会调用updatePositionStrategy同步元素的position样式自定义回调的透传onMouseDown/onMouseMove/onMouseUp与onTouchStart/onTouchMove/onTouchEnd/onTouchCancel通过 adapter 的notify*系列方法转发给 props与 foundation 内部逻辑解耦便于用户在拖拽的不同阶段接入自己的业务如记录起始点、上报埋点。七、事件机制与生命周期从按下到松手的完整链路DragMove 的事件体系是理解其行为的关键全部集中在 foundation 层initcomponentDidMount获取被拖拽元素不存在则抛出Error(drag element must be a valid element)应用定位策略将 handler 的cursor设为move随后注册mousedown/touchstartpackages/semi-foundation/dragMove/foundation.ts按下mousedown/touchstart先计算移动范围_calcMoveRange()通知onMouseDown/onTouchStart回调再经_allowMove校验通过后向document注册mousemove/mouseup或touchmove/touchend/touchcancel并记录起始偏移_calcOffset最后preventDefault阻止拖拽过程中选中文本/图片等默认行为packages/semi-foundation/dragMove/foundation.ts移动document 级监听_changePos计算新坐标并钳制范围包裹在requestAnimationFrame中写入样式或交由customMove处理期间触发onMouseMove/onTouchMovepackages/semi-foundation/dragMove/foundation.ts松手mouseup/touchend/touchcancel通知对应回调并注销 document 级监听packages/semi-foundation/dragMove/foundation.ts。将移动/松手监听挂在document上而非元素自身保证了即使鼠标快速移出元素边界拖动也不会中断——这是拖拽组件应有的健壮性设计。组件卸载时foundation.destroy()会同时注销开始事件与 document 级事件避免内存泄漏packages/semi-foundation/dragMove/foundation.ts。八、实战组合可拖拽的居中 ModalDragMove 在 Semi Design 中最具代表性的实战场景是与 Modal 组合实现可拖拽弹窗。官方 Modal 文档content/show/modal/index.md给出的做法是通过modalRender自定义渲染 Modal 内容并用positionStrategyrelative包裹——因为居中的 Modal 需要保留其在布局流中的位置再通过相对偏移实现拖动import React, { useState } from react; import { ConfigProvider, Button, Modal, DragMove } from douyinfe/semi-ui; function Demo(props {}) { const [visible, setVisible] useState(false); return ( div Button onClick{() setVisible(true)}Open Modal/Button Modal title可拖拽Modal visible{visible} centered onCancel{() setVisible(false)} modalRender{(modal) ( DragMove positionStrategyrelative{modal}/DragMove )} pThis is the content of a basic sidesheet./p pHere is more content.../p /Modal /div ); }这一用法直接印证了文档中的核心提醒使用positionStrategyrelative可保留元素原有布局位置例如居中的 Modal。与之呼应的是 changelogcontent/ecosystem/changelog/index.md中的一条记录DragMove 新增positionStrategyrelative定位策略在保留元素原有布局位置的同时通过相对偏移实现拖动修复了centered可拖拽 Modal 向下偏移的问题默认absolute策略保持不变。九、注意事项与版本沿革小结综合文档、源码与 changelog使用 DragMove 时建议关注以下几点children 必须是可透传 props/ref 的元素否则事件无法绑定文档明确支持 Class Component、forwardRef 函数组件与真实 DOM 节点三类constrainer 返回的元素必须为 relative 定位且范围计算依赖元素的offsetParent链或getBoundingClientRect因此嵌套在多层定位容器中时需自行验证边界行为默认 absolute 定位会改变元素在文档流中的位置需要保留布局位置如居中 Modal时务必设置positionStrategyrelativecustomMove接管位置写入后组件不再直接设置样式此时仍会执行范围钳制但怎样落位完全由你的回调决定输入框场景默认不可拖动allowInputDrag默认 false避免影响正常的文本输入交互。从版本沿革看content/ecosystem/changelog/index.mdDragMove 自 v2.71.0 引入后持续演进曾修复设置 handler 后子元素仍可拖动的问题、修正handler/constrainer的 TS 类型声明统一为() HTMLElement/() HTMLElement | parent、修复类型定义错误并新增positionStrategyrelative策略。这些演进说明 DragMove 的 API 已趋于稳定文档中给出的类型与行为描述均可放心依据。若需要深入阅读实现可继续查看组件入口 packages/semi-ui/dragMove/index.ts、算法核心 packages/semi-foundation/dragMove/foundation.ts 以及单元测试 packages/semi-ui/dragMove/test/dragMove.test.js。【免费下载链接】semi-designA modern, comprehensive, flexible design system and React UI library, AI-friendly built-in.Provide 3000 Design Tokens, easy to build your design system. Make Semi Design to Any Design.‍ Design to Code in one click项目地址: https://gitcode.com/gh_mirrors/se/semi-design创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表