@aiquants/drag-drop-panels 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +32 -10
- package/dist/drag-drop/hooks/useCustomDragHandlers.d.ts.map +1 -1
- package/dist/index.es.js +88 -88
- package/dist/index.umd.js +2 -2
- package/dist/styles/drag-drop-panels.standalone.css +3 -0
- package/package.json +11 -4
- package/src/DragDropLayout.tsx +751 -0
- package/src/drag-drop/ColumnControlPanel.tsx +123 -0
- package/src/drag-drop/ColumnInsertPanel.tsx +106 -0
- package/src/drag-drop/DebugComponents.tsx +222 -0
- package/src/drag-drop/DebugControlPanel.tsx +200 -0
- package/src/drag-drop/DebugOverlay.tsx +250 -0
- package/src/drag-drop/DragDropLayout.css +130 -0
- package/src/drag-drop/ResizeHandle.tsx +208 -0
- package/src/drag-drop/ResponsiveControl.tsx +33 -0
- package/src/drag-drop/hooks/dragGhostScale.spec.tsx +153 -0
- package/src/drag-drop/hooks/useColumnLayout.ts +457 -0
- package/src/drag-drop/hooks/useCustomDragHandlers.tsx +333 -0
- package/src/drag-drop/hooks/useDragDropColumns.tsx +446 -0
- package/src/drag-drop/hooks/useDragDropState.tsx +200 -0
- package/src/drag-drop/hooks/useFitScale.ts +48 -0
- package/src/drag-drop/hooks/useMouseTracking.tsx +102 -0
- package/src/drag-drop/hooks/useNormalDragHandlers.tsx +427 -0
- package/src/drag-drop/hooks/usePanelManagement.tsx +238 -0
- package/src/drag-drop/hooks/useResponsiveColumns.ts +210 -0
- package/src/drag-drop/resize-handle.css +71 -0
- package/src/drag-drop/types.ts +248 -0
- package/src/drag-drop/utils/columnIdUtils.ts +104 -0
- package/src/drag-drop/utils/edgeScrollUtils.ts +124 -0
- package/src/drag-drop/utils/simple-logger.ts +162 -0
- package/src/index.ts +60 -0
- package/src/styles/standalone.entry.css +27 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/** これ以上縮小しても実用にならない下限 (25%) */
|
|
4
|
+
const DEFAULT_MIN_FIT_SCALE = 0.25
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 初回計測を paint 前に行い「等倍で一瞬描画→縮小へスナップ」のフラッシュを防ぐため layout effect を使う。
|
|
8
|
+
* ただし SSR では useLayoutEffect が警告を出す (サーバーでは何もしない) ため、クライアントのみ layout、
|
|
9
|
+
* サーバーでは通常 effect にフォールバックする。
|
|
10
|
+
*/
|
|
11
|
+
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* コンテンツ領域の幅を ResizeObserver で監視し、requiredWidth に満たない場合に
|
|
15
|
+
* ワークスペース全体を視覚縮小するための scale (minScale 〜 1) を返すフック。
|
|
16
|
+
*
|
|
17
|
+
* @param {number} requiredWidth - 等倍表示に必要な最小幅
|
|
18
|
+
* @param {number} [minScale] - 縮小の下限値
|
|
19
|
+
*/
|
|
20
|
+
export const useFitScale = (requiredWidth: number, minScale: number = DEFAULT_MIN_FIT_SCALE) => {
|
|
21
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
22
|
+
const [scale, setScale] = useState(1)
|
|
23
|
+
|
|
24
|
+
useIsomorphicLayoutEffect(() => {
|
|
25
|
+
const element = containerRef.current
|
|
26
|
+
if (!element || requiredWidth <= 0) {
|
|
27
|
+
setScale(1)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const update = () => {
|
|
32
|
+
const width = element.clientWidth
|
|
33
|
+
if (width <= 0) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
const next = width >= requiredWidth ? 1 : Math.max(width / requiredWidth, minScale)
|
|
37
|
+
// サブピクセルの揺れで再レンダーしないよう 0.5% 未満の変化は無視する
|
|
38
|
+
setScale((previous) => (Math.abs(previous - next) < 0.005 ? previous : next))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
update()
|
|
42
|
+
const observer = new ResizeObserver(update)
|
|
43
|
+
observer.observe(element)
|
|
44
|
+
return () => observer.disconnect()
|
|
45
|
+
}, [requiredWidth, minScale])
|
|
46
|
+
|
|
47
|
+
return { containerRef, scale }
|
|
48
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview This file defines the `useMouseTracking` custom hook, which provides
|
|
3
|
+
* utilities for tracking mouse position, click events, and gestures like virtual clicks.
|
|
4
|
+
* It's essential for implementing custom drag-and-drop behavior.
|
|
5
|
+
*
|
|
6
|
+
* このファイルは `useMouseTracking` カスタムフックを定義します。これは、マウスの位置、
|
|
7
|
+
* クリックイベント、および仮想クリックのようなジェスチャーを追跡するためのユーティリティを提供します。
|
|
8
|
+
* カスタムのドラッグ&ドロップ動作を実装するために不可欠です。
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useRef, useState } from "react"
|
|
11
|
+
import type { MousePosition } from "../types"
|
|
12
|
+
|
|
13
|
+
export const useMouseTracking = () => {
|
|
14
|
+
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
|
15
|
+
x: 0,
|
|
16
|
+
y: 0,
|
|
17
|
+
isVisible: false,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const lastClickPositionRef = useRef<{ x: number; y: number } | null>(null)
|
|
21
|
+
const mouseDownTimeRef = useRef<number>(0)
|
|
22
|
+
const mouseDownPositionRef = useRef<{ x: number; y: number } | null>(null)
|
|
23
|
+
|
|
24
|
+
const showMousePosition = useCallback((x: number, y: number) => {
|
|
25
|
+
setMousePosition({ x, y, isVisible: true })
|
|
26
|
+
}, [])
|
|
27
|
+
|
|
28
|
+
const hideMousePosition = useCallback(() => {
|
|
29
|
+
setMousePosition((prev) => ({ ...prev, isVisible: false }))
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
const updateMousePosition = useCallback((x: number, y: number) => {
|
|
33
|
+
setMousePosition((prevPosition) => {
|
|
34
|
+
if (prevPosition.x === x && prevPosition.y === y) {
|
|
35
|
+
return prevPosition
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
x,
|
|
40
|
+
y,
|
|
41
|
+
isVisible: prevPosition.isVisible,
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
const recordMouseDown = useCallback((x: number, y: number) => {
|
|
47
|
+
mouseDownTimeRef.current = Date.now()
|
|
48
|
+
mouseDownPositionRef.current = { x, y }
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
const recordClickPosition = useCallback((x: number, y: number) => {
|
|
52
|
+
lastClickPositionRef.current = { x, y }
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
const isVirtualClick = useCallback((x: number, y: number, maxTime = 500, maxDistance = 5): boolean => {
|
|
56
|
+
const currentTime = Date.now()
|
|
57
|
+
const timeSinceMouseDown = currentTime - (mouseDownTimeRef.current || 0)
|
|
58
|
+
|
|
59
|
+
if (timeSinceMouseDown >= maxTime) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!mouseDownPositionRef.current) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const distance = Math.sqrt((x - mouseDownPositionRef.current.x) ** 2 + (y - mouseDownPositionRef.current.y) ** 2)
|
|
68
|
+
|
|
69
|
+
return distance < maxDistance
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
const calculateMoveDistance = useCallback((x: number, y: number): number => {
|
|
73
|
+
if (!lastClickPositionRef.current) {
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dx = Math.abs(x - lastClickPositionRef.current.x)
|
|
78
|
+
const dy = Math.abs(y - lastClickPositionRef.current.y)
|
|
79
|
+
return Math.sqrt(dx * dx + dy * dy)
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
82
|
+
const clearMouseTracking = useCallback(() => {
|
|
83
|
+
lastClickPositionRef.current = null
|
|
84
|
+
mouseDownTimeRef.current = 0
|
|
85
|
+
mouseDownPositionRef.current = null
|
|
86
|
+
}, [])
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
mousePosition,
|
|
90
|
+
lastClickPosition: lastClickPositionRef.current,
|
|
91
|
+
mouseDownTime: mouseDownTimeRef.current,
|
|
92
|
+
mouseDownPosition: mouseDownPositionRef.current,
|
|
93
|
+
showMousePosition,
|
|
94
|
+
hideMousePosition,
|
|
95
|
+
updateMousePosition,
|
|
96
|
+
recordMouseDown,
|
|
97
|
+
recordClickPosition,
|
|
98
|
+
isVirtualClick,
|
|
99
|
+
calculateMoveDistance,
|
|
100
|
+
clearMouseTracking,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `useNormalDragHandlers` カスタムフックの定義。
|
|
3
|
+
*
|
|
4
|
+
* 標準的なHTML5のドラッグ&ドロップ操作に関連する全てのイベントハンドリングロジックをカプセル化。
|
|
5
|
+
* ドラッグの開始、終了、オーバー、ドロップといった一連のイベントを管理し、
|
|
6
|
+
* `useCustomDragHandlers` と連携して一貫性のあるD&D体験の提供を責務とする。
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Defines the `useNormalDragHandlers` custom hook。
|
|
9
|
+
*
|
|
10
|
+
* Encapsulates all event handling logic for standard HTML5 drag-and-drop operations。
|
|
11
|
+
* This hook is responsible for managing the sequence of events like drag start, end, over,
|
|
12
|
+
* and drop, working in concert with `useCustomDragHandlers` to provide a consistent D&D experience.
|
|
13
|
+
*/
|
|
14
|
+
import { useCallback, useEffect, useRef } from "react"
|
|
15
|
+
import { debugLog } from "../DebugComponents"
|
|
16
|
+
import type { CustomDragState, DebugConfig, DragOverPosition, DragState, OriginalPanelPosition } from "../types"
|
|
17
|
+
import { DEFAULT_EDGE_SCROLL_CONFIG, EdgeScrollManager } from "../utils/edgeScrollUtils"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `useNormalDragHandlers` フックが受け取るプロパティの型定義。
|
|
21
|
+
* Defines the props structure for the `useNormalDragHandlers` hook.
|
|
22
|
+
*/
|
|
23
|
+
interface UseNormalDragHandlersProps {
|
|
24
|
+
debugConfig: DebugConfig
|
|
25
|
+
dragState: DragState
|
|
26
|
+
customDrag: CustomDragState
|
|
27
|
+
dragModes: Record<string, "normal" | "custom">
|
|
28
|
+
columnPanels: Record<string, string[]>
|
|
29
|
+
dragOverPosition: DragOverPosition | null
|
|
30
|
+
originalPanelPosition: OriginalPanelPosition | null
|
|
31
|
+
|
|
32
|
+
// State update functions / 状態更新関数
|
|
33
|
+
setDragState: (state: DragState | ((prev: DragState) => DragState)) => void
|
|
34
|
+
setDragOverPosition: (position: DragOverPosition | null) => void
|
|
35
|
+
setOriginalPanelPosition: (position: OriginalPanelPosition | null) => void
|
|
36
|
+
|
|
37
|
+
// Other functions / その他の関数
|
|
38
|
+
performPanelMove: (panelId: string, targetColumn: string, insertIndex: number) => void
|
|
39
|
+
showMousePosition: (x: number, y: number) => void
|
|
40
|
+
updateMousePosition: (x: number, y: number) => void
|
|
41
|
+
resetDragState: () => void
|
|
42
|
+
handlePlaceholderDragEnter: (columnIndex: number, insertIndex: number) => void
|
|
43
|
+
handlePlaceholderDragLeave: (e?: React.DragEvent<Element>, columnIndex?: number, insertIndex?: number) => void
|
|
44
|
+
|
|
45
|
+
// 表示倍率
|
|
46
|
+
scale?: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 標準HTML5 D&Dイベントハンドラを提供するカスタムフック。
|
|
51
|
+
*
|
|
52
|
+
* `useCallback` でメモ化されたイベントハンドラ群を返す。
|
|
53
|
+
* パフォーマンス向上のため、頻繁に更新される値は `useRef` を介して参照し、
|
|
54
|
+
* コールバック関数の再生成を最小限に抑える。
|
|
55
|
+
*
|
|
56
|
+
* A custom hook that provides memoized event handlers for standard HTML5 drag-and-drop。
|
|
57
|
+
*
|
|
58
|
+
* It returns a set of event handlers memoized with `useCallback`。
|
|
59
|
+
* For performance optimization, frequently updated values are accessed via `useRef`
|
|
60
|
+
* to minimize the re-creation of callback functions。
|
|
61
|
+
*/
|
|
62
|
+
export const useNormalDragHandlers = ({
|
|
63
|
+
debugConfig,
|
|
64
|
+
dragState,
|
|
65
|
+
customDrag,
|
|
66
|
+
dragModes,
|
|
67
|
+
columnPanels,
|
|
68
|
+
dragOverPosition,
|
|
69
|
+
originalPanelPosition,
|
|
70
|
+
setDragState,
|
|
71
|
+
setDragOverPosition,
|
|
72
|
+
setOriginalPanelPosition,
|
|
73
|
+
performPanelMove,
|
|
74
|
+
showMousePosition,
|
|
75
|
+
updateMousePosition,
|
|
76
|
+
resetDragState,
|
|
77
|
+
handlePlaceholderDragEnter,
|
|
78
|
+
handlePlaceholderDragLeave,
|
|
79
|
+
scale = 1,
|
|
80
|
+
}: UseNormalDragHandlersProps) => {
|
|
81
|
+
// --- Refs for Stable Callbacks ---
|
|
82
|
+
// useCallbackの依存配列から頻繁に更新される値(stateやprops)を排除するため、
|
|
83
|
+
// refに格納して最新値を参照する。これにより、コールバック関数のインスタンスが安定し、
|
|
84
|
+
// 不要な再レンダリングを防止する。
|
|
85
|
+
//
|
|
86
|
+
// To prevent re-creating callbacks on every render, we store frequently changing
|
|
87
|
+
// state and props in refs. This allows us to access their latest values inside
|
|
88
|
+
// `useCallback` without adding them to the dependency array, thus stabilizing the callbacks。
|
|
89
|
+
|
|
90
|
+
const debugConfigRef = useRef(debugConfig)
|
|
91
|
+
debugConfigRef.current = debugConfig
|
|
92
|
+
|
|
93
|
+
const columnPanelsRef = useRef(columnPanels)
|
|
94
|
+
columnPanelsRef.current = columnPanels
|
|
95
|
+
|
|
96
|
+
const dragStateRef = useRef(dragState)
|
|
97
|
+
dragStateRef.current = dragState
|
|
98
|
+
const dragOverPositionRef = useRef(dragOverPosition)
|
|
99
|
+
dragOverPositionRef.current = dragOverPosition
|
|
100
|
+
const originalPanelPositionRef = useRef(originalPanelPosition)
|
|
101
|
+
originalPanelPositionRef.current = originalPanelPosition
|
|
102
|
+
|
|
103
|
+
// 表示倍率は render 毎に更新されるため、安定した handleDragStart から最新値を読むために ref 化する。
|
|
104
|
+
// (scale を useCallback の依存に入れるとハンドラが毎回再生成され、他の ref 化した値と設計が不整合になる)
|
|
105
|
+
const scaleRef = useRef(scale)
|
|
106
|
+
scaleRef.current = scale
|
|
107
|
+
|
|
108
|
+
// エッジスクロール管理用のインスタンス
|
|
109
|
+
const edgeScrollManagerRef = useRef<EdgeScrollManager>(new EdgeScrollManager(DEFAULT_EDGE_SCROLL_CONFIG))
|
|
110
|
+
|
|
111
|
+
// スクロール対象の要素を設定(パネルエリアのmain要素。クラスセレクタ禁止のため data 属性で特定)
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const scrollElement = document.querySelector<HTMLElement>("main[data-aqdd-scroll-root]")
|
|
114
|
+
if (scrollElement) {
|
|
115
|
+
edgeScrollManagerRef.current.setScrollElement(scrollElement)
|
|
116
|
+
}
|
|
117
|
+
}, [])
|
|
118
|
+
|
|
119
|
+
// --- Event Handlers ---
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `dragstart` イベントのハンドラ。
|
|
123
|
+
*
|
|
124
|
+
* ドラッグ操作の初期化、ドラッグ対象パネルの元の位置の記録、
|
|
125
|
+
* `dataTransfer` オブジェクトへのデータ設定を行う。
|
|
126
|
+
*
|
|
127
|
+
* Handles the `dragstart` event。
|
|
128
|
+
*
|
|
129
|
+
* Initializes the drag operation, records the original position of the dragged panel,
|
|
130
|
+
* and sets data on the `dataTransfer` object。
|
|
131
|
+
*/
|
|
132
|
+
const handleDragStart = useCallback(
|
|
133
|
+
(e: React.DragEvent, panelId: string) => {
|
|
134
|
+
// カスタムドラッグがアクティブ、またはパネルがカスタムモードの場合、標準ドラッグをキャンセル。
|
|
135
|
+
// Cancel standard drag if a custom drag is active or the panel is in 'custom' mode。
|
|
136
|
+
if (customDrag.isActive) {
|
|
137
|
+
debugLog(debugConfigRef.current, 2, "カスタムドラッグアクティブ中 - 通常ドラッグを無効化:", panelId)
|
|
138
|
+
e.preventDefault()
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
if (dragModes[panelId] === "custom") {
|
|
142
|
+
debugLog(debugConfigRef.current, 2, "カスタムドラッグモード - 通常ドラッグを無効化:", panelId)
|
|
143
|
+
e.preventDefault()
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
debugLog(debugConfigRef.current, 1, "ドラッグ開始:", panelId)
|
|
148
|
+
|
|
149
|
+
// ドロップ時に元の位置と同じか判定するため、ドラッグ開始時の位置を保存。
|
|
150
|
+
// Record the panel's original position to prevent dropping it back in the same spot。
|
|
151
|
+
const columns = Object.keys(columnPanelsRef.current)
|
|
152
|
+
let originalPos: OriginalPanelPosition | null = null
|
|
153
|
+
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
|
|
154
|
+
const columnKey = columns[columnIndex]
|
|
155
|
+
const panelIndex = columnPanelsRef.current[columnKey].indexOf(panelId)
|
|
156
|
+
if (panelIndex !== -1) {
|
|
157
|
+
originalPos = { columnIndex, panelIndex }
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
setOriginalPanelPosition(originalPos)
|
|
162
|
+
|
|
163
|
+
// ドラッグ操作に必要なデータを設定。
|
|
164
|
+
// Set data for the drag operation。
|
|
165
|
+
if (e.dataTransfer) {
|
|
166
|
+
e.dataTransfer.setData("text/plain", panelId)
|
|
167
|
+
e.dataTransfer.effectAllowed = "move"
|
|
168
|
+
|
|
169
|
+
// 縮小表示中はドラッグゴースト画像も同じ倍率で縮小する。
|
|
170
|
+
// If the workspace is scaled down, shrink the native drag ghost to match.
|
|
171
|
+
//
|
|
172
|
+
// setDragImage のスナップショットは要素の「レイアウト境界ボックス」を基準に生成される。
|
|
173
|
+
// そのため縮小後サイズの外枠 (overflow:hidden) の中へ、自然サイズの等倍クローンを
|
|
174
|
+
// transform:scale で入れる。これで raster キャンバス自体が縮小後サイズになり、余白のない
|
|
175
|
+
// 綺麗な縮小ゴーストが得られる (単にクローンへ transform:scale を掛けるだけだと境界ボックスは
|
|
176
|
+
// 原寸のままで、内容が左上 1/scale 象限に描かれる)。ドラッグ元パネルは data-panel-id を持つ
|
|
177
|
+
// 祖先なので、dragHandle="header" でもパネル全体をゴースト化する。
|
|
178
|
+
const currentScale = scaleRef.current
|
|
179
|
+
if (currentScale < 1) {
|
|
180
|
+
const panelElement = (e.currentTarget as HTMLElement).closest<HTMLElement>("[data-panel-id]") ?? (e.currentTarget as HTMLElement)
|
|
181
|
+
const rect = panelElement.getBoundingClientRect()
|
|
182
|
+
const naturalWidth = panelElement.offsetWidth
|
|
183
|
+
const naturalHeight = panelElement.offsetHeight
|
|
184
|
+
|
|
185
|
+
// 自然サイズの等倍クローン (内部レイアウトを元パネルと一致させるため width/height は原寸)
|
|
186
|
+
const inner = panelElement.cloneNode(true) as HTMLElement
|
|
187
|
+
// 同一性属性を除去 (画面外に一瞬存在するクローンが [data-panel-id]/id の重複を作らないように)
|
|
188
|
+
inner.removeAttribute("data-panel-id")
|
|
189
|
+
inner.removeAttribute("id")
|
|
190
|
+
inner.style.margin = "0"
|
|
191
|
+
inner.style.width = `${naturalWidth}px`
|
|
192
|
+
inner.style.height = `${naturalHeight}px`
|
|
193
|
+
inner.style.transform = `scale(${currentScale})`
|
|
194
|
+
inner.style.transformOrigin = "top left"
|
|
195
|
+
|
|
196
|
+
// 縮小後サイズの外枠 (raster キャンバスを縮小後サイズに一致させる)。
|
|
197
|
+
// 画面外 (left:-10000px) に置きスナップショット取得中のちらつきを避ける。
|
|
198
|
+
const ghost = document.createElement("div")
|
|
199
|
+
ghost.style.position = "fixed"
|
|
200
|
+
ghost.style.top = "0"
|
|
201
|
+
ghost.style.left = "-10000px"
|
|
202
|
+
ghost.style.width = `${naturalWidth * currentScale}px`
|
|
203
|
+
ghost.style.height = `${naturalHeight * currentScale}px`
|
|
204
|
+
ghost.style.overflow = "hidden"
|
|
205
|
+
ghost.style.opacity = "0.8"
|
|
206
|
+
ghost.style.pointerEvents = "none"
|
|
207
|
+
ghost.appendChild(inner)
|
|
208
|
+
document.body.appendChild(ghost)
|
|
209
|
+
|
|
210
|
+
// ホットスポットは画面上 (縮小後) の要素内カーソル位置。ghost は縮小後サイズなので直接渡せる
|
|
211
|
+
e.dataTransfer.setDragImage(ghost, e.clientX - rect.left, e.clientY - rect.top)
|
|
212
|
+
|
|
213
|
+
// スナップショットは dragstart 完了時に同期取得されるため、次フレームで撤去する
|
|
214
|
+
requestAnimationFrame(() => ghost.remove())
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// マウス位置の追跡を開始。
|
|
219
|
+
// Set initial mouse position for tracking。
|
|
220
|
+
showMousePosition(e.clientX, e.clientY)
|
|
221
|
+
|
|
222
|
+
// requestAnimationFrameを使用して、ブラウザの描画サイクルに合わせて状態を更新。
|
|
223
|
+
// これにより、ドラッグ開始時の視覚効果がスムーズに適用される。
|
|
224
|
+
// Update the global drag state, using requestAnimationFrame to ensure
|
|
225
|
+
// visual updates are synchronized with the browser's rendering cycle。
|
|
226
|
+
requestAnimationFrame(() => {
|
|
227
|
+
setDragState({
|
|
228
|
+
isDragging: true,
|
|
229
|
+
draggedPanel: panelId,
|
|
230
|
+
dragOverColumn: null,
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
},
|
|
234
|
+
[dragModes, customDrag.isActive, setOriginalPanelPosition, showMousePosition, setDragState],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* `dragend` イベントのハンドラ。
|
|
239
|
+
*
|
|
240
|
+
* ドラッグ操作の終了時に、関連する全ての状態をリセットする。
|
|
241
|
+
*
|
|
242
|
+
* Handles the `dragend` event。
|
|
243
|
+
*
|
|
244
|
+
* Resets all drag-related states upon completion of the drag operation。
|
|
245
|
+
*/
|
|
246
|
+
const handleDragEnd = useCallback(() => {
|
|
247
|
+
debugLog(debugConfigRef.current, 1, "ドラッグ終了")
|
|
248
|
+
|
|
249
|
+
// エッジスクロールを停止
|
|
250
|
+
edgeScrollManagerRef.current.stop()
|
|
251
|
+
|
|
252
|
+
resetDragState()
|
|
253
|
+
}, [resetDragState])
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* カラム上での `dragover` イベントのハンドラ。
|
|
257
|
+
*
|
|
258
|
+
* `document.elementFromPoint` を使用してカーソル直下の要素を特定し、
|
|
259
|
+
* プレースホルダーのハイライト処理を制御する。`useCustomDragHandlers` とロジックを統一済み。
|
|
260
|
+
*
|
|
261
|
+
* Handles the `dragover` event on a column。
|
|
262
|
+
*
|
|
263
|
+
* Uses `document.elementFromPoint` to identify the element directly under the cursor,
|
|
264
|
+
* controlling the placeholder highlighting. The logic is unified with `useCustomDragHandlers`。
|
|
265
|
+
*/
|
|
266
|
+
const handleDragOverWithInsert = useCallback(
|
|
267
|
+
(e: React.DragEvent, columnIndex: number) => {
|
|
268
|
+
e.preventDefault()
|
|
269
|
+
e.dataTransfer.dropEffect = "move"
|
|
270
|
+
|
|
271
|
+
if (!dragStateRef.current.isDragging) return
|
|
272
|
+
|
|
273
|
+
updateMousePosition(e.clientX, e.clientY)
|
|
274
|
+
|
|
275
|
+
// ドラッグオーバー中のカラムIDを状態に保存。
|
|
276
|
+
// Update the column being dragged over。
|
|
277
|
+
if (columnIndex !== dragStateRef.current.dragOverColumn) {
|
|
278
|
+
setDragState((prev) => ({ ...prev, dragOverColumn: columnIndex }))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- 統一ロジック: elementFromPoint を使用して挿入位置を特定 ---
|
|
282
|
+
// --- Unified Logic: Identify insertion point using elementFromPoint ---
|
|
283
|
+
const elementAtPoint = document.elementFromPoint(e.clientX, e.clientY)
|
|
284
|
+
const placeholder = elementAtPoint?.closest<HTMLElement>("[data-placeholder-index]")
|
|
285
|
+
|
|
286
|
+
if (placeholder) {
|
|
287
|
+
// ケース1: カーソル直下にプレースホルダー要素が存在する場合。
|
|
288
|
+
// Case 1: A placeholder element is found directly under the cursor。
|
|
289
|
+
const placeholderIndexAttr = placeholder.getAttribute("data-placeholder-index")
|
|
290
|
+
const placeholderColumnAttr = placeholder.getAttribute("data-placeholder-column")
|
|
291
|
+
|
|
292
|
+
if (placeholderIndexAttr && placeholderColumnAttr) {
|
|
293
|
+
const insertIndex = parseInt(placeholderIndexAttr, 10)
|
|
294
|
+
const targetColumnIndex = parseInt(placeholderColumnAttr, 10)
|
|
295
|
+
// 親コンポーネントのハイライト処理を呼び出す。
|
|
296
|
+
// Trigger the highlight by calling the parent component's handler。
|
|
297
|
+
handlePlaceholderDragEnter(targetColumnIndex, insertIndex)
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
const dropZone = elementAtPoint?.closest<HTMLElement>("[data-drop-zone]")
|
|
301
|
+
if (dropZone) {
|
|
302
|
+
// ケース2: ドロップゾーン上ではあるが、特定のプレースホルダー上ではない場合。
|
|
303
|
+
// (例: パネル自体のコンテンツ上など)
|
|
304
|
+
// この場合、ハイライトは維持したいため、何もしない。
|
|
305
|
+
// カラムから完全に離れた際の `handleColumnDragLeave` が後処理を担当する。
|
|
306
|
+
//
|
|
307
|
+
// Case 2: Over a drop zone, but not a specific placeholder (e.g., over panel content)。
|
|
308
|
+
// Do nothing here to maintain the current highlight. The `handleColumnDragLeave`
|
|
309
|
+
// will handle the cleanup when the cursor leaves the column entirely。
|
|
310
|
+
} else {
|
|
311
|
+
// ケース3: ドロップゾーン外にカーソルがある場合。
|
|
312
|
+
// ハイライトをクリアする。
|
|
313
|
+
// Case 3: Cursor is outside of any drop zone。
|
|
314
|
+
// Clear the highlight。
|
|
315
|
+
handlePlaceholderDragLeave()
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
[updateMousePosition, setDragState, handlePlaceholderDragEnter, handlePlaceholderDragLeave],
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* `drop` イベントのハンドラ。
|
|
324
|
+
*
|
|
325
|
+
* パネルの移動を確定し、関連する状態をリセットする。
|
|
326
|
+
*
|
|
327
|
+
* Handles the `drop` event。
|
|
328
|
+
*
|
|
329
|
+
* Finalizes the panel move and resets related states。
|
|
330
|
+
*/
|
|
331
|
+
const handleDropWithInsert = useCallback(
|
|
332
|
+
(e: React.DragEvent, targetColumn: string) => {
|
|
333
|
+
e.preventDefault()
|
|
334
|
+
const draggedPanel = e.dataTransfer.getData("text/plain")
|
|
335
|
+
const currentOverPos = dragOverPositionRef.current
|
|
336
|
+
|
|
337
|
+
if (!(draggedPanel && currentOverPos)) return
|
|
338
|
+
|
|
339
|
+
debugLog(debugConfigRef.current, 1, "🎯 ドロップ完了:", {
|
|
340
|
+
draggedPanel,
|
|
341
|
+
targetColumn,
|
|
342
|
+
insertPosition: `カラム${currentOverPos.columnIndex} - 位置${currentOverPos.insertIndex}`,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// 親コンポーネントにパネル移動の実行を依頼。
|
|
346
|
+
// Perform the actual panel move in the layout state。
|
|
347
|
+
performPanelMove(draggedPanel, targetColumn, currentOverPos.insertIndex)
|
|
348
|
+
|
|
349
|
+
// ドラッグ状態をリセット。
|
|
350
|
+
// Reset drag states。
|
|
351
|
+
setDragOverPosition(null)
|
|
352
|
+
handleDragEnd()
|
|
353
|
+
},
|
|
354
|
+
[performPanelMove, setDragOverPosition, handleDragEnd],
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* カラムでの `dragleave` イベントのハンドラ。
|
|
359
|
+
*
|
|
360
|
+
* マウスがカラムエリア全体から離れた場合にのみ、ドラッグオーバー関連の状態をクリアする。
|
|
361
|
+
*
|
|
362
|
+
* Handles the `dragleave` event on a column。
|
|
363
|
+
*
|
|
364
|
+
* Clears drag-over related states only when the mouse leaves the entire column area,
|
|
365
|
+
* not when moving between child elements within it。
|
|
366
|
+
*/
|
|
367
|
+
const handleColumnDragLeave = useCallback(
|
|
368
|
+
(e: React.DragEvent) => {
|
|
369
|
+
const dropZone = e.currentTarget
|
|
370
|
+
const relatedTarget = e.relatedTarget as Node | null
|
|
371
|
+
|
|
372
|
+
// `relatedTarget` が dropZone の子要素でないことを確認し、カラムから完全に出たことを判定。
|
|
373
|
+
// If `relatedTarget` doesn't exist or is not a descendant of the `dropZone`,
|
|
374
|
+
// we conclude the mouse has truly left the column area。
|
|
375
|
+
if (!(relatedTarget && dropZone.contains(relatedTarget))) {
|
|
376
|
+
setDragOverPosition(null)
|
|
377
|
+
setDragState((prev) => ({ ...prev, dragOverColumn: null }))
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
[setDragOverPosition, setDragState],
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
// --- Global Event Listener ---
|
|
384
|
+
/**
|
|
385
|
+
* ドラッグ操作中、`document` にグローバルな `dragover` リスナーをアタッチする。
|
|
386
|
+
*
|
|
387
|
+
* これにより、カーソルが特定のドロップゾーン上にない場合でもマウス位置の追跡を継続できる。
|
|
388
|
+
*
|
|
389
|
+
* Attaches a global `dragover` listener to the `document` during a drag operation。
|
|
390
|
+
*
|
|
391
|
+
* This ensures continuous tracking of the mouse position, even when the cursor
|
|
392
|
+
* is not over a specific drop zone。
|
|
393
|
+
*/
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
const handleDragOver = (e: globalThis.DragEvent) => {
|
|
396
|
+
if (!dragStateRef.current.isDragging) return
|
|
397
|
+
updateMousePosition(e.clientX, e.clientY)
|
|
398
|
+
|
|
399
|
+
// エッジスクロールの処理
|
|
400
|
+
edgeScrollManagerRef.current.update(e.clientX, e.clientY)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (dragState.isDragging) {
|
|
404
|
+
document.addEventListener("dragover", handleDragOver)
|
|
405
|
+
return () => {
|
|
406
|
+
document.removeEventListener("dragover", handleDragOver)
|
|
407
|
+
// ドラッグ終了時にエッジスクロールを停止
|
|
408
|
+
edgeScrollManagerRef.current.stop()
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}, [dragState.isDragging, updateMousePosition])
|
|
412
|
+
|
|
413
|
+
// コンポーネントのアンマウント時にエッジスクロールを停止
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
return () => {
|
|
416
|
+
edgeScrollManagerRef.current.stop()
|
|
417
|
+
}
|
|
418
|
+
}, [])
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
handleDragStart,
|
|
422
|
+
handleDragEnd,
|
|
423
|
+
handleDragOverWithInsert,
|
|
424
|
+
handleDropWithInsert,
|
|
425
|
+
handleColumnDragLeave,
|
|
426
|
+
}
|
|
427
|
+
}
|