@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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview This file defines the `usePanelManagement` custom hook, a simplified
|
|
3
|
+
* utility for managing panel properties like visibility, size, and DOM element references.
|
|
4
|
+
* It also provides functions for applying visual effects during drag operations.
|
|
5
|
+
*
|
|
6
|
+
* このファイルは `usePanelManagement` カスタムフックを定義します。これは、パネルの可視性、サイズ、
|
|
7
|
+
* DOM要素の参照などのプロパティを管理するための簡略化されたユーティリティです。
|
|
8
|
+
* また、ドラッグ操作中に視覚効果を適用する機能も提供します。
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useRef, useState } from "react"
|
|
11
|
+
import type { PanelSizeInfo } from "../types"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A simplified custom hook for managing panel state such as size, visibility, and refs。
|
|
15
|
+
* It provides basic functionalities without the complexity of ResizeObserver for performance。
|
|
16
|
+
*
|
|
17
|
+
* パネルの状態(サイズ、可視性、refなど)を管理するための簡略化されたカスタムフック。
|
|
18
|
+
* パフォーマンスのためにResizeObserverの複雑さなしに、基本的な機能を提供します。
|
|
19
|
+
* @param {Record<string, boolean>} initialPanelVisibility - An object mapping panel IDs to their initial visibility state.
|
|
20
|
+
* @param {number} scale - Current layout scale.
|
|
21
|
+
*/
|
|
22
|
+
export const usePanelManagement = (initialPanelVisibility: Record<string, boolean>, scale: number = 1) => {
|
|
23
|
+
/**
|
|
24
|
+
* Manages the visibility state for each panel。
|
|
25
|
+
* 各パネルの可視性状態を管理します。
|
|
26
|
+
*/
|
|
27
|
+
const [panelVisibility, setPanelVisibility] = useState(initialPanelVisibility)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stores size information for each panel. Note: This is a simplified implementation。
|
|
31
|
+
* 各パネルのサイズ情報を格納します。注意:これは簡略化された実装です。
|
|
32
|
+
*/
|
|
33
|
+
const [panelSizes, setPanelSizes] = useState<Record<string, PanelSizeInfo>>({})
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A ref to track previous panel sizes to prevent unnecessary updates。
|
|
37
|
+
* 不要な更新を防ぐために、前回のパネルサイズを追跡するref。
|
|
38
|
+
*/
|
|
39
|
+
const previousSizesRef = useRef<Record<string, PanelSizeInfo>>({})
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A ref to hold the cloned drag image element。
|
|
43
|
+
* クローンされたドラッグイメージ要素を保持するための ref。
|
|
44
|
+
*/
|
|
45
|
+
const dragImageRef = useRef<HTMLElement | null>(null)
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A ref to hold the offset of the mouse pointer within the dragged panel。
|
|
49
|
+
* ドラッグされたパネル内のマウスポインタのオフセットを保持するための ref。
|
|
50
|
+
*/
|
|
51
|
+
const dragImageOffset = useRef({ x: 0, y: 0 })
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 表示倍率を安定コールバック (createDragImage / updateDragImagePosition) から最新値で読むための ref。
|
|
55
|
+
* scale を直接クロージャで参照すると useCallback の依存に含める必要が生じ、初期値 (=1) で固定される。
|
|
56
|
+
* A ref to read the latest layout scale from stable callbacks without stale-closure capture.
|
|
57
|
+
*/
|
|
58
|
+
const scaleRef = useRef(scale)
|
|
59
|
+
scaleRef.current = scale
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Retrieves the DOM element for a given panel ID using a data attribute selector。
|
|
63
|
+
* データ属性セレクタを使用して、指定されたパネルIDのDOM要素を取得します。
|
|
64
|
+
* @param {string} panelId - The ID of the panel to find。
|
|
65
|
+
* @returns {HTMLElement | null} The found element or null。
|
|
66
|
+
*/
|
|
67
|
+
const getPanelElement = useCallback((panelId: string): HTMLElement | null => {
|
|
68
|
+
return document.querySelector(`[data-panel-id="${panelId}"]`)
|
|
69
|
+
}, [])
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 内部関数:指定された要素のサイズ情報を更新します。
|
|
73
|
+
* @param panelId - パネルのID
|
|
74
|
+
* @param element - サイズを測定するHTML要素
|
|
75
|
+
*/
|
|
76
|
+
const _updateSize = useCallback(
|
|
77
|
+
(panelId: string, element: Element) => {
|
|
78
|
+
const rect = element.getBoundingClientRect()
|
|
79
|
+
const computedStyle = window.getComputedStyle(element)
|
|
80
|
+
|
|
81
|
+
const newSizeInfo: PanelSizeInfo = {
|
|
82
|
+
width: Math.round(rect.width),
|
|
83
|
+
height: Math.round(rect.height),
|
|
84
|
+
x: Math.round(rect.left),
|
|
85
|
+
y: Math.round(rect.top),
|
|
86
|
+
minHeight: computedStyle.minHeight || "auto",
|
|
87
|
+
maxHeight: computedStyle.maxHeight || "none",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const previousSize = previousSizesRef.current[panelId]
|
|
91
|
+
// オブジェクトの各キーを比較して、変更があった場合のみ更新
|
|
92
|
+
if (!previousSize || (Object.keys(newSizeInfo) as Array<keyof PanelSizeInfo>).some((key) => previousSize[key] !== newSizeInfo[key])) {
|
|
93
|
+
previousSizesRef.current[panelId] = newSizeInfo
|
|
94
|
+
setPanelSizes((prevSizes) => ({
|
|
95
|
+
...prevSizes,
|
|
96
|
+
[panelId]: newSizeInfo,
|
|
97
|
+
}))
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
[], // setPanelSizes は React の state セッターなので依存配列に含める必要はありません
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Updates a panel's size information。
|
|
105
|
+
* パネルのサイズ情報を更新します。
|
|
106
|
+
* @param {string} panelId - The ID of the panel to update。
|
|
107
|
+
*/
|
|
108
|
+
const updatePanelSize = useCallback(
|
|
109
|
+
(panelId: string) => {
|
|
110
|
+
const element = getPanelElement(panelId)
|
|
111
|
+
if (element) {
|
|
112
|
+
_updateSize(panelId, element)
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
[getPanelElement, _updateSize],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Sets a panel's ref and updates its size information。
|
|
120
|
+
* パネルのrefを設定し、サイズ情報を更新します。
|
|
121
|
+
* @param {string} panelId - The ID of the panel。
|
|
122
|
+
* @param {HTMLDivElement | null} el - The panel element。
|
|
123
|
+
*/
|
|
124
|
+
const setPanelRef = useCallback(
|
|
125
|
+
(panelId: string, el: HTMLDivElement | null) => {
|
|
126
|
+
if (el) {
|
|
127
|
+
// パネル要素がマウントされた際にサイズ情報を更新(requestAnimationFrameを使用して最適化)
|
|
128
|
+
requestAnimationFrame(() => _updateSize(panelId, el))
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
[_updateSize],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates and displays a visual clone (drag image) for dragging。
|
|
136
|
+
* ドラッグ用の視覚的なクローン(ドラッグイメージ)を作成して表示します。
|
|
137
|
+
* @param {string} panelId - The ID of the panel to clone。
|
|
138
|
+
* @param {number} offsetX - The horizontal offset of the mouse from the panel's left edge。
|
|
139
|
+
* @param {number} offsetY - The vertical offset of the mouse from the panel's top edge。
|
|
140
|
+
*/
|
|
141
|
+
const createDragImage = useCallback(
|
|
142
|
+
(panelId: string, offsetX: number, offsetY: number) => {
|
|
143
|
+
const originalElement = getPanelElement(panelId)
|
|
144
|
+
if (!originalElement || dragImageRef.current) return
|
|
145
|
+
|
|
146
|
+
const currentScale = scaleRef.current
|
|
147
|
+
const rect = originalElement.getBoundingClientRect()
|
|
148
|
+
const clone = originalElement.cloneNode(true) as HTMLElement
|
|
149
|
+
|
|
150
|
+
// data-panel-id を除去する (getPanelElement の querySelector が DOM 上のクローンを
|
|
151
|
+
// 誤ってヒットしないように。id は下部で drag-image-<id> に上書きするため重複しない)
|
|
152
|
+
clone.removeAttribute("data-panel-id")
|
|
153
|
+
|
|
154
|
+
// Style the clone to be a "ghost" image
|
|
155
|
+
clone.style.position = "fixed"
|
|
156
|
+
clone.style.top = "0px"
|
|
157
|
+
clone.style.left = "0px"
|
|
158
|
+
// レイアウト原寸 (offsetWidth/Height) を使う。getBoundingClientRect は縮小ラッパ配下では
|
|
159
|
+
// 既に ×scale された値を返すため、それを幅に使うと transform:scale で二重縮小 (scale²) になる。
|
|
160
|
+
clone.style.width = `${originalElement.offsetWidth}px`
|
|
161
|
+
clone.style.height = `${originalElement.offsetHeight}px`
|
|
162
|
+
clone.style.pointerEvents = "none"
|
|
163
|
+
clone.style.zIndex = "1000"
|
|
164
|
+
clone.style.opacity = "0.5"
|
|
165
|
+
clone.style.margin = "0"
|
|
166
|
+
clone.style.transition = "none" // Disable transitions during drag for smooth tracking
|
|
167
|
+
clone.style.boxShadow = "0 8px 32px rgba(0,0,0,0.3)"
|
|
168
|
+
// transform-origin を top-left に固定 (既定の center だと縮小時に位置がずれる)
|
|
169
|
+
clone.style.transformOrigin = "top left"
|
|
170
|
+
clone.style.transform = `translate(${rect.left}px, ${rect.top}px) scale(${currentScale})`
|
|
171
|
+
clone.id = `drag-image-${panelId}` // Avoid duplicate IDs
|
|
172
|
+
|
|
173
|
+
document.body.appendChild(clone)
|
|
174
|
+
dragImageRef.current = clone
|
|
175
|
+
dragImageOffset.current = { x: offsetX, y: offsetY }
|
|
176
|
+
},
|
|
177
|
+
[getPanelElement],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Updates the position of the drag image to follow the mouse cursor。
|
|
182
|
+
* ドラッグイメージの位置をマウスカーソルに追従させます。
|
|
183
|
+
* @param {number} x - The current horizontal mouse coordinate。
|
|
184
|
+
* @param {number} y - The current vertical mouse coordinate。
|
|
185
|
+
*/
|
|
186
|
+
const updateDragImagePosition = useCallback((x: number, y: number) => {
|
|
187
|
+
if (dragImageRef.current) {
|
|
188
|
+
const newX = x - dragImageOffset.current.x
|
|
189
|
+
const newY = y - dragImageOffset.current.y
|
|
190
|
+
// 掴んだ瞬間の scale(currentScale) から 5% だけ拡大して「持ち上げ」感を出す (top-left 原点は create 時に設定済み)
|
|
191
|
+
dragImageRef.current.style.transform = `translate(${newX}px, ${newY}px) scale(${scaleRef.current * 1.05})`
|
|
192
|
+
}
|
|
193
|
+
}, [])
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Removes the drag image from the DOM。
|
|
197
|
+
* ドラッグイメージをDOMから削除します。
|
|
198
|
+
*/
|
|
199
|
+
const removeDragImage = useCallback(() => {
|
|
200
|
+
if (dragImageRef.current) {
|
|
201
|
+
dragImageRef.current.remove()
|
|
202
|
+
dragImageRef.current = null
|
|
203
|
+
}
|
|
204
|
+
}, [])
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Toggles the visibility of a specific panel。
|
|
208
|
+
* 特定のパネルの可視性を切り替えます。
|
|
209
|
+
* @param {string} panelId - The ID of the panel to toggle。
|
|
210
|
+
*/
|
|
211
|
+
const togglePanelVisibility = useCallback((panelId: string) => {
|
|
212
|
+
setPanelVisibility((prev) => ({
|
|
213
|
+
...prev,
|
|
214
|
+
[panelId]: !prev[panelId],
|
|
215
|
+
}))
|
|
216
|
+
}, [])
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
// State / 状態
|
|
220
|
+
panelVisibility,
|
|
221
|
+
panelSizes,
|
|
222
|
+
|
|
223
|
+
// State update functions / 状態更新関数
|
|
224
|
+
setPanelVisibility,
|
|
225
|
+
setPanelSizes,
|
|
226
|
+
setPanelRef,
|
|
227
|
+
updatePanelSize,
|
|
228
|
+
togglePanelVisibility,
|
|
229
|
+
|
|
230
|
+
// Utility functions / ユーティリティ関数
|
|
231
|
+
getPanelElement,
|
|
232
|
+
|
|
233
|
+
// Functions for drag image management / ドラッグイメージ管理用の関数
|
|
234
|
+
createDragImage,
|
|
235
|
+
updateDragImagePosition,
|
|
236
|
+
removeDragImage,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview This file defines the `useResponsiveColumns` custom hook, which calculates
|
|
3
|
+
* the optimal number of columns to display based on the screen width and provided constraints.
|
|
4
|
+
* It allows for a responsive layout that adapts to different screen sizes.
|
|
5
|
+
*
|
|
6
|
+
* このファイルは `useResponsiveColumns` カスタムフックを定義します。これは、画面の幅と
|
|
7
|
+
* 提供された制約に基づいて表示する最適なカラム数を計算します。これにより、さまざまな
|
|
8
|
+
* 画面サイズに適応するレスポンシブレイアウトが可能になります。
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Props for the `useResponsiveColumns` hook.
|
|
14
|
+
* `useResponsiveColumns` フックのプロパティ。
|
|
15
|
+
*/
|
|
16
|
+
export interface UseResponsiveColumnsProps {
|
|
17
|
+
/** The minimum width for each column in pixels. / 各カラムの最小幅(ピクセル単位)。 */
|
|
18
|
+
minColumnWidth?: number
|
|
19
|
+
/** The default number of columns to display when not in responsive mode. / レスポンシブモードでない場合に表示するデフォルトのカラム数。 */
|
|
20
|
+
defaultColumns?: number
|
|
21
|
+
/** The maximum number of columns allowed. / 許容される最大カラム数。 */
|
|
22
|
+
maxColumns?: number
|
|
23
|
+
/** The minimum number of columns allowed. / 許容される最小カラム数。 */
|
|
24
|
+
minColumns?: number
|
|
25
|
+
/** The current column widths to consider for responsive calculation. / レスポンシブ計算で考慮する現在のカラム幅。 */
|
|
26
|
+
currentColumnWidths?: Record<string, number>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The return value of the `useResponsiveColumns` hook.
|
|
31
|
+
* `useResponsiveColumns` フックの戻り値。
|
|
32
|
+
*/
|
|
33
|
+
export interface UseResponsiveColumnsReturn {
|
|
34
|
+
/** The calculated number of columns for the current screen width. / 現在の画面幅に対して計算されたカラム数。 */
|
|
35
|
+
responsiveColumnCount: number
|
|
36
|
+
/** A boolean indicating if responsive mode is currently active. / レスポンシブモードが現在アクティブかどうかを示すブール値。 */
|
|
37
|
+
isResponsiveMode: boolean
|
|
38
|
+
/** The current width of the screen in pixels. / 現在の画面幅(ピクセル単位)。 */
|
|
39
|
+
screenWidth: number
|
|
40
|
+
/** A function to toggle responsive mode on and off. / レスポンシブモードのオン/オフを切り替える関数。 */
|
|
41
|
+
toggleResponsiveMode: () => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A custom hook that determines the number of columns for a responsive layout.
|
|
46
|
+
* It listens to screen resize events and calculates the column count based on the available width.
|
|
47
|
+
*
|
|
48
|
+
* レスポンシブレイアウトのカラム数を決定するカスタムフック。
|
|
49
|
+
* 画面のリサイズイベントをリッスンし、利用可能な幅に基づいてカラム数を計算します。
|
|
50
|
+
* @param {UseResponsiveColumnsProps} [props={}] - Configuration properties for the hook.
|
|
51
|
+
* @returns {UseResponsiveColumnsReturn} An object containing the calculated column count and related state.
|
|
52
|
+
*/
|
|
53
|
+
export const useResponsiveColumns = ({ minColumnWidth = 350, defaultColumns = 3, maxColumns = 6, minColumns = 1, currentColumnWidths = {} }: UseResponsiveColumnsProps = {}): UseResponsiveColumnsReturn => {
|
|
54
|
+
/**
|
|
55
|
+
* State to store the current width of the browser window.
|
|
56
|
+
* ブラウザウィンドウの現在の幅を格納するstate。
|
|
57
|
+
*/
|
|
58
|
+
const [screenWidth, setScreenWidth] = useState(0)
|
|
59
|
+
const lastViewportWidthRef = useRef<number>(0)
|
|
60
|
+
const animationFrameIdRef = useRef<number | null>(null)
|
|
61
|
+
/**
|
|
62
|
+
* State to toggle the responsive calculation logic.
|
|
63
|
+
* レスポンシブ計算ロジックを切り替えるためのstate。
|
|
64
|
+
*/
|
|
65
|
+
const [isResponsiveMode, setIsResponsiveMode] = useState(false)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A ref to store the last calculated column count. This helps stabilize the return value
|
|
69
|
+
* of `responsiveColumnCount` by preventing changes if the new calculation results in the same number.
|
|
70
|
+
*
|
|
71
|
+
* 最後に計算されたカラム数を格納するref。これにより、新しい計算が同じ数値になった場合に
|
|
72
|
+
* 変更を防ぎ、`responsiveColumnCount` の戻り値を安定させます。
|
|
73
|
+
*/
|
|
74
|
+
const responsiveColumnCountRef = useRef(defaultColumns)
|
|
75
|
+
|
|
76
|
+
// 画面リサイズリスナーをセットアップおよびクリーンアップする effect
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (typeof window === "undefined") {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const computeViewportWidth = () => {
|
|
83
|
+
const visualViewportWidth = window.visualViewport?.width ?? 0
|
|
84
|
+
const documentElementWidth = document?.documentElement?.clientWidth ?? 0
|
|
85
|
+
const bodyWidth = document?.body?.clientWidth ?? 0
|
|
86
|
+
const innerWidth = window.innerWidth ?? 0
|
|
87
|
+
|
|
88
|
+
if (documentElementWidth > 0) {
|
|
89
|
+
return Math.floor(documentElementWidth)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (visualViewportWidth > 0) {
|
|
93
|
+
return Math.floor(visualViewportWidth)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (bodyWidth > 0) {
|
|
97
|
+
return Math.floor(bodyWidth)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Math.floor(innerWidth)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const updateScreenWidth = () => {
|
|
104
|
+
if (animationFrameIdRef.current !== null) {
|
|
105
|
+
cancelAnimationFrame(animationFrameIdRef.current)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
animationFrameIdRef.current = requestAnimationFrame(() => {
|
|
109
|
+
const width = computeViewportWidth()
|
|
110
|
+
animationFrameIdRef.current = null
|
|
111
|
+
if (width === 0) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Very small viewport fluctuations (e.g., scrollbar appear/disappear) are ignored to prevent layout thrashing.
|
|
116
|
+
if (Math.abs(lastViewportWidthRef.current - width) < 2) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
lastViewportWidthRef.current = width
|
|
121
|
+
setScreenWidth(width)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
updateScreenWidth()
|
|
126
|
+
|
|
127
|
+
window.addEventListener("resize", updateScreenWidth)
|
|
128
|
+
window.addEventListener("orientationchange", updateScreenWidth)
|
|
129
|
+
window.visualViewport?.addEventListener("resize", updateScreenWidth)
|
|
130
|
+
|
|
131
|
+
return () => {
|
|
132
|
+
if (animationFrameIdRef.current !== null) {
|
|
133
|
+
cancelAnimationFrame(animationFrameIdRef.current)
|
|
134
|
+
}
|
|
135
|
+
window.removeEventListener("resize", updateScreenWidth)
|
|
136
|
+
window.removeEventListener("orientationchange", updateScreenWidth)
|
|
137
|
+
window.visualViewport?.removeEventListener("resize", updateScreenWidth)
|
|
138
|
+
}
|
|
139
|
+
}, [])
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Calculates the number of columns to display.
|
|
143
|
+
* This calculation is memoized with `useMemo` to avoid re-computation on every render
|
|
144
|
+
* unless its dependencies (`isResponsiveMode`, `screenWidth`, etc.) change.
|
|
145
|
+
*
|
|
146
|
+
* 表示するカラム数を計算します。
|
|
147
|
+
* この計算は `useMemo` でメモ化され、依存関係(`isResponsiveMode`、`screenWidth`など)が
|
|
148
|
+
* 変更されない限り、再レンダリングごとに再計算されるのを防ぎます。
|
|
149
|
+
*/
|
|
150
|
+
const responsiveColumnCount = useMemo(() => {
|
|
151
|
+
// レスポンシブモードでない場合、または画面幅が未設定の場合はデフォルト値を返す
|
|
152
|
+
if (!isResponsiveMode || screenWidth === 0) {
|
|
153
|
+
responsiveColumnCountRef.current = defaultColumns
|
|
154
|
+
return defaultColumns
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// パディングとギャップを考慮して利用可能な幅を計算する
|
|
158
|
+
const availableWidth = Math.max(screenWidth - 64, 0) // Tailwind の px-8(32px × 2)を想定
|
|
159
|
+
const gapWidth = 24 // Tailwind の gap-6(24px)を想定
|
|
160
|
+
|
|
161
|
+
// 実際のカラム幅を使用してカラム数を計算
|
|
162
|
+
const actualWidths = Object.values(currentColumnWidths)
|
|
163
|
+
|
|
164
|
+
let columns: number
|
|
165
|
+
|
|
166
|
+
if (actualWidths.length > 0) {
|
|
167
|
+
// 手動調整されたカラム幅がある場合はそれを基準に計算
|
|
168
|
+
const averageWidth = actualWidths.reduce((sum, width) => sum + width, 0) / actualWidths.length
|
|
169
|
+
const effectiveMinWidth = Math.max(minColumnWidth, averageWidth)
|
|
170
|
+
|
|
171
|
+
columns = Math.floor((availableWidth + gapWidth) / (effectiveMinWidth + gapWidth))
|
|
172
|
+
|
|
173
|
+
// 実際の幅の合計でも確認
|
|
174
|
+
const totalCurrentWidth = actualWidths.reduce((sum, width) => sum + width, 0) + (actualWidths.length - 1) * gapWidth
|
|
175
|
+
if (totalCurrentWidth > availableWidth && actualWidths.length > minColumns) {
|
|
176
|
+
// 現在の幅が画面幅を超える場合はカラム数を減らす
|
|
177
|
+
columns = Math.min(columns, actualWidths.length - 1)
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// デフォルトの最小幅を使用
|
|
181
|
+
columns = Math.floor((availableWidth + gapWidth) / (minColumnWidth + gapWidth))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 最小および最大カラム数の制約を適用
|
|
185
|
+
columns = Math.max(minColumns, Math.min(maxColumns, columns))
|
|
186
|
+
|
|
187
|
+
// カウントが変更されていない場合は前の値を返して参照を安定させる
|
|
188
|
+
if (responsiveColumnCountRef.current === columns) {
|
|
189
|
+
return responsiveColumnCountRef.current
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
responsiveColumnCountRef.current = columns
|
|
193
|
+
return columns
|
|
194
|
+
}, [isResponsiveMode, screenWidth, defaultColumns, minColumnWidth, maxColumns, minColumns, currentColumnWidths])
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A memoized function to toggle the responsive mode.
|
|
198
|
+
* レスポンシブモードを切り替えるためのメモ化された関数。
|
|
199
|
+
*/
|
|
200
|
+
const toggleResponsiveMode = useCallback(() => {
|
|
201
|
+
setIsResponsiveMode((prev) => !prev)
|
|
202
|
+
}, [])
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
responsiveColumnCount,
|
|
206
|
+
isResponsiveMode,
|
|
207
|
+
screenWidth,
|
|
208
|
+
toggleResponsiveMode,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* リサイズハンドルの基礎レイアウトとカーソル指定 */
|
|
2
|
+
.resize-handle {
|
|
3
|
+
position: absolute;
|
|
4
|
+
top: 0;
|
|
5
|
+
bottom: 0;
|
|
6
|
+
width: 12px;
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
cursor: col-resize;
|
|
11
|
+
z-index: 10;
|
|
12
|
+
transition: background-color 0.2s ease;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* 右側ハンドルの配置を決定 */
|
|
16
|
+
.resize-handle.right {
|
|
17
|
+
right: -6px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* 左側ハンドルの配置を決定 */
|
|
21
|
+
.resize-handle.left {
|
|
22
|
+
left: -6px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ホバーやドラッグ時に背景色を強調 */
|
|
26
|
+
.resize-handle.resizing,
|
|
27
|
+
.resize-handle:hover {
|
|
28
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ハンドルバー本体の見た目を定義 */
|
|
32
|
+
.resize-handle-bar {
|
|
33
|
+
width: 2px;
|
|
34
|
+
height: 40px;
|
|
35
|
+
background-color: rgba(99, 102, 241, 0.5);
|
|
36
|
+
border-radius: 999px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ドラッグ中のハンドルバーを強調表示 */
|
|
40
|
+
.resize-handle.resizing .resize-handle-bar {
|
|
41
|
+
background-color: rgba(99, 102, 241, 0.9);
|
|
42
|
+
box-shadow: 0 0 6px rgba(99, 102, 241, 0.6);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* デバッグ情報ラベルの位置とスタイル */
|
|
46
|
+
.resize-handle-debug-info {
|
|
47
|
+
position: absolute;
|
|
48
|
+
bottom: 8px;
|
|
49
|
+
left: 50%;
|
|
50
|
+
transform: translateX(-50%);
|
|
51
|
+
background: rgba(30, 64, 175, 0.85);
|
|
52
|
+
color: white;
|
|
53
|
+
padding: 4px 8px;
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
font-size: 11px;
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* カラム幅デバッグ表示のスタイル */
|
|
61
|
+
.column-debug-size {
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: 8px;
|
|
64
|
+
right: 12px;
|
|
65
|
+
background: rgba(30, 64, 175, 0.85);
|
|
66
|
+
color: white;
|
|
67
|
+
padding: 4px 8px;
|
|
68
|
+
border-radius: 4px;
|
|
69
|
+
font-size: 11px;
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
}
|