1 | import { EnterLeaveCounter } from './EnterLeaveCounter';
|
2 | import { isFirefox } from './BrowserDetector';
|
3 | import { getNodeClientOffset, getEventClientOffset, getDragPreviewOffset, } from './OffsetUtils';
|
4 | import { createNativeDragSource, matchNativeItemType, } from './NativeDragSources';
|
5 | import * as NativeTypes from './NativeTypes';
|
6 | import { OptionsReader } from './OptionsReader';
|
7 | export class HTML5BackendImpl {
|
8 | constructor(manager, globalContext) {
|
9 | this.sourcePreviewNodes = new Map();
|
10 | this.sourcePreviewNodeOptions = new Map();
|
11 | this.sourceNodes = new Map();
|
12 | this.sourceNodeOptions = new Map();
|
13 | this.dragStartSourceIds = null;
|
14 | this.dropTargetIds = [];
|
15 | this.dragEnterTargetIds = [];
|
16 | this.currentNativeSource = null;
|
17 | this.currentNativeHandle = null;
|
18 | this.currentDragSourceNode = null;
|
19 | this.altKeyPressed = false;
|
20 | this.mouseMoveTimeoutTimer = null;
|
21 | this.asyncEndDragFrameId = null;
|
22 | this.dragOverTargetIds = null;
|
23 | this.getSourceClientOffset = (sourceId) => {
|
24 | const source = this.sourceNodes.get(sourceId);
|
25 | return (source && getNodeClientOffset(source)) || null;
|
26 | };
|
27 | this.endDragNativeItem = () => {
|
28 | if (!this.isDraggingNativeItem()) {
|
29 | return;
|
30 | }
|
31 | this.actions.endDrag();
|
32 | if (this.currentNativeHandle) {
|
33 | this.registry.removeSource(this.currentNativeHandle);
|
34 | }
|
35 | this.currentNativeHandle = null;
|
36 | this.currentNativeSource = null;
|
37 | };
|
38 | this.isNodeInDocument = (node) => {
|
39 |
|
40 | return Boolean(node &&
|
41 | this.document &&
|
42 | this.document.body &&
|
43 | document.body.contains(node));
|
44 | };
|
45 | this.endDragIfSourceWasRemovedFromDOM = () => {
|
46 | const node = this.currentDragSourceNode;
|
47 | if (this.isNodeInDocument(node)) {
|
48 | return;
|
49 | }
|
50 | if (this.clearCurrentDragSourceNode()) {
|
51 | this.actions.endDrag();
|
52 | }
|
53 | };
|
54 | this.handleTopDragStartCapture = () => {
|
55 | this.clearCurrentDragSourceNode();
|
56 | this.dragStartSourceIds = [];
|
57 | };
|
58 | this.handleTopDragStart = (e) => {
|
59 | if (e.defaultPrevented) {
|
60 | return;
|
61 | }
|
62 | const { dragStartSourceIds } = this;
|
63 | this.dragStartSourceIds = null;
|
64 | const clientOffset = getEventClientOffset(e);
|
65 |
|
66 | if (this.monitor.isDragging()) {
|
67 | this.actions.endDrag();
|
68 | }
|
69 |
|
70 | this.actions.beginDrag(dragStartSourceIds || [], {
|
71 | publishSource: false,
|
72 | getSourceClientOffset: this.getSourceClientOffset,
|
73 | clientOffset,
|
74 | });
|
75 | const { dataTransfer } = e;
|
76 | const nativeType = matchNativeItemType(dataTransfer);
|
77 | if (this.monitor.isDragging()) {
|
78 | if (dataTransfer && typeof dataTransfer.setDragImage === 'function') {
|
79 |
|
80 |
|
81 |
|
82 | const sourceId = this.monitor.getSourceId();
|
83 | const sourceNode = this.sourceNodes.get(sourceId);
|
84 | const dragPreview = this.sourcePreviewNodes.get(sourceId) || sourceNode;
|
85 | if (dragPreview) {
|
86 | const { anchorX, anchorY, offsetX, offsetY, } = this.getCurrentSourcePreviewNodeOptions();
|
87 | const anchorPoint = { anchorX, anchorY };
|
88 | const offsetPoint = { offsetX, offsetY };
|
89 | const dragPreviewOffset = getDragPreviewOffset(sourceNode, dragPreview, clientOffset, anchorPoint, offsetPoint);
|
90 | dataTransfer.setDragImage(dragPreview, dragPreviewOffset.x, dragPreviewOffset.y);
|
91 | }
|
92 | }
|
93 | try {
|
94 |
|
95 | dataTransfer?.setData('application/json', {});
|
96 | }
|
97 | catch (err) {
|
98 |
|
99 | }
|
100 |
|
101 |
|
102 | this.setCurrentDragSourceNode(e.target);
|
103 |
|
104 | const { captureDraggingState } = this.getCurrentSourcePreviewNodeOptions();
|
105 | if (!captureDraggingState) {
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 | setTimeout(() => this.actions.publishDragSource(), 0);
|
112 | }
|
113 | else {
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 | this.actions.publishDragSource();
|
125 | }
|
126 | }
|
127 | else if (nativeType) {
|
128 |
|
129 | this.beginDragNativeItem(nativeType);
|
130 | }
|
131 | else if (dataTransfer &&
|
132 | !dataTransfer.types &&
|
133 | ((e.target && !e.target.hasAttribute) ||
|
134 | !e.target.hasAttribute('draggable'))) {
|
135 |
|
136 |
|
137 |
|
138 | return;
|
139 | }
|
140 | else {
|
141 |
|
142 | e.preventDefault();
|
143 | }
|
144 | };
|
145 | this.handleTopDragEndCapture = () => {
|
146 | if (this.clearCurrentDragSourceNode()) {
|
147 |
|
148 |
|
149 |
|
150 | this.actions.endDrag();
|
151 | }
|
152 | };
|
153 | this.handleTopDragEnterCapture = (e) => {
|
154 | this.dragEnterTargetIds = [];
|
155 | const isFirstEnter = this.enterLeaveCounter.enter(e.target);
|
156 | if (!isFirstEnter || this.monitor.isDragging()) {
|
157 | return;
|
158 | }
|
159 | const { dataTransfer } = e;
|
160 | const nativeType = matchNativeItemType(dataTransfer);
|
161 | if (nativeType) {
|
162 |
|
163 | this.beginDragNativeItem(nativeType, dataTransfer);
|
164 | }
|
165 | };
|
166 | this.handleTopDragEnter = (e) => {
|
167 | const { dragEnterTargetIds } = this;
|
168 | this.dragEnterTargetIds = [];
|
169 | if (!this.monitor.isDragging()) {
|
170 |
|
171 | return;
|
172 | }
|
173 | this.altKeyPressed = e.altKey;
|
174 | if (!isFirefox()) {
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | this.actions.hover(dragEnterTargetIds, {
|
180 | clientOffset: getEventClientOffset(e),
|
181 | });
|
182 | }
|
183 | const canDrop = dragEnterTargetIds.some((targetId) => this.monitor.canDropOnTarget(targetId));
|
184 | if (canDrop) {
|
185 |
|
186 | e.preventDefault();
|
187 | if (e.dataTransfer) {
|
188 | e.dataTransfer.dropEffect = this.getCurrentDropEffect();
|
189 | }
|
190 | }
|
191 | };
|
192 | this.handleTopDragOverCapture = () => {
|
193 | this.dragOverTargetIds = [];
|
194 | };
|
195 | this.handleTopDragOver = (e) => {
|
196 | const { dragOverTargetIds } = this;
|
197 | this.dragOverTargetIds = [];
|
198 | if (!this.monitor.isDragging()) {
|
199 |
|
200 |
|
201 | e.preventDefault();
|
202 | if (e.dataTransfer) {
|
203 | e.dataTransfer.dropEffect = 'none';
|
204 | }
|
205 | return;
|
206 | }
|
207 | this.altKeyPressed = e.altKey;
|
208 | this.actions.hover(dragOverTargetIds || [], {
|
209 | clientOffset: getEventClientOffset(e),
|
210 | });
|
211 | const canDrop = (dragOverTargetIds || []).some((targetId) => this.monitor.canDropOnTarget(targetId));
|
212 | if (canDrop) {
|
213 |
|
214 | e.preventDefault();
|
215 | if (e.dataTransfer) {
|
216 | e.dataTransfer.dropEffect = this.getCurrentDropEffect();
|
217 | }
|
218 | }
|
219 | else if (this.isDraggingNativeItem()) {
|
220 |
|
221 |
|
222 | e.preventDefault();
|
223 | }
|
224 | else {
|
225 | e.preventDefault();
|
226 | if (e.dataTransfer) {
|
227 | e.dataTransfer.dropEffect = 'none';
|
228 | }
|
229 | }
|
230 | };
|
231 | this.handleTopDragLeaveCapture = (e) => {
|
232 | if (this.isDraggingNativeItem()) {
|
233 | e.preventDefault();
|
234 | }
|
235 | const isLastLeave = this.enterLeaveCounter.leave(e.target);
|
236 | if (!isLastLeave) {
|
237 | return;
|
238 | }
|
239 | if (this.isDraggingNativeItem()) {
|
240 | this.endDragNativeItem();
|
241 | }
|
242 | };
|
243 | this.handleTopDropCapture = (e) => {
|
244 | this.dropTargetIds = [];
|
245 | e.preventDefault();
|
246 | if (this.isDraggingNativeItem()) {
|
247 | this.currentNativeSource?.loadDataTransfer(e.dataTransfer);
|
248 | }
|
249 | this.enterLeaveCounter.reset();
|
250 | };
|
251 | this.handleTopDrop = (e) => {
|
252 | const { dropTargetIds } = this;
|
253 | this.dropTargetIds = [];
|
254 | this.actions.hover(dropTargetIds, {
|
255 | clientOffset: getEventClientOffset(e),
|
256 | });
|
257 | this.actions.drop({ dropEffect: this.getCurrentDropEffect() });
|
258 | if (this.isDraggingNativeItem()) {
|
259 | this.endDragNativeItem();
|
260 | }
|
261 | else {
|
262 | this.endDragIfSourceWasRemovedFromDOM();
|
263 | }
|
264 | };
|
265 | this.handleSelectStart = (e) => {
|
266 | const target = e.target;
|
267 |
|
268 |
|
269 | if (typeof target.dragDrop !== 'function') {
|
270 | return;
|
271 | }
|
272 |
|
273 | if (target.tagName === 'INPUT' ||
|
274 | target.tagName === 'SELECT' ||
|
275 | target.tagName === 'TEXTAREA' ||
|
276 | target.isContentEditable) {
|
277 | return;
|
278 | }
|
279 |
|
280 |
|
281 | e.preventDefault();
|
282 | target.dragDrop();
|
283 | };
|
284 | this.options = new OptionsReader(globalContext);
|
285 | this.actions = manager.getActions();
|
286 | this.monitor = manager.getMonitor();
|
287 | this.registry = manager.getRegistry();
|
288 | this.enterLeaveCounter = new EnterLeaveCounter(this.isNodeInDocument);
|
289 | }
|
290 | |
291 |
|
292 |
|
293 | profile() {
|
294 | return {
|
295 | sourcePreviewNodes: this.sourcePreviewNodes.size,
|
296 | sourcePreviewNodeOptions: this.sourcePreviewNodeOptions.size,
|
297 | sourceNodeOptions: this.sourceNodeOptions.size,
|
298 | sourceNodes: this.sourceNodes.size,
|
299 | dragStartSourceIds: this.dragStartSourceIds?.length || 0,
|
300 | dropTargetIds: this.dropTargetIds.length,
|
301 | dragEnterTargetIds: this.dragEnterTargetIds.length,
|
302 | dragOverTargetIds: this.dragOverTargetIds?.length || 0,
|
303 | };
|
304 | }
|
305 |
|
306 | get window() {
|
307 | return this.options.window;
|
308 | }
|
309 | get document() {
|
310 | return this.options.document;
|
311 | }
|
312 | setup() {
|
313 | if (this.window === undefined) {
|
314 | return;
|
315 | }
|
316 | if (this.window.__isReactDndBackendSetUp) {
|
317 | throw new Error('Cannot have two HTML5 backends at the same time.');
|
318 | }
|
319 | this.window.__isReactDndBackendSetUp = true;
|
320 | this.addEventListeners(this.window);
|
321 | }
|
322 | teardown() {
|
323 | if (this.window === undefined) {
|
324 | return;
|
325 | }
|
326 | this.window.__isReactDndBackendSetUp = false;
|
327 | this.removeEventListeners(this.window);
|
328 | this.clearCurrentDragSourceNode();
|
329 | if (this.asyncEndDragFrameId) {
|
330 | this.window.cancelAnimationFrame(this.asyncEndDragFrameId);
|
331 | }
|
332 | }
|
333 | connectDragPreview(sourceId, node, options) {
|
334 | this.sourcePreviewNodeOptions.set(sourceId, options);
|
335 | this.sourcePreviewNodes.set(sourceId, node);
|
336 | return () => {
|
337 | this.sourcePreviewNodes.delete(sourceId);
|
338 | this.sourcePreviewNodeOptions.delete(sourceId);
|
339 | };
|
340 | }
|
341 | connectDragSource(sourceId, node, options) {
|
342 | this.sourceNodes.set(sourceId, node);
|
343 | this.sourceNodeOptions.set(sourceId, options);
|
344 | const handleDragStart = (e) => this.handleDragStart(e, sourceId);
|
345 | const handleSelectStart = (e) => this.handleSelectStart(e);
|
346 | node.setAttribute('draggable', 'true');
|
347 | node.addEventListener('dragstart', handleDragStart);
|
348 | node.addEventListener('selectstart', handleSelectStart);
|
349 | return () => {
|
350 | this.sourceNodes.delete(sourceId);
|
351 | this.sourceNodeOptions.delete(sourceId);
|
352 | node.removeEventListener('dragstart', handleDragStart);
|
353 | node.removeEventListener('selectstart', handleSelectStart);
|
354 | node.setAttribute('draggable', 'false');
|
355 | };
|
356 | }
|
357 | connectDropTarget(targetId, node) {
|
358 | const handleDragEnter = (e) => this.handleDragEnter(e, targetId);
|
359 | const handleDragOver = (e) => this.handleDragOver(e, targetId);
|
360 | const handleDrop = (e) => this.handleDrop(e, targetId);
|
361 | node.addEventListener('dragenter', handleDragEnter);
|
362 | node.addEventListener('dragover', handleDragOver);
|
363 | node.addEventListener('drop', handleDrop);
|
364 | return () => {
|
365 | node.removeEventListener('dragenter', handleDragEnter);
|
366 | node.removeEventListener('dragover', handleDragOver);
|
367 | node.removeEventListener('drop', handleDrop);
|
368 | };
|
369 | }
|
370 | addEventListeners(target) {
|
371 |
|
372 | if (!target.addEventListener) {
|
373 | return;
|
374 | }
|
375 | target.addEventListener('dragstart', this.handleTopDragStart);
|
376 | target.addEventListener('dragstart', this.handleTopDragStartCapture, true);
|
377 | target.addEventListener('dragend', this.handleTopDragEndCapture, true);
|
378 | target.addEventListener('dragenter', this.handleTopDragEnter);
|
379 | target.addEventListener('dragenter', this.handleTopDragEnterCapture, true);
|
380 | target.addEventListener('dragleave', this.handleTopDragLeaveCapture, true);
|
381 | target.addEventListener('dragover', this.handleTopDragOver);
|
382 | target.addEventListener('dragover', this.handleTopDragOverCapture, true);
|
383 | target.addEventListener('drop', this.handleTopDrop);
|
384 | target.addEventListener('drop', this.handleTopDropCapture, true);
|
385 | }
|
386 | removeEventListeners(target) {
|
387 |
|
388 | if (!target.removeEventListener) {
|
389 | return;
|
390 | }
|
391 | target.removeEventListener('dragstart', this.handleTopDragStart);
|
392 | target.removeEventListener('dragstart', this.handleTopDragStartCapture, true);
|
393 | target.removeEventListener('dragend', this.handleTopDragEndCapture, true);
|
394 | target.removeEventListener('dragenter', this.handleTopDragEnter);
|
395 | target.removeEventListener('dragenter', this.handleTopDragEnterCapture, true);
|
396 | target.removeEventListener('dragleave', this.handleTopDragLeaveCapture, true);
|
397 | target.removeEventListener('dragover', this.handleTopDragOver);
|
398 | target.removeEventListener('dragover', this.handleTopDragOverCapture, true);
|
399 | target.removeEventListener('drop', this.handleTopDrop);
|
400 | target.removeEventListener('drop', this.handleTopDropCapture, true);
|
401 | }
|
402 | getCurrentSourceNodeOptions() {
|
403 | const sourceId = this.monitor.getSourceId();
|
404 | const sourceNodeOptions = this.sourceNodeOptions.get(sourceId);
|
405 | return {
|
406 | dropEffect: this.altKeyPressed ? 'copy' : 'move',
|
407 | ...(sourceNodeOptions || {}),
|
408 | };
|
409 | }
|
410 | getCurrentDropEffect() {
|
411 | if (this.isDraggingNativeItem()) {
|
412 |
|
413 | return 'copy';
|
414 | }
|
415 | return this.getCurrentSourceNodeOptions().dropEffect;
|
416 | }
|
417 | getCurrentSourcePreviewNodeOptions() {
|
418 | const sourceId = this.monitor.getSourceId();
|
419 | const sourcePreviewNodeOptions = this.sourcePreviewNodeOptions.get(sourceId);
|
420 | return {
|
421 | anchorX: 0.5,
|
422 | anchorY: 0.5,
|
423 | captureDraggingState: false,
|
424 | ...(sourcePreviewNodeOptions || {}),
|
425 | };
|
426 | }
|
427 | isDraggingNativeItem() {
|
428 | const itemType = this.monitor.getItemType();
|
429 | return Object.keys(NativeTypes).some((key) => NativeTypes[key] === itemType);
|
430 | }
|
431 | beginDragNativeItem(type, dataTransfer) {
|
432 | this.clearCurrentDragSourceNode();
|
433 | this.currentNativeSource = createNativeDragSource(type, dataTransfer);
|
434 | this.currentNativeHandle = this.registry.addSource(type, this.currentNativeSource);
|
435 | this.actions.beginDrag([this.currentNativeHandle]);
|
436 | }
|
437 | setCurrentDragSourceNode(node) {
|
438 | this.clearCurrentDragSourceNode();
|
439 | this.currentDragSourceNode = node;
|
440 |
|
441 |
|
442 |
|
443 |
|
444 | const MOUSE_MOVE_TIMEOUT = 1000;
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 | this.mouseMoveTimeoutTimer = setTimeout(() => {
|
458 | return (this.window &&
|
459 | this.window.addEventListener('mousemove', this.endDragIfSourceWasRemovedFromDOM, true));
|
460 | }, MOUSE_MOVE_TIMEOUT);
|
461 | }
|
462 | clearCurrentDragSourceNode() {
|
463 | if (this.currentDragSourceNode) {
|
464 | this.currentDragSourceNode = null;
|
465 | if (this.window) {
|
466 | this.window.clearTimeout(this.mouseMoveTimeoutTimer || undefined);
|
467 | this.window.removeEventListener('mousemove', this.endDragIfSourceWasRemovedFromDOM, true);
|
468 | }
|
469 | this.mouseMoveTimeoutTimer = null;
|
470 | return true;
|
471 | }
|
472 | return false;
|
473 | }
|
474 | handleDragStart(e, sourceId) {
|
475 | if (e.defaultPrevented) {
|
476 | return;
|
477 | }
|
478 | if (!this.dragStartSourceIds) {
|
479 | this.dragStartSourceIds = [];
|
480 | }
|
481 | this.dragStartSourceIds.unshift(sourceId);
|
482 | }
|
483 | handleDragEnter(e, targetId) {
|
484 | this.dragEnterTargetIds.unshift(targetId);
|
485 | }
|
486 | handleDragOver(e, targetId) {
|
487 | if (this.dragOverTargetIds === null) {
|
488 | this.dragOverTargetIds = [];
|
489 | }
|
490 | this.dragOverTargetIds.unshift(targetId);
|
491 | }
|
492 | handleDrop(e, targetId) {
|
493 | this.dropTargetIds.unshift(targetId);
|
494 | }
|
495 | }
|