@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.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -10
  3. package/dist/drag-drop/hooks/useCustomDragHandlers.d.ts.map +1 -1
  4. package/dist/index.es.js +88 -88
  5. package/dist/index.umd.js +2 -2
  6. package/dist/styles/drag-drop-panels.standalone.css +3 -0
  7. package/package.json +11 -4
  8. package/src/DragDropLayout.tsx +751 -0
  9. package/src/drag-drop/ColumnControlPanel.tsx +123 -0
  10. package/src/drag-drop/ColumnInsertPanel.tsx +106 -0
  11. package/src/drag-drop/DebugComponents.tsx +222 -0
  12. package/src/drag-drop/DebugControlPanel.tsx +200 -0
  13. package/src/drag-drop/DebugOverlay.tsx +250 -0
  14. package/src/drag-drop/DragDropLayout.css +130 -0
  15. package/src/drag-drop/ResizeHandle.tsx +208 -0
  16. package/src/drag-drop/ResponsiveControl.tsx +33 -0
  17. package/src/drag-drop/hooks/dragGhostScale.spec.tsx +153 -0
  18. package/src/drag-drop/hooks/useColumnLayout.ts +457 -0
  19. package/src/drag-drop/hooks/useCustomDragHandlers.tsx +333 -0
  20. package/src/drag-drop/hooks/useDragDropColumns.tsx +446 -0
  21. package/src/drag-drop/hooks/useDragDropState.tsx +200 -0
  22. package/src/drag-drop/hooks/useFitScale.ts +48 -0
  23. package/src/drag-drop/hooks/useMouseTracking.tsx +102 -0
  24. package/src/drag-drop/hooks/useNormalDragHandlers.tsx +427 -0
  25. package/src/drag-drop/hooks/usePanelManagement.tsx +238 -0
  26. package/src/drag-drop/hooks/useResponsiveColumns.ts +210 -0
  27. package/src/drag-drop/resize-handle.css +71 -0
  28. package/src/drag-drop/types.ts +248 -0
  29. package/src/drag-drop/utils/columnIdUtils.ts +104 -0
  30. package/src/drag-drop/utils/edgeScrollUtils.ts +124 -0
  31. package/src/drag-drop/utils/simple-logger.ts +162 -0
  32. package/src/index.ts +60 -0
  33. package/src/styles/standalone.entry.css +27 -0
@@ -0,0 +1,333 @@
1
+ /**
2
+ * @fileoverview This file defines the `useCustomDragHandlers` custom hook.
3
+ * This hook encapsulates the complex logic for a custom drag-and-drop implementation
4
+ * that does not rely on the standard HTML5 Drag and Drop API. It manages drag initiation,
5
+ * movement, and completion using mouse events (`mousedown`, `mousemove`, `mouseup`).
6
+ *
7
+ * このファイルは `useCustomDragHandlers` カスタムフックを定義します。
8
+ * このフックは、標準のHTML5ドラッグ&ドロップAPIに依存しない、カスタムのドラッグ&ドロップ実装の
9
+ * 複雑なロジックをカプセル化します。マウスイベント(`mousedown`, `mousemove`, `mouseup`)を
10
+ * 使用して、ドラッグの開始、移動、完了を管理します。
11
+ */
12
+ import { type DragEvent, type MouseEvent, useCallback, useEffect, useRef } from "react"
13
+ import { debugLog } from "../DebugComponents"
14
+ import type { ClickState, CustomDragState, DebugConfig, DragOverPosition, DragState, OriginalPanelPosition } from "../types"
15
+ import { DEFAULT_EDGE_SCROLL_CONFIG, EdgeScrollManager } from "../utils/edgeScrollUtils"
16
+
17
+ interface UseCustomDragHandlersProps {
18
+ debugConfig: DebugConfig
19
+ dragState: DragState
20
+ customDrag: CustomDragState
21
+ clickState: ClickState
22
+ dragModes: Record<string, "normal" | "custom">
23
+ isMouseDown: boolean
24
+ originalPanelPosition: OriginalPanelPosition | null
25
+ dragOverPosition: DragOverPosition | null
26
+ setCustomDrag: (state: CustomDragState) => void
27
+ setDragState: (state: DragState | ((prev: DragState) => DragState)) => void
28
+ setDragOverPosition: (position: DragOverPosition | null) => void
29
+ setOriginalPanelPosition: (position: OriginalPanelPosition | null) => void
30
+ setClickState: (state: ClickState) => void
31
+ setIsMouseDown: (isDown: boolean) => void
32
+ findPanelPosition: (panelId: string) => OriginalPanelPosition | null
33
+ performPanelMove: (panelId: string, targetColumn: string, insertIndex: number) => void
34
+ showMousePosition: (x: number, y: number) => void
35
+ updateMousePosition: (x: number, y: number) => void
36
+ recordMouseDown: (x: number, y: number) => void
37
+ recordClickPosition: (x: number, y: number) => void
38
+ isVirtualClick: (x: number, y: number, maxTime?: number, maxDistance?: number) => boolean
39
+ calculateMoveDistance: (x: number, y: number) => number
40
+ clearMouseTracking: () => void
41
+ resetDragState: () => void
42
+ resetClickState: () => void
43
+ createDragImage: (panelId: string, offsetX: number, offsetY: number) => void
44
+ updateDragImagePosition: (x: number, y: number) => void
45
+ removeDragImage: () => void
46
+ handlePlaceholderDragEnter: (columnIndex: number, insertIndex: number) => void
47
+ handlePlaceholderDragLeave: (e?: DragEvent<Element>, columnIndex?: number, insertIndex?: number) => void
48
+ }
49
+
50
+ export const useCustomDragHandlers = ({
51
+ debugConfig,
52
+ dragState,
53
+ customDrag,
54
+ clickState,
55
+ dragModes,
56
+ isMouseDown,
57
+ dragOverPosition,
58
+ setCustomDrag,
59
+ setDragState,
60
+ setDragOverPosition,
61
+ setOriginalPanelPosition,
62
+ setClickState,
63
+ setIsMouseDown,
64
+ findPanelPosition,
65
+ performPanelMove,
66
+ showMousePosition,
67
+ updateMousePosition,
68
+ recordMouseDown,
69
+ recordClickPosition,
70
+ isVirtualClick,
71
+ calculateMoveDistance,
72
+ clearMouseTracking,
73
+ resetDragState,
74
+ resetClickState,
75
+ createDragImage,
76
+ updateDragImagePosition,
77
+ removeDragImage,
78
+ handlePlaceholderDragEnter,
79
+ handlePlaceholderDragLeave,
80
+ }: UseCustomDragHandlersProps) => {
81
+ const debugConfigRef = useRef(debugConfig)
82
+ debugConfigRef.current = debugConfig
83
+
84
+ const clickStateTimeoutRef = useRef<number | null>(null)
85
+ const dragStartMouseCoordRef = useRef<{ x: number; y: number } | null>(null)
86
+ const clickCountRef = useRef(0)
87
+
88
+ const edgeScrollManagerRef = useRef<EdgeScrollManager>(new EdgeScrollManager(DEFAULT_EDGE_SCROLL_CONFIG))
89
+
90
+ useEffect(() => {
91
+ // DragDropLayout が出力する data 属性で特定する (クラスセレクタは utility 変更で壊れるため禁止)
92
+ const scrollElement = document.querySelector<HTMLElement>("main[data-aqdd-scroll-root]")
93
+ if (scrollElement) {
94
+ edgeScrollManagerRef.current.setScrollElement(scrollElement)
95
+ }
96
+ }, [])
97
+
98
+ const dragOverPositionRef = useRef(dragOverPosition)
99
+ useEffect(() => {
100
+ dragOverPositionRef.current = dragOverPosition
101
+ debugLog(debugConfigRef.current, 2, "dragOverPositionRef更新:", dragOverPosition)
102
+ }, [dragOverPosition])
103
+
104
+ const handleGlobalMouseMove = useCallback(
105
+ (e: globalThis.MouseEvent) => {
106
+ if (customDrag.isActive || clickState.isClicked) {
107
+ e.preventDefault()
108
+ const selection = window.getSelection()
109
+ if (selection && selection.rangeCount > 0) {
110
+ selection.removeAllRanges()
111
+ }
112
+ }
113
+
114
+ if (customDrag.isActive) {
115
+ updateMousePosition(e.clientX, e.clientY)
116
+ updateDragImagePosition(e.clientX, e.clientY)
117
+ edgeScrollManagerRef.current.update(e.clientX, e.clientY)
118
+
119
+ const elementAtPoint = document.elementFromPoint(e.clientX, e.clientY)
120
+ const dropZone = elementAtPoint?.closest<HTMLElement>("[data-drop-zone]")
121
+
122
+ if (dropZone) {
123
+ document.body.classList.add("droppable")
124
+ const columnIndexAttr = dropZone.getAttribute("data-column-index")
125
+ const columnIndex = columnIndexAttr ? parseInt(columnIndexAttr, 10) : null
126
+
127
+ if (columnIndex !== null && columnIndex !== dragState.dragOverColumn) {
128
+ setDragState((prev) => ({ ...prev, dragOverColumn: columnIndex }))
129
+ }
130
+
131
+ const placeholder = elementAtPoint?.closest<HTMLElement>("[data-placeholder-index]")
132
+ if (placeholder) {
133
+ const placeholderIndexAttr = placeholder.getAttribute("data-placeholder-index")
134
+ const placeholderColumnAttr = placeholder.getAttribute("data-placeholder-column")
135
+
136
+ if (placeholderIndexAttr && placeholderColumnAttr) {
137
+ const insertIndex = parseInt(placeholderIndexAttr, 10)
138
+ const targetColumnIndex = parseInt(placeholderColumnAttr, 10)
139
+ handlePlaceholderDragEnter(targetColumnIndex, insertIndex)
140
+ }
141
+ } else {
142
+ handlePlaceholderDragLeave()
143
+ }
144
+ } else {
145
+ document.body.classList.remove("droppable")
146
+ setDragOverPosition(null)
147
+ setDragState((prev) => ({ ...prev, dragOverColumn: null }))
148
+ }
149
+ } else if (isMouseDown && clickState.isClicked && clickState.panelId) {
150
+ if (dragModes[clickState.panelId] !== "custom") return
151
+
152
+ const distance = calculateMoveDistance(e.clientX, e.clientY)
153
+ if (distance < 10) return
154
+
155
+ debugLog(debugConfigRef.current, 1, "🚀 カスタムドラッグ開始 -", clickState.panelId)
156
+
157
+ const element = document.querySelector<HTMLElement>(`[data-panel-id="${clickState.panelId}"]`)
158
+ if (element && dragStartMouseCoordRef.current) {
159
+ const rect = element.getBoundingClientRect()
160
+ const offsetX = dragStartMouseCoordRef.current.x - rect.left
161
+ const offsetY = dragStartMouseCoordRef.current.y - rect.top
162
+
163
+ createDragImage(clickState.panelId, offsetX, offsetY)
164
+ document.body.classList.add("custom-dragging")
165
+
166
+ setCustomDrag({
167
+ isActive: true,
168
+ panelId: clickState.panelId,
169
+ startX: offsetX,
170
+ startY: offsetY,
171
+ })
172
+ }
173
+
174
+ setDragState({ isDragging: true, draggedPanel: clickState.panelId, dragOverColumn: null })
175
+ setOriginalPanelPosition(findPanelPosition(clickState.panelId))
176
+ showMousePosition(e.clientX, e.clientY)
177
+
178
+ setClickState({ isClicked: false, panelId: null })
179
+ setIsMouseDown(false)
180
+ clearMouseTracking()
181
+ dragStartMouseCoordRef.current = null
182
+
183
+ if (clickStateTimeoutRef.current) {
184
+ clearTimeout(clickStateTimeoutRef.current)
185
+ clickStateTimeoutRef.current = null
186
+ }
187
+ }
188
+ },
189
+ [
190
+ customDrag.isActive,
191
+ clickState,
192
+ dragState.dragOverColumn,
193
+ dragModes,
194
+ isMouseDown,
195
+ updateMousePosition,
196
+ setDragState,
197
+ setDragOverPosition,
198
+ calculateMoveDistance,
199
+ findPanelPosition,
200
+ setCustomDrag,
201
+ setOriginalPanelPosition,
202
+ showMousePosition,
203
+ setClickState,
204
+ setIsMouseDown,
205
+ clearMouseTracking,
206
+ createDragImage,
207
+ updateDragImagePosition,
208
+ handlePlaceholderDragEnter,
209
+ handlePlaceholderDragLeave,
210
+ ],
211
+ )
212
+
213
+ const handleGlobalMouseUp = useCallback(
214
+ (e: globalThis.MouseEvent) => {
215
+ debugLog(debugConfigRef.current, 1, "🐭 カスタムドラッグハンドラー - グローバルマウスアップ", {
216
+ isActive: customDrag.isActive,
217
+ panelId: customDrag.panelId,
218
+ dragMode: customDrag.panelId ? dragModes[customDrag.panelId] : "unknown",
219
+ })
220
+
221
+ if (customDrag.isActive) {
222
+ edgeScrollManagerRef.current.stop()
223
+
224
+ const currentDragOverPosition = dragOverPositionRef.current
225
+ if (currentDragOverPosition) {
226
+ const elementUnderMouse = document.elementFromPoint(e.clientX, e.clientY)
227
+ const dropZone = elementUnderMouse?.closest<HTMLElement>("[data-drop-zone]")
228
+
229
+ if (dropZone) {
230
+ const targetColumnKey = dropZone.getAttribute("data-column-key")
231
+ if (targetColumnKey && dragModes[customDrag.panelId] === "custom") {
232
+ debugLog(debugConfigRef.current, 1, "🔄 カスタムドラッグパネル移動実行:", customDrag.panelId, "→", targetColumnKey)
233
+ performPanelMove(customDrag.panelId, targetColumnKey, currentDragOverPosition.insertIndex)
234
+ } else {
235
+ debugLog(debugConfigRef.current, 1, "🚫 カスタムドラッグパネル移動スキップ:", customDrag.panelId, "モード:", dragModes[customDrag.panelId])
236
+ }
237
+ }
238
+ }
239
+ removeDragImage()
240
+ document.body.classList.remove("custom-dragging", "droppable")
241
+ resetDragState()
242
+ clickCountRef.current = 0
243
+ } else if (isVirtualClick(e.clientX, e.clientY)) {
244
+ const elementUnderMouse = document.elementFromPoint(e.clientX, e.clientY)
245
+ const panelElement = elementUnderMouse?.closest<HTMLElement>("[data-panel-id]")
246
+ const panelId = panelElement?.getAttribute("data-panel-id") ?? null
247
+
248
+ if (!(panelElement && panelId) || dragModes[panelId] !== "custom") {
249
+ if (clickStateTimeoutRef.current) {
250
+ clearTimeout(clickStateTimeoutRef.current)
251
+ clickStateTimeoutRef.current = null
252
+ }
253
+ resetClickState()
254
+ dragStartMouseCoordRef.current = null
255
+ clickCountRef.current = 0
256
+ return
257
+ }
258
+
259
+ clickCountRef.current += 1
260
+
261
+ if (clickState.isClicked === false && clickCountRef.current === 1) {
262
+ debugLog(debugConfigRef.current, 1, "🖱️ 仮想クリック検出 - ドラッグ待機状態へ:", panelId)
263
+ setClickState({ isClicked: true, panelId })
264
+ clickStateTimeoutRef.current = window.setTimeout(() => {
265
+ debugLog(debugConfigRef.current, 1, "⏳ クリック待機状態を自動解除:", panelId)
266
+ resetClickState()
267
+ dragStartMouseCoordRef.current = null
268
+ clickCountRef.current = 0
269
+ }, 2000)
270
+ } else {
271
+ debugLog(debugConfigRef.current, 1, "🚫 クリック待機状態を解除 - 仮想クリック検出停止:", panelId)
272
+ if (clickStateTimeoutRef.current) {
273
+ clearTimeout(clickStateTimeoutRef.current)
274
+ clickStateTimeoutRef.current = null
275
+ }
276
+ resetClickState()
277
+ dragStartMouseCoordRef.current = null
278
+ clickCountRef.current = 0
279
+ clickStateTimeoutRef.current = window.setTimeout(() => {
280
+ debugLog(debugConfigRef.current, 1, "⏳ 仮想クリック検出再開:", panelId)
281
+ }, 2000)
282
+ }
283
+ } else if (clickState.isClicked) {
284
+ resetClickState()
285
+ dragStartMouseCoordRef.current = null
286
+ clickCountRef.current = 0
287
+ }
288
+
289
+ if (isMouseDown) {
290
+ setIsMouseDown(false)
291
+ }
292
+ },
293
+ [customDrag.isActive, customDrag.panelId, clickState, dragModes, isMouseDown, performPanelMove, resetDragState, resetClickState, isVirtualClick, setClickState, setIsMouseDown, removeDragImage],
294
+ )
295
+
296
+ const handleMouseDown = useCallback(
297
+ (e: MouseEvent, panelId: string) => {
298
+ if (dragModes[panelId] !== "custom") {
299
+ debugLog(debugConfigRef.current, 1, "🚫 カスタムドラッグモードではない - スキップ:", panelId)
300
+ return
301
+ }
302
+
303
+ recordMouseDown(e.clientX, e.clientY)
304
+
305
+ if (clickState.isClicked && clickState.panelId === panelId) {
306
+ setIsMouseDown(true)
307
+
308
+ if (clickStateTimeoutRef.current !== null) {
309
+ clearTimeout(clickStateTimeoutRef.current)
310
+ clickStateTimeoutRef.current = null
311
+ }
312
+ recordClickPosition(e.clientX, e.clientY)
313
+ dragStartMouseCoordRef.current = { x: e.clientX, y: e.clientY }
314
+ }
315
+ },
316
+ [clickState, dragModes, recordMouseDown, setIsMouseDown, recordClickPosition],
317
+ )
318
+
319
+ useEffect(() => {
320
+ return () => {
321
+ if (clickStateTimeoutRef.current !== null) {
322
+ clearTimeout(clickStateTimeoutRef.current)
323
+ }
324
+ edgeScrollManagerRef.current.stop()
325
+ }
326
+ }, [])
327
+
328
+ return {
329
+ handleGlobalMouseMove,
330
+ handleGlobalMouseUp,
331
+ handleMouseDown,
332
+ }
333
+ }