系列:一張投影片背後的工程:從零打造網頁簡報編輯器
對應程式版本:Git tag
v0.1.0版本說明:本文延續 Day 08 的座標換算,改從一次完整拖曳的事件生命週期說明實作。
Day 08 已經解決公式:螢幕位移要除以顯示比例,才能回到固定的邏輯畫布。真正做成拖曳功能後,還有另一個問題:游標會離開物件、操作可能被取消,而且不能在每一次 pointermove 都保存文件。
這一篇把拖曳拆成「記住起點、即時預覽、放開提交」三個階段,讓同一套行為在 50%、100% 或 125% 下都保持一致。
拖曳開始時,程式只接受主要按鍵,先選取物件,再保存這次操作專屬的資料:
dragRef.current = {
pointerId: event.pointerId,
screenStart: { x: event.clientX, y: event.clientY },
elementStart: position,
}
elementStart 是文件中的原始邏輯座標,不會隨著每次畫面更新而改變。後續每一步都以它為基準,加上「目前游標減掉按下位置」的差值。這樣不會累積四捨五入誤差,也不會因為 React 畫面尚未更新而讀到舊位置。
同時呼叫 setPointerCapture(pointerId)。游標離開物件邊界後,後續指標事件仍會送回這個物件;不同手指或指標的事件則以 pointerId 排除,不會混進同一次拖曳。
pointermove 先用 Day 08 的函式把螢幕差值轉成邏輯差值,再加回起點、四捨五入並限制在畫布內:
const delta = screenDeltaToCanvas(
event.clientX - drag.screenStart.x,
event.clientY - drag.screenStart.y,
scale,
)
const nextPosition = clampElementPosition(
{
x: Math.round(drag.elementStart.x + delta.x),
y: Math.round(drag.elementStart.y + delta.y),
},
size,
canvasSize,
)
nextPosition 只放在元件的暫時 state,畫面立刻跟著游標移動,文件資料卻保持不變。到了 pointerup,才以一次 element/move 提交最後座標;接著釋放 Pointer Capture 並清除預覽。這樣自動儲存與復原系統只看見一個完整動作。
若收到 pointercancel,暫時位置會直接丟棄,文件維持按下前的內容。文字框正在輸入時也傳入 disabled: true,避免選字或中文組字意外開啟拖曳。
介面測試的畫布比例是 50%。游標從 (100, 100) 移到 (150, 130),螢幕差值是 (50, 30),換算後應為邏輯差值 (100, 60);預設文字框便從 (180, 170) 到 (280, 230)。
座標純函式另外驗證 125%:螢幕移動 100 像素時,邏輯座標只前進 80 單位。兩個比例都通過,才能確認公式沒有只在預設縮放下碰巧正確。Edge 手動驗收也在不同縮放比例拖曳物件,游標與物件位移一致。
拖曳規則穩定後,下一篇會共用相同的事件生命週期,處理尺寸調整時為什麼圖片不能和文字框使用完全相同的規則。