-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
use-list-view-drop-zone.js
260 lines (231 loc) · 7.87 KB
/
use-list-view-drop-zone.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
import { useState, useCallback } from '@wordpress/element';
import {
useThrottle,
__experimentalUseDropZone as useDropZone,
} from '@wordpress/compose';
/**
* Internal dependencies
*/
import {
getDistanceToNearestEdge,
isPointContainedByRect,
} from '../../utils/math';
import useOnBlockDrop from '../use-on-block-drop';
import { store as blockEditorStore } from '../../store';
/** @typedef {import('../../utils/math').WPPoint} WPPoint */
/**
* The type of a drag event.
*
* @typedef {'default'|'file'|'html'} WPDragEventType
*/
/**
* An array representing data for blocks in the DOM used by drag and drop.
*
* @typedef {Object} WPListViewDropZoneBlocks
* @property {string} clientId The client id for the block.
* @property {string} rootClientId The root client id for the block.
* @property {number} blockIndex The block's index.
* @property {Element} element The DOM element representing the block.
* @property {number} innerBlockCount The number of inner blocks the block has.
* @property {boolean} isDraggedBlock Whether the block is currently being dragged.
* @property {boolean} canInsertDraggedBlocksAsSibling Whether the dragged block can be a sibling of this block.
* @property {boolean} canInsertDraggedBlocksAsChild Whether the dragged block can be a child of this block.
*/
/**
* An object containing details of a drop target.
*
* @typedef {Object} WPListViewDropZoneTarget
* @property {string} blockIndex The insertion index.
* @property {string} rootClientId The root client id for the block.
* @property {string|undefined} clientId The client id for the block.
* @property {'top'|'bottom'|'inside'} dropPosition The position relative to the block that the user is dropping to.
* 'inside' refers to nesting as an inner block.
*/
/**
* Determines whether the user positioning the dragged block to nest as an
* inner block.
*
* Presently this is determined by whether the cursor is on the right hand side
* of the block.
*
* @param {WPPoint} point The point representing the cursor position when dragging.
* @param {DOMRect} rect The rectangle.
*/
function isNestingGesture( point, rect ) {
const blockCenterX = rect.left + rect.width / 2;
return point.x > blockCenterX;
}
// Block navigation is always a vertical list, so only allow dropping
// to the above or below a block.
const ALLOWED_DROP_EDGES = [ 'top', 'bottom' ];
/**
* Given blocks data and the cursor position, compute the drop target.
*
* @param {WPListViewDropZoneBlocks} blocksData Data about the blocks in list view.
* @param {WPPoint} position The point representing the cursor position when dragging.
*
* @return {WPListViewDropZoneTarget | undefined} An object containing data about the drop target.
*/
function getListViewDropTarget( blocksData, position ) {
let candidateEdge;
let candidateBlockData;
let candidateDistance;
let candidateRect;
for ( const blockData of blocksData ) {
if ( blockData.isDraggedBlock ) {
continue;
}
const rect = blockData.element.getBoundingClientRect();
const [ distance, edge ] = getDistanceToNearestEdge(
position,
rect,
ALLOWED_DROP_EDGES
);
const isCursorWithinBlock = isPointContainedByRect( position, rect );
if (
candidateDistance === undefined ||
distance < candidateDistance ||
isCursorWithinBlock
) {
candidateDistance = distance;
const index = blocksData.indexOf( blockData );
const previousBlockData = blocksData[ index - 1 ];
// If dragging near the top of a block and the preceding block
// is at the same level, use the preceding block as the candidate
// instead, as later it makes determining a nesting drop easier.
if (
edge === 'top' &&
previousBlockData &&
previousBlockData.rootClientId === blockData.rootClientId &&
! previousBlockData.isDraggedBlock
) {
candidateBlockData = previousBlockData;
candidateEdge = 'bottom';
candidateRect =
previousBlockData.element.getBoundingClientRect();
} else {
candidateBlockData = blockData;
candidateEdge = edge;
candidateRect = rect;
}
// If the mouse position is within the block, break early
// as the user would intend to drop either before or after
// this block.
//
// This solves an issue where some rows in the list view
// tree overlap slightly due to sub-pixel rendering.
if ( isCursorWithinBlock ) {
break;
}
}
}
if ( ! candidateBlockData ) {
return;
}
const isDraggingBelow = candidateEdge === 'bottom';
// If the user is dragging towards the bottom of the block check whether
// they might be trying to nest the block as a child.
// If the block already has inner blocks, this should always be treated
// as nesting since the next block in the tree will be the first child.
if (
isDraggingBelow &&
candidateBlockData.canInsertDraggedBlocksAsChild &&
( candidateBlockData.innerBlockCount > 0 ||
isNestingGesture( position, candidateRect ) )
) {
return {
rootClientId: candidateBlockData.clientId,
blockIndex: 0,
dropPosition: 'inside',
};
}
// If dropping as a sibling, but block cannot be inserted in
// this context, return early.
if ( ! candidateBlockData.canInsertDraggedBlocksAsSibling ) {
return;
}
const offset = isDraggingBelow ? 1 : 0;
return {
rootClientId: candidateBlockData.rootClientId,
clientId: candidateBlockData.clientId,
blockIndex: candidateBlockData.blockIndex + offset,
dropPosition: candidateEdge,
};
}
/**
* A react hook for implementing a drop zone in list view.
*
* @return {WPListViewDropZoneTarget} The drop target.
*/
export default function useListViewDropZone() {
const {
getBlockRootClientId,
getBlockIndex,
getBlockCount,
getDraggedBlockClientIds,
canInsertBlocks,
} = useSelect( blockEditorStore );
const [ target, setTarget ] = useState();
const { rootClientId: targetRootClientId, blockIndex: targetBlockIndex } =
target || {};
const onBlockDrop = useOnBlockDrop( targetRootClientId, targetBlockIndex );
const draggedBlockClientIds = getDraggedBlockClientIds();
const throttled = useThrottle(
useCallback(
( event, currentTarget ) => {
const position = { x: event.clientX, y: event.clientY };
const isBlockDrag = !! draggedBlockClientIds?.length;
const blockElements = Array.from(
currentTarget.querySelectorAll( '[data-block]' )
);
const blocksData = blockElements.map( ( blockElement ) => {
const clientId = blockElement.dataset.block;
const rootClientId = getBlockRootClientId( clientId );
return {
clientId,
rootClientId,
blockIndex: getBlockIndex( clientId ),
element: blockElement,
isDraggedBlock: isBlockDrag
? draggedBlockClientIds.includes( clientId )
: false,
innerBlockCount: getBlockCount( clientId ),
canInsertDraggedBlocksAsSibling: isBlockDrag
? canInsertBlocks(
draggedBlockClientIds,
rootClientId
)
: true,
canInsertDraggedBlocksAsChild: isBlockDrag
? canInsertBlocks( draggedBlockClientIds, clientId )
: true,
};
} );
const newTarget = getListViewDropTarget( blocksData, position );
if ( newTarget ) {
setTarget( newTarget );
}
},
[ draggedBlockClientIds ]
),
200
);
const ref = useDropZone( {
onDrop: onBlockDrop,
onDragOver( event ) {
// `currentTarget` is only available while the event is being
// handled, so get it now and pass it to the thottled function.
// https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget
throttled( event, event.currentTarget );
},
onDragEnd() {
throttled.cancel();
setTarget( null );
},
} );
return { ref, target };
}