UNPKG

17.4 kBJavaScriptView Raw
1import { invariant } from '@react-dnd/invariant';
2import { ListenerType } from './interfaces.js';
3import { OptionsReader } from './OptionsReader.js';
4import { distance, inAngleRanges } from './utils/math.js';
5import { getEventClientOffset, getNodeClientOffset } from './utils/offsets.js';
6import { eventShouldEndDrag, eventShouldStartDrag, isTouchEvent } from './utils/predicates.js';
7import { supportsPassive } from './utils/supportsPassive.js';
8const eventNames = {
9 [ListenerType.mouse]: {
10 start: 'mousedown',
11 move: 'mousemove',
12 end: 'mouseup',
13 contextmenu: 'contextmenu'
14 },
15 [ListenerType.touch]: {
16 start: 'touchstart',
17 move: 'touchmove',
18 end: 'touchend'
19 },
20 [ListenerType.keyboard]: {
21 keydown: 'keydown'
22 }
23};
24export class TouchBackendImpl {
25 /**
26 * Generate profiling statistics for the HTML5Backend.
27 */ profile() {
28 var ref;
29 return {
30 sourceNodes: this.sourceNodes.size,
31 sourcePreviewNodes: this.sourcePreviewNodes.size,
32 sourcePreviewNodeOptions: this.sourcePreviewNodeOptions.size,
33 targetNodes: this.targetNodes.size,
34 dragOverTargetIds: ((ref = this.dragOverTargetIds) === null || ref === void 0 ? void 0 : ref.length) || 0
35 };
36 }
37 // public for test
38 get document() {
39 return this.options.document;
40 }
41 setup() {
42 const root = this.options.rootElement;
43 if (!root) {
44 return;
45 }
46 invariant(!TouchBackendImpl.isSetUp, 'Cannot have two Touch backends at the same time.');
47 TouchBackendImpl.isSetUp = true;
48 this.addEventListener(root, 'start', this.getTopMoveStartHandler());
49 this.addEventListener(root, 'start', this.handleTopMoveStartCapture, true);
50 this.addEventListener(root, 'move', this.handleTopMove);
51 this.addEventListener(root, 'move', this.handleTopMoveCapture, true);
52 this.addEventListener(root, 'end', this.handleTopMoveEndCapture, true);
53 if (this.options.enableMouseEvents && !this.options.ignoreContextMenu) {
54 this.addEventListener(root, 'contextmenu', this.handleTopMoveEndCapture);
55 }
56 if (this.options.enableKeyboardEvents) {
57 this.addEventListener(root, 'keydown', this.handleCancelOnEscape, true);
58 }
59 }
60 teardown() {
61 const root = this.options.rootElement;
62 if (!root) {
63 return;
64 }
65 TouchBackendImpl.isSetUp = false;
66 this._mouseClientOffset = {};
67 this.removeEventListener(root, 'start', this.handleTopMoveStartCapture, true);
68 this.removeEventListener(root, 'start', this.handleTopMoveStart);
69 this.removeEventListener(root, 'move', this.handleTopMoveCapture, true);
70 this.removeEventListener(root, 'move', this.handleTopMove);
71 this.removeEventListener(root, 'end', this.handleTopMoveEndCapture, true);
72 if (this.options.enableMouseEvents && !this.options.ignoreContextMenu) {
73 this.removeEventListener(root, 'contextmenu', this.handleTopMoveEndCapture);
74 }
75 if (this.options.enableKeyboardEvents) {
76 this.removeEventListener(root, 'keydown', this.handleCancelOnEscape, true);
77 }
78 this.uninstallSourceNodeRemovalObserver();
79 }
80 addEventListener(subject, event, handler, capture = false) {
81 const options = supportsPassive ? {
82 capture,
83 passive: false
84 } : capture;
85 this.listenerTypes.forEach(function(listenerType) {
86 const evt = eventNames[listenerType][event];
87 if (evt) {
88 subject.addEventListener(evt, handler, options);
89 }
90 });
91 }
92 removeEventListener(subject, event, handler, capture = false) {
93 const options = supportsPassive ? {
94 capture,
95 passive: false
96 } : capture;
97 this.listenerTypes.forEach(function(listenerType) {
98 const evt = eventNames[listenerType][event];
99 if (evt) {
100 subject.removeEventListener(evt, handler, options);
101 }
102 });
103 }
104 connectDragSource(sourceId, node) {
105 const handleMoveStart = this.handleMoveStart.bind(this, sourceId);
106 this.sourceNodes.set(sourceId, node);
107 this.addEventListener(node, 'start', handleMoveStart);
108 return ()=>{
109 this.sourceNodes.delete(sourceId);
110 this.removeEventListener(node, 'start', handleMoveStart);
111 };
112 }
113 connectDragPreview(sourceId, node, options) {
114 this.sourcePreviewNodeOptions.set(sourceId, options);
115 this.sourcePreviewNodes.set(sourceId, node);
116 return ()=>{
117 this.sourcePreviewNodes.delete(sourceId);
118 this.sourcePreviewNodeOptions.delete(sourceId);
119 };
120 }
121 connectDropTarget(targetId, node) {
122 const root = this.options.rootElement;
123 if (!this.document || !root) {
124 return ()=>{
125 /* noop */ };
126 }
127 const handleMove = (e)=>{
128 if (!this.document || !root || !this.monitor.isDragging()) {
129 return;
130 }
131 let coords;
132 /**
133 * Grab the coordinates for the current mouse/touch position
134 */ switch(e.type){
135 case eventNames.mouse.move:
136 coords = {
137 x: e.clientX,
138 y: e.clientY
139 };
140 break;
141 case eventNames.touch.move:
142 var ref, ref1;
143 coords = {
144 x: ((ref = e.touches[0]) === null || ref === void 0 ? void 0 : ref.clientX) || 0,
145 y: ((ref1 = e.touches[0]) === null || ref1 === void 0 ? void 0 : ref1.clientY) || 0
146 };
147 break;
148 }
149 /**
150 * Use the coordinates to grab the element the drag ended on.
151 * If the element is the same as the target node (or any of it's children) then we have hit a drop target and can handle the move.
152 */ const droppedOn = coords != null ? this.document.elementFromPoint(coords.x, coords.y) : undefined;
153 const childMatch = droppedOn && node.contains(droppedOn);
154 if (droppedOn === node || childMatch) {
155 return this.handleMove(e, targetId);
156 }
157 };
158 /**
159 * Attaching the event listener to the body so that touchmove will work while dragging over multiple target elements.
160 */ this.addEventListener(this.document.body, 'move', handleMove);
161 this.targetNodes.set(targetId, node);
162 return ()=>{
163 if (this.document) {
164 this.targetNodes.delete(targetId);
165 this.removeEventListener(this.document.body, 'move', handleMove);
166 }
167 };
168 }
169 getTopMoveStartHandler() {
170 if (!this.options.delayTouchStart && !this.options.delayMouseStart) {
171 return this.handleTopMoveStart;
172 }
173 return this.handleTopMoveStartDelay;
174 }
175 installSourceNodeRemovalObserver(node) {
176 this.uninstallSourceNodeRemovalObserver();
177 this.draggedSourceNode = node;
178 this.draggedSourceNodeRemovalObserver = new MutationObserver(()=>{
179 if (node && !node.parentElement) {
180 this.resurrectSourceNode();
181 this.uninstallSourceNodeRemovalObserver();
182 }
183 });
184 if (!node || !node.parentElement) {
185 return;
186 }
187 this.draggedSourceNodeRemovalObserver.observe(node.parentElement, {
188 childList: true
189 });
190 }
191 resurrectSourceNode() {
192 if (this.document && this.draggedSourceNode) {
193 this.draggedSourceNode.style.display = 'none';
194 this.draggedSourceNode.removeAttribute('data-reactid');
195 this.document.body.appendChild(this.draggedSourceNode);
196 }
197 }
198 uninstallSourceNodeRemovalObserver() {
199 if (this.draggedSourceNodeRemovalObserver) {
200 this.draggedSourceNodeRemovalObserver.disconnect();
201 }
202 this.draggedSourceNodeRemovalObserver = undefined;
203 this.draggedSourceNode = undefined;
204 }
205 constructor(manager, context, options){
206 this.getSourceClientOffset = (sourceId)=>{
207 const element = this.sourceNodes.get(sourceId);
208 return element && getNodeClientOffset(element);
209 };
210 this.handleTopMoveStartCapture = (e)=>{
211 if (!eventShouldStartDrag(e)) {
212 return;
213 }
214 this.moveStartSourceIds = [];
215 };
216 this.handleMoveStart = (sourceId)=>{
217 // Just because we received an event doesn't necessarily mean we need to collect drag sources.
218 // We only collect start collecting drag sources on touch and left mouse events.
219 if (Array.isArray(this.moveStartSourceIds)) {
220 this.moveStartSourceIds.unshift(sourceId);
221 }
222 };
223 this.handleTopMoveStart = (e)=>{
224 if (!eventShouldStartDrag(e)) {
225 return;
226 }
227 // Don't prematurely preventDefault() here since it might:
228 // 1. Mess up scrolling
229 // 2. Mess up long tap (which brings up context menu)
230 // 3. If there's an anchor link as a child, tap won't be triggered on link
231 const clientOffset = getEventClientOffset(e);
232 if (clientOffset) {
233 if (isTouchEvent(e)) {
234 this.lastTargetTouchFallback = e.targetTouches[0];
235 }
236 this._mouseClientOffset = clientOffset;
237 }
238 this.waitingForDelay = false;
239 };
240 this.handleTopMoveStartDelay = (e)=>{
241 if (!eventShouldStartDrag(e)) {
242 return;
243 }
244 const delay = e.type === eventNames.touch.start ? this.options.delayTouchStart : this.options.delayMouseStart;
245 this.timeout = setTimeout(this.handleTopMoveStart.bind(this, e), delay);
246 this.waitingForDelay = true;
247 };
248 this.handleTopMoveCapture = ()=>{
249 this.dragOverTargetIds = [];
250 };
251 this.handleMove = (_evt, targetId)=>{
252 if (this.dragOverTargetIds) {
253 this.dragOverTargetIds.unshift(targetId);
254 }
255 };
256 this.handleTopMove = (e1)=>{
257 if (this.timeout) {
258 clearTimeout(this.timeout);
259 }
260 if (!this.document || this.waitingForDelay) {
261 return;
262 }
263 const { moveStartSourceIds , dragOverTargetIds } = this;
264 const enableHoverOutsideTarget = this.options.enableHoverOutsideTarget;
265 const clientOffset = getEventClientOffset(e1, this.lastTargetTouchFallback);
266 if (!clientOffset) {
267 return;
268 }
269 // If the touch move started as a scroll, or is is between the scroll angles
270 if (this._isScrolling || !this.monitor.isDragging() && inAngleRanges(this._mouseClientOffset.x || 0, this._mouseClientOffset.y || 0, clientOffset.x, clientOffset.y, this.options.scrollAngleRanges)) {
271 this._isScrolling = true;
272 return;
273 }
274 // If we're not dragging and we've moved a little, that counts as a drag start
275 if (!this.monitor.isDragging() && // eslint-disable-next-line no-prototype-builtins
276 this._mouseClientOffset.hasOwnProperty('x') && moveStartSourceIds && distance(this._mouseClientOffset.x || 0, this._mouseClientOffset.y || 0, clientOffset.x, clientOffset.y) > (this.options.touchSlop ? this.options.touchSlop : 0)) {
277 this.moveStartSourceIds = undefined;
278 this.actions.beginDrag(moveStartSourceIds, {
279 clientOffset: this._mouseClientOffset,
280 getSourceClientOffset: this.getSourceClientOffset,
281 publishSource: false
282 });
283 }
284 if (!this.monitor.isDragging()) {
285 return;
286 }
287 const sourceNode = this.sourceNodes.get(this.monitor.getSourceId());
288 this.installSourceNodeRemovalObserver(sourceNode);
289 this.actions.publishDragSource();
290 if (e1.cancelable) e1.preventDefault();
291 // Get the node elements of the hovered DropTargets
292 const dragOverTargetNodes = (dragOverTargetIds || []).map((key)=>this.targetNodes.get(key)
293 ).filter((e)=>!!e
294 );
295 // Get the a ordered list of nodes that are touched by
296 const elementsAtPoint = this.options.getDropTargetElementsAtPoint ? this.options.getDropTargetElementsAtPoint(clientOffset.x, clientOffset.y, dragOverTargetNodes) : this.document.elementsFromPoint(clientOffset.x, clientOffset.y);
297 // Extend list with parents that are not receiving elementsFromPoint events (size 0 elements and svg groups)
298 const elementsAtPointExtended = [];
299 for(const nodeId in elementsAtPoint){
300 // eslint-disable-next-line no-prototype-builtins
301 if (!elementsAtPoint.hasOwnProperty(nodeId)) {
302 continue;
303 }
304 let currentNode = elementsAtPoint[nodeId];
305 if (currentNode != null) {
306 elementsAtPointExtended.push(currentNode);
307 }
308 while(currentNode){
309 currentNode = currentNode.parentElement;
310 if (currentNode && elementsAtPointExtended.indexOf(currentNode) === -1) {
311 elementsAtPointExtended.push(currentNode);
312 }
313 }
314 }
315 const orderedDragOverTargetIds = elementsAtPointExtended// Filter off nodes that arent a hovered DropTargets nodes
316 .filter((node)=>dragOverTargetNodes.indexOf(node) > -1
317 )// Map back the nodes elements to targetIds
318 .map((node)=>this._getDropTargetId(node)
319 )// Filter off possible null rows
320 .filter((node)=>!!node
321 ).filter((id, index, ids)=>ids.indexOf(id) === index
322 );
323 // Invoke hover for drop targets when source node is still over and pointer is outside
324 if (enableHoverOutsideTarget) {
325 for(const targetId in this.targetNodes){
326 const targetNode = this.targetNodes.get(targetId);
327 if (sourceNode && targetNode && targetNode.contains(sourceNode) && orderedDragOverTargetIds.indexOf(targetId) === -1) {
328 orderedDragOverTargetIds.unshift(targetId);
329 break;
330 }
331 }
332 }
333 // Reverse order because dnd-core reverse it before calling the DropTarget drop methods
334 orderedDragOverTargetIds.reverse();
335 this.actions.hover(orderedDragOverTargetIds, {
336 clientOffset: clientOffset
337 });
338 };
339 /**
340 *
341 * visible for testing
342 */ this._getDropTargetId = (node)=>{
343 const keys = this.targetNodes.keys();
344 let next = keys.next();
345 while(next.done === false){
346 const targetId = next.value;
347 if (node === this.targetNodes.get(targetId)) {
348 return targetId;
349 } else {
350 next = keys.next();
351 }
352 }
353 return undefined;
354 };
355 this.handleTopMoveEndCapture = (e)=>{
356 this._isScrolling = false;
357 this.lastTargetTouchFallback = undefined;
358 if (!eventShouldEndDrag(e)) {
359 return;
360 }
361 if (!this.monitor.isDragging() || this.monitor.didDrop()) {
362 this.moveStartSourceIds = undefined;
363 return;
364 }
365 if (e.cancelable) e.preventDefault();
366 this._mouseClientOffset = {};
367 this.uninstallSourceNodeRemovalObserver();
368 this.actions.drop();
369 this.actions.endDrag();
370 };
371 this.handleCancelOnEscape = (e)=>{
372 if (e.key === 'Escape' && this.monitor.isDragging()) {
373 this._mouseClientOffset = {};
374 this.uninstallSourceNodeRemovalObserver();
375 this.actions.endDrag();
376 }
377 };
378 this.options = new OptionsReader(options, context);
379 this.actions = manager.getActions();
380 this.monitor = manager.getMonitor();
381 this.sourceNodes = new Map();
382 this.sourcePreviewNodes = new Map();
383 this.sourcePreviewNodeOptions = new Map();
384 this.targetNodes = new Map();
385 this.listenerTypes = [];
386 this._mouseClientOffset = {};
387 this._isScrolling = false;
388 if (this.options.enableMouseEvents) {
389 this.listenerTypes.push(ListenerType.mouse);
390 }
391 if (this.options.enableTouchEvents) {
392 this.listenerTypes.push(ListenerType.touch);
393 }
394 if (this.options.enableKeyboardEvents) {
395 this.listenerTypes.push(ListenerType.keyboard);
396 }
397 }
398}
399
400//# sourceMappingURL=TouchBackendImpl.js.map
\No newline at end of file