、范圍約束與自定義位置處理)
Semi Design DragMove 組件完全指南拖拽移動(dòng)、范圍約束與自定義位置處理【免費(fèi)下載鏈接】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項(xiàng)目地址: https://gitcode.com/gh_mirrors/se/semi-design本文以 Semi Designdouyinfe/semi-uiPlus 分類下的DragMove組件為核心系統(tǒng)講解如何讓任意元素通過拖拽改變位置涵蓋基礎(chǔ)用法、拖拽范圍約束constrainer、自定義觸發(fā)元素handler、自定義位置處理customMove以及完整的 API 參數(shù)說明。文中所有演示代碼均取自倉庫文檔并經(jīng)過源碼級(jí)解析讀者學(xué)完后可直接將 DragMove 應(yīng)用于可拖拽彈窗、自由布局畫布、面板移動(dòng)等真實(shí)場(chǎng)景。DragMove是 Semi Design 從 v2.71.0 開始提供、位于 Plus 分類下的拖拽移動(dòng)組件文檔見 content/plus/dragMove/index-en-US.md它的作用是設(shè)置元素可被拖動(dòng)改變位置并支持限制拖拽范圍、自定義觸發(fā)拖動(dòng)的元素兩大核心能力。組件源碼位于 packages/semi-ui/dragMove/index.ts底層算法實(shí)現(xiàn)在 packages/semi-foundation/dragMove/foundation.ts并在 packages/semi-ui/index.ts 中以DragMove名義從douyinfe/semi-ui包導(dǎo)出。一、使用場(chǎng)景與引入方式什么時(shí)候使用 DragMoveDragMove 適用于以下典型場(chǎng)景可拖拽彈窗Modal 默認(rèn)不可拖動(dòng)配合DragMove可讓彈窗任意移動(dòng)Semi Design 官方文檔中可拖拽 Modal即通過 DragMove 實(shí)現(xiàn)見 content/show/modal/index.md自由布局畫布讓卡片、控件在畫布內(nèi)自由擺放并通過constrainer限制其不超出容器面板/工具條移動(dòng)自定義拖拽手柄讓用戶僅通過指定區(qū)域如圖標(biāo)、標(biāo)題欄拖動(dòng)整個(gè)面板。如何引入DragMove 從 v2.71.0 開始支持直接從douyinfe/semi-ui引入即可import { DragMove } from douyinfe/semi-ui;二、基礎(chǔ)用法最簡(jiǎn)單的可拖拽元素被DragMove包裹的元素將能夠通過拖拽鼠標(biāo)或觸摸改變位置。最簡(jiǎn)單的示例如下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 ); }這里被包裹的是一個(gè) 80×80 的圓角色塊。從源碼看DragMove的渲染邏輯非常輕量它通過React.cloneElement將組件自身的ref注入唯一的 childrenpackages/semi-ui/dragMove/index.ts并在componentDidMount時(shí)調(diào)用foundation.init()完成事件綁定packages/semi-ui/dragMove/index.ts。必須注意的兩個(gè)前提官方文檔對(duì)使用 DragMove 提出了兩條明確提醒定位策略DragMove 默認(rèn)會(huì)將可拖拽元素設(shè)置為absolute定位需要保留元素原有布局位置例如居中的 Modal時(shí)可設(shè)置positionStrategyrelative。該邏輯在 foundation 的updatePositionStrategy中執(zhí)行packages/semi-foundation/dragMove/foundation.tspositionStrategy relative時(shí)設(shè)為relative否則一律為absolute。children 的類型約束DragMove 需要把 DOM 事件監(jiān)聽器應(yīng)用到 children 上因此如果子元素是自定義組件必須確保它能將屬性props透?jìng)鞯秸鎸?shí) DOM 節(jié)點(diǎn)。官方支持以下三類 childrenClass Component不強(qiáng)制綁定 ref但需要確保 props 可被透?jìng)髦琳鎸?shí)的 DOM 節(jié)點(diǎn)上使用forwardRef包裹后的函數(shù)式組件將 props 與 ref 透?jìng)鞯?children 內(nèi)真實(shí)的 DOM 節(jié)點(diǎn)上真實(shí) DOM 節(jié)點(diǎn)如span、div、p等。這一約束與源碼中g(shù)etDragElement的取值方式相呼應(yīng)組件從this.elementRef.current取元素若isHTMLElement判斷不通過則調(diào)用resolveDOM解析packages/semi-ui/dragMove/index.tschildren 無法透?jìng)?ref 時(shí)組件將拿不到真實(shí) DOM 節(jié)點(diǎn)。三、限制拖拽范圍constrainer傳入constrainer該函數(shù)返回限制可拖拽范圍的元素。注意constrainer 返回的元素需要為relative定位且其位置需覆蓋被拖拽元素的初始所在區(qū)域。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返回藍(lán)色色塊只能在容器內(nèi)移動(dòng)、不會(huì)拖出邊界。constrainer 的兩種取值形式從DragMoveProps類型與 adapter 實(shí)現(xiàn)看packages/semi-ui/dragMove/index.tsconstrainer支持兩種寫法形式說明() HTMLElement返回一個(gè) HTMLElement 作為邊界容器parent字符串常量直接使用可拖拽元素的parentNode父節(jié)點(diǎn)作為邊界當(dāng)傳入字符串parent時(shí)adapter 返回this.elementRef.current?.parentNode否則若傳入函數(shù)則調(diào)用之兩者都不滿足時(shí)返回null表示不限制范圍。底層范圍算法解析范圍計(jì)算在 foundation 的_calcMoveRange中完成packages/semi-foundation/dragMove/foundation.tsabsolute 定位從element.offsetParent開始向上遍歷累加各層offsetLeft/offsetTop得到起點(diǎn)偏移然后按容器尺寸 ? 元素尺寸推算出xMin/xMax/yMin/yMaxrelative 定位通過getBoundingClientRect()分別取元素與容器的矩形再結(jié)合當(dāng)前style.left/top計(jì)算出相對(duì)偏移的上下界。拖拽過程中_changePos會(huì)把新坐標(biāo)通過clampValueInRangeMath.min(Math.max(value, min), max)見 packages/semi-foundation/dragMove/foundation.ts鉗制在范圍內(nèi)確保元素永不越界。四、自定義觸發(fā)拖動(dòng)的元素handler可通過handler自定義觸發(fā)拖動(dòng)的元素。如果不設(shè)置則點(diǎn)擊元素任意位置均可拖動(dòng)如果設(shè)置則僅點(diǎn)擊 handler 返回的部分可拖動(dòng)。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 ) }示例中藍(lán)色色塊內(nèi)的拖拽手柄IconTransparentStroked圖標(biāo)來自douyinfe/semi-icons被handlerRef捕獲并通過handler返回只有按住該圖標(biāo)時(shí)才能拖動(dòng)整個(gè)色塊。從源碼看getHandler的邏輯是若傳入了handler函數(shù)則返回其返回值否則回退到被拖拽元素本身packages/semi-ui/dragMove/index.ts。事件綁定階段foundation 只對(duì) handler 注冊(cè)mousedown/touchstartpackages/semi-foundation/dragMove/foundation.ts并在init時(shí)將 handler 的cursor設(shè)為movepackages/semi-foundation/dragMove/foundation.ts提示用戶該區(qū)域可拖拽。五、自定義拖動(dòng)后的位置處理customMove可通過customMove自定義拖動(dòng)后的位置處理。該參數(shù)設(shè)置后DragMove 組件內(nèi)部將僅通過參數(shù)返回計(jì)算后的位置element, top, left不做任何樣式設(shè)置由用戶按需自行設(shè)置新位置。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 / ) }該示例演示了一個(gè)進(jìn)階能力點(diǎn)擊藍(lán)色色塊可將其寬度在 50px 與 100px 之間切換且無論寬度如何變化色塊都不會(huì)超出容器邊界。這里的關(guān)鍵在于customMove接管了位置寫入當(dāng)left 100超過容器寬度時(shí)改用right定位、將left置為auto實(shí)現(xiàn)貼著右邊界的效果用戶通過onMouseDown/onMouseUp自行記錄點(diǎn)擊起止坐標(biāo)實(shí)現(xiàn)點(diǎn)擊切換寬度位移小于 5px 判定為點(diǎn)擊而非拖動(dòng)constrainer仍然生效底層_changePos在調(diào)用customMove前已經(jīng)用clampValueInRange將top/left鉗制在合法范圍內(nèi)packages/semi-foundation/dragMove/foundation.ts。源碼中_changePos的執(zhí)行順序值得注意packages/semi-foundation/dragMove/foundation.ts無論是默認(rèn)行為還是自定義行為位置寫入都被包裹在requestAnimationFrame中——傳入customMove時(shí)回調(diào)customMove(element, newTop, newLeft)否則直接設(shè)置element.style.top/left。這也解釋了測(cè)試文件 packages/semi-ui/dragMove/test/dragMove.test.js 中為何要先將global.requestAnimationFrame替換為同步回調(diào)callback callback()以確保測(cè)試斷言同步生效。六、API 完整參考DragMove 屬性屬性說明類型默認(rèn)值allowInputDrag點(diǎn)擊原生 input/textarea 時(shí)是否允許拖動(dòng)booleanfalseallowMove點(diǎn)擊/觸摸時(shí)是否允許拖動(dòng)的判斷函數(shù)(event: TouchEvent | MouseEvent, element: HTMLElement) boolean-constrainer返回限制可拖拽的范圍的元素() HTMLElement | parent-customMove自定義拖動(dòng)后的位置處理(element: HTMLElement, top: number, left: number) void-handler返回觸發(fā)拖動(dòng)的元素() HTMLElement-positionStrategy拖拽元素的定位策略relative 可保留元素原有布局位置absolute | relativeabsoluteonMouseDown鼠標(biāo)按下時(shí)的回調(diào)(e: MouseEvent) void-onMouseMove鼠標(biāo)移動(dòng)時(shí)的回調(diào)(e: MouseEvent) void-onMouseUp鼠標(biāo)抬起時(shí)的回調(diào)(e: MouseEvent) void-onTouchCancel觸摸取消時(shí)的回調(diào)(e: TouchEvent) void-onTouchEnd觸摸結(jié)束時(shí)的回調(diào)(e: TouchEvent) void-onTouchMove觸摸移動(dòng)時(shí)的回調(diào)(e: TouchEvent) void-onTouchStart觸摸開始時(shí)的回調(diào)(e: TouchEvent) void-關(guān)鍵屬性的源碼行為說明結(jié)合 packages/semi-ui/dragMove/index.ts 與 packages/semi-foundation/dragMove/foundation.ts 的實(shí)現(xiàn)以下幾個(gè)屬性值得深入理解allowInputDrag默認(rèn) false_allowMove中默認(rèn)攔截input與textarea標(biāo)簽的點(diǎn)擊保證在輸入框中點(diǎn)擊不會(huì)觸發(fā)拖動(dòng)僅允許正常輸入顯式傳入true后可放行packages/semi-foundation/dragMove/foundation.tsallowMove在_allowMove中作為攔截判斷函數(shù)執(zhí)行接收原生事件與元素本身返回true才繼續(xù)注冊(cè)拖動(dòng)監(jiān)聽。該函數(shù)在 allowInputDrag 的標(biāo)簽檢查之后執(zhí)行positionStrategy默認(rèn)absolute。在 foundation 初始化、以及組件的componentDidUpdate檢測(cè)到該值變化時(shí)packages/semi-ui/dragMove/index.ts都會(huì)調(diào)用updatePositionStrategy同步元素的position樣式自定義回調(diào)的透?jìng)鱫nMouseDown/onMouseMove/onMouseUp與onTouchStart/onTouchMove/onTouchEnd/onTouchCancel通過 adapter 的notify*系列方法轉(zhuǎn)發(fā)給 props與 foundation 內(nèi)部邏輯解耦便于用戶在拖拽的不同階段接入自己的業(yè)務(wù)如記錄起始點(diǎn)、上報(bào)埋點(diǎn)。七、事件機(jī)制與生命周期從按下到松手的完整鏈路DragMove 的事件體系是理解其行為的關(guān)鍵全部集中在 foundation 層initcomponentDidMount獲取被拖拽元素不存在則拋出Error(drag element must be a valid element)應(yīng)用定位策略將 handler 的cursor設(shè)為move隨后注冊(cè)mousedown/touchstartpackages/semi-foundation/dragMove/foundation.ts按下mousedown/touchstart先計(jì)算移動(dòng)范圍_calcMoveRange()通知onMouseDown/onTouchStart回調(diào)再經(jīng)_allowMove校驗(yàn)通過后向document注冊(cè)mousemove/mouseup或touchmove/touchend/touchcancel并記錄起始偏移_calcOffset最后preventDefault阻止拖拽過程中選中文本/圖片等默認(rèn)行為packages/semi-foundation/dragMove/foundation.ts移動(dòng)document 級(jí)監(jiān)聽_changePos計(jì)算新坐標(biāo)并鉗制范圍包裹在requestAnimationFrame中寫入樣式或交由customMove處理期間觸發(fā)onMouseMove/onTouchMovepackages/semi-foundation/dragMove/foundation.ts松手mouseup/touchend/touchcancel通知對(duì)應(yīng)回調(diào)并注銷 document 級(jí)監(jiān)聽packages/semi-foundation/dragMove/foundation.ts。將移動(dòng)/松手監(jiān)聽掛在document上而非元素自身保證了即使鼠標(biāo)快速移出元素邊界拖動(dòng)也不會(huì)中斷——這是拖拽組件應(yīng)有的健壯性設(shè)計(jì)。組件卸載時(shí)foundation.destroy()會(huì)同時(shí)注銷開始事件與 document 級(jí)事件避免內(nèi)存泄漏packages/semi-foundation/dragMove/foundation.ts。八、實(shí)戰(zhàn)組合可拖拽的居中 ModalDragMove 在 Semi Design 中最具代表性的實(shí)戰(zhàn)場(chǎng)景是與 Modal 組合實(shí)現(xiàn)可拖拽彈窗。官方 Modal 文檔content/show/modal/index.md給出的做法是通過modalRender自定義渲染 Modal 內(nèi)容并用positionStrategyrelative包裹——因?yàn)榫又械?Modal 需要保留其在布局流中的位置再通過相對(duì)偏移實(shí)現(xiàn)拖動(dòng)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。與之呼應(yīng)的是 changelogcontent/ecosystem/changelog/index.md中的一條記錄DragMove 新增positionStrategyrelative定位策略在保留元素原有布局位置的同時(shí)通過相對(duì)偏移實(shí)現(xiàn)拖動(dòng)修復(fù)了centered可拖拽 Modal 向下偏移的問題默認(rèn)absolute策略保持不變。九、注意事項(xiàng)與版本沿革小結(jié)綜合文檔、源碼與 changelog使用 DragMove 時(shí)建議關(guān)注以下幾點(diǎn)children 必須是可透?jìng)?props/ref 的元素否則事件無法綁定文檔明確支持 Class Component、forwardRef 函數(shù)組件與真實(shí) DOM 節(jié)點(diǎn)三類constrainer 返回的元素必須為 relative 定位且范圍計(jì)算依賴元素的offsetParent鏈或getBoundingClientRect因此嵌套在多層定位容器中時(shí)需自行驗(yàn)證邊界行為默認(rèn) absolute 定位會(huì)改變?cè)卦谖臋n流中的位置需要保留布局位置如居中 Modal時(shí)務(wù)必設(shè)置positionStrategyrelativecustomMove接管位置寫入后組件不再直接設(shè)置樣式此時(shí)仍會(huì)執(zhí)行范圍鉗制但怎樣落位完全由你的回調(diào)決定輸入框場(chǎng)景默認(rèn)不可拖動(dòng)allowInputDrag默認(rèn) false避免影響正常的文本輸入交互。從版本沿革看content/ecosystem/changelog/index.mdDragMove 自 v2.71.0 引入后持續(xù)演進(jìn)曾修復(fù)設(shè)置 handler 后子元素仍可拖動(dòng)的問題、修正handler/constrainer的 TS 類型聲明統(tǒng)一為() HTMLElement/() HTMLElement | parent、修復(fù)類型定義錯(cuò)誤并新增positionStrategyrelative策略。這些演進(jìn)說明 DragMove 的 API 已趨于穩(wěn)定文檔中給出的類型與行為描述均可放心依據(jù)。若需要深入閱讀實(shí)現(xiàn)可繼續(xù)查看組件入口 packages/semi-ui/dragMove/index.ts、算法核心 packages/semi-foundation/dragMove/foundation.ts 以及單元測(cè)試 packages/semi-ui/dragMove/test/dragMove.test.js?!久赓M(fèi)下載鏈接】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項(xiàng)目地址: https://gitcode.com/gh_mirrors/se/semi-design創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考