react-dnd-touch-backend
Version:
Touch backend for react-dnd
400 lines (399 loc) • 17.4 kB
JavaScript
import { invariant } from '@react-dnd/invariant';
import { ListenerType } from './interfaces.js';
import { OptionsReader } from './OptionsReader.js';
import { distance, inAngleRanges } from './utils/math.js';
import { getEventClientOffset, getNodeClientOffset } from './utils/offsets.js';
import { eventShouldEndDrag, eventShouldStartDrag, isTouchEvent } from './utils/predicates.js';
import { supportsPassive } from './utils/supportsPassive.js';
const eventNames = {
[ListenerType.mouse]: {
start: 'mousedown',
move: 'mousemove',
end: 'mouseup',
contextmenu: 'contextmenu'
},
[ListenerType.touch]: {
start: 'touchstart',
move: 'touchmove',
end: 'touchend'
},
[ListenerType.keyboard]: {
keydown: 'keydown'
}
};
export class TouchBackendImpl {
/**
* Generate profiling statistics for the HTML5Backend.
*/ profile() {
var ref;
return {
sourceNodes: this.sourceNodes.size,
sourcePreviewNodes: this.sourcePreviewNodes.size,
sourcePreviewNodeOptions: this.sourcePreviewNodeOptions.size,
targetNodes: this.targetNodes.size,
dragOverTargetIds: ((ref = this.dragOverTargetIds) === null || ref === void 0 ? void 0 : ref.length) || 0
};
}
// public for test
get document() {
return this.options.document;
}
setup() {
const root = this.options.rootElement;
if (!root) {
return;
}
invariant(!TouchBackendImpl.isSetUp, 'Cannot have two Touch backends at the same time.');
TouchBackendImpl.isSetUp = true;
this.addEventListener(root, 'start', this.getTopMoveStartHandler());
this.addEventListener(root, 'start', this.handleTopMoveStartCapture, true);
this.addEventListener(root, 'move', this.handleTopMove);
this.addEventListener(root, 'move', this.handleTopMoveCapture, true);
this.addEventListener(root, 'end', this.handleTopMoveEndCapture, true);
if (this.options.enableMouseEvents && !this.options.ignoreContextMenu) {
this.addEventListener(root, 'contextmenu', this.handleTopMoveEndCapture);
}
if (this.options.enableKeyboardEvents) {
this.addEventListener(root, 'keydown', this.handleCancelOnEscape, true);
}
}
teardown() {
const root = this.options.rootElement;
if (!root) {
return;
}
TouchBackendImpl.isSetUp = false;
this._mouseClientOffset = {};
this.removeEventListener(root, 'start', this.handleTopMoveStartCapture, true);
this.removeEventListener(root, 'start', this.handleTopMoveStart);
this.removeEventListener(root, 'move', this.handleTopMoveCapture, true);
this.removeEventListener(root, 'move', this.handleTopMove);
this.removeEventListener(root, 'end', this.handleTopMoveEndCapture, true);
if (this.options.enableMouseEvents && !this.options.ignoreContextMenu) {
this.removeEventListener(root, 'contextmenu', this.handleTopMoveEndCapture);
}
if (this.options.enableKeyboardEvents) {
this.removeEventListener(root, 'keydown', this.handleCancelOnEscape, true);
}
this.uninstallSourceNodeRemovalObserver();
}
addEventListener(subject, event, handler, capture = false) {
const options = supportsPassive ? {
capture,
passive: false
} : capture;
this.listenerTypes.forEach(function(listenerType) {
const evt = eventNames[listenerType][event];
if (evt) {
subject.addEventListener(evt, handler, options);
}
});
}
removeEventListener(subject, event, handler, capture = false) {
const options = supportsPassive ? {
capture,
passive: false
} : capture;
this.listenerTypes.forEach(function(listenerType) {
const evt = eventNames[listenerType][event];
if (evt) {
subject.removeEventListener(evt, handler, options);
}
});
}
connectDragSource(sourceId, node) {
const handleMoveStart = this.handleMoveStart.bind(this, sourceId);
this.sourceNodes.set(sourceId, node);
this.addEventListener(node, 'start', handleMoveStart);
return ()=>{
this.sourceNodes.delete(sourceId);
this.removeEventListener(node, 'start', handleMoveStart);
};
}
connectDragPreview(sourceId, node, options) {
this.sourcePreviewNodeOptions.set(sourceId, options);
this.sourcePreviewNodes.set(sourceId, node);
return ()=>{
this.sourcePreviewNodes.delete(sourceId);
this.sourcePreviewNodeOptions.delete(sourceId);
};
}
connectDropTarget(targetId, node) {
const root = this.options.rootElement;
if (!this.document || !root) {
return ()=>{
/* noop */ };
}
const handleMove = (e)=>{
if (!this.document || !root || !this.monitor.isDragging()) {
return;
}
let coords;
/**
* Grab the coordinates for the current mouse/touch position
*/ switch(e.type){
case eventNames.mouse.move:
coords = {
x: e.clientX,
y: e.clientY
};
break;
case eventNames.touch.move:
var ref, ref1;
coords = {
x: ((ref = e.touches[0]) === null || ref === void 0 ? void 0 : ref.clientX) || 0,
y: ((ref1 = e.touches[0]) === null || ref1 === void 0 ? void 0 : ref1.clientY) || 0
};
break;
}
/**
* Use the coordinates to grab the element the drag ended on.
* 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.
*/ const droppedOn = coords != null ? this.document.elementFromPoint(coords.x, coords.y) : undefined;
const childMatch = droppedOn && node.contains(droppedOn);
if (droppedOn === node || childMatch) {
return this.handleMove(e, targetId);
}
};
/**
* Attaching the event listener to the body so that touchmove will work while dragging over multiple target elements.
*/ this.addEventListener(this.document.body, 'move', handleMove);
this.targetNodes.set(targetId, node);
return ()=>{
if (this.document) {
this.targetNodes.delete(targetId);
this.removeEventListener(this.document.body, 'move', handleMove);
}
};
}
getTopMoveStartHandler() {
if (!this.options.delayTouchStart && !this.options.delayMouseStart) {
return this.handleTopMoveStart;
}
return this.handleTopMoveStartDelay;
}
installSourceNodeRemovalObserver(node) {
this.uninstallSourceNodeRemovalObserver();
this.draggedSourceNode = node;
this.draggedSourceNodeRemovalObserver = new MutationObserver(()=>{
if (node && !node.parentElement) {
this.resurrectSourceNode();
this.uninstallSourceNodeRemovalObserver();
}
});
if (!node || !node.parentElement) {
return;
}
this.draggedSourceNodeRemovalObserver.observe(node.parentElement, {
childList: true
});
}
resurrectSourceNode() {
if (this.document && this.draggedSourceNode) {
this.draggedSourceNode.style.display = 'none';
this.draggedSourceNode.removeAttribute('data-reactid');
this.document.body.appendChild(this.draggedSourceNode);
}
}
uninstallSourceNodeRemovalObserver() {
if (this.draggedSourceNodeRemovalObserver) {
this.draggedSourceNodeRemovalObserver.disconnect();
}
this.draggedSourceNodeRemovalObserver = undefined;
this.draggedSourceNode = undefined;
}
constructor(manager, context, options){
this.getSourceClientOffset = (sourceId)=>{
const element = this.sourceNodes.get(sourceId);
return element && getNodeClientOffset(element);
};
this.handleTopMoveStartCapture = (e)=>{
if (!eventShouldStartDrag(e)) {
return;
}
this.moveStartSourceIds = [];
};
this.handleMoveStart = (sourceId)=>{
// Just because we received an event doesn't necessarily mean we need to collect drag sources.
// We only collect start collecting drag sources on touch and left mouse events.
if (Array.isArray(this.moveStartSourceIds)) {
this.moveStartSourceIds.unshift(sourceId);
}
};
this.handleTopMoveStart = (e)=>{
if (!eventShouldStartDrag(e)) {
return;
}
// Don't prematurely preventDefault() here since it might:
// 1. Mess up scrolling
// 2. Mess up long tap (which brings up context menu)
// 3. If there's an anchor link as a child, tap won't be triggered on link
const clientOffset = getEventClientOffset(e);
if (clientOffset) {
if (isTouchEvent(e)) {
this.lastTargetTouchFallback = e.targetTouches[0];
}
this._mouseClientOffset = clientOffset;
}
this.waitingForDelay = false;
};
this.handleTopMoveStartDelay = (e)=>{
if (!eventShouldStartDrag(e)) {
return;
}
const delay = e.type === eventNames.touch.start ? this.options.delayTouchStart : this.options.delayMouseStart;
this.timeout = setTimeout(this.handleTopMoveStart.bind(this, e), delay);
this.waitingForDelay = true;
};
this.handleTopMoveCapture = ()=>{
this.dragOverTargetIds = [];
};
this.handleMove = (_evt, targetId)=>{
if (this.dragOverTargetIds) {
this.dragOverTargetIds.unshift(targetId);
}
};
this.handleTopMove = (e1)=>{
if (this.timeout) {
clearTimeout(this.timeout);
}
if (!this.document || this.waitingForDelay) {
return;
}
const { moveStartSourceIds , dragOverTargetIds } = this;
const enableHoverOutsideTarget = this.options.enableHoverOutsideTarget;
const clientOffset = getEventClientOffset(e1, this.lastTargetTouchFallback);
if (!clientOffset) {
return;
}
// If the touch move started as a scroll, or is is between the scroll angles
if (this._isScrolling || !this.monitor.isDragging() && inAngleRanges(this._mouseClientOffset.x || 0, this._mouseClientOffset.y || 0, clientOffset.x, clientOffset.y, this.options.scrollAngleRanges)) {
this._isScrolling = true;
return;
}
// If we're not dragging and we've moved a little, that counts as a drag start
if (!this.monitor.isDragging() && // eslint-disable-next-line no-prototype-builtins
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)) {
this.moveStartSourceIds = undefined;
this.actions.beginDrag(moveStartSourceIds, {
clientOffset: this._mouseClientOffset,
getSourceClientOffset: this.getSourceClientOffset,
publishSource: false
});
}
if (!this.monitor.isDragging()) {
return;
}
const sourceNode = this.sourceNodes.get(this.monitor.getSourceId());
this.installSourceNodeRemovalObserver(sourceNode);
this.actions.publishDragSource();
if (e1.cancelable) e1.preventDefault();
// Get the node elements of the hovered DropTargets
const dragOverTargetNodes = (dragOverTargetIds || []).map((key)=>this.targetNodes.get(key)
).filter((e)=>!!e
);
// Get the a ordered list of nodes that are touched by
const elementsAtPoint = this.options.getDropTargetElementsAtPoint ? this.options.getDropTargetElementsAtPoint(clientOffset.x, clientOffset.y, dragOverTargetNodes) : this.document.elementsFromPoint(clientOffset.x, clientOffset.y);
// Extend list with parents that are not receiving elementsFromPoint events (size 0 elements and svg groups)
const elementsAtPointExtended = [];
for(const nodeId in elementsAtPoint){
// eslint-disable-next-line no-prototype-builtins
if (!elementsAtPoint.hasOwnProperty(nodeId)) {
continue;
}
let currentNode = elementsAtPoint[nodeId];
if (currentNode != null) {
elementsAtPointExtended.push(currentNode);
}
while(currentNode){
currentNode = currentNode.parentElement;
if (currentNode && elementsAtPointExtended.indexOf(currentNode) === -1) {
elementsAtPointExtended.push(currentNode);
}
}
}
const orderedDragOverTargetIds = elementsAtPointExtended// Filter off nodes that arent a hovered DropTargets nodes
.filter((node)=>dragOverTargetNodes.indexOf(node) > -1
)// Map back the nodes elements to targetIds
.map((node)=>this._getDropTargetId(node)
)// Filter off possible null rows
.filter((node)=>!!node
).filter((id, index, ids)=>ids.indexOf(id) === index
);
// Invoke hover for drop targets when source node is still over and pointer is outside
if (enableHoverOutsideTarget) {
for(const targetId in this.targetNodes){
const targetNode = this.targetNodes.get(targetId);
if (sourceNode && targetNode && targetNode.contains(sourceNode) && orderedDragOverTargetIds.indexOf(targetId) === -1) {
orderedDragOverTargetIds.unshift(targetId);
break;
}
}
}
// Reverse order because dnd-core reverse it before calling the DropTarget drop methods
orderedDragOverTargetIds.reverse();
this.actions.hover(orderedDragOverTargetIds, {
clientOffset: clientOffset
});
};
/**
*
* visible for testing
*/ this._getDropTargetId = (node)=>{
const keys = this.targetNodes.keys();
let next = keys.next();
while(next.done === false){
const targetId = next.value;
if (node === this.targetNodes.get(targetId)) {
return targetId;
} else {
next = keys.next();
}
}
return undefined;
};
this.handleTopMoveEndCapture = (e)=>{
this._isScrolling = false;
this.lastTargetTouchFallback = undefined;
if (!eventShouldEndDrag(e)) {
return;
}
if (!this.monitor.isDragging() || this.monitor.didDrop()) {
this.moveStartSourceIds = undefined;
return;
}
if (e.cancelable) e.preventDefault();
this._mouseClientOffset = {};
this.uninstallSourceNodeRemovalObserver();
this.actions.drop();
this.actions.endDrag();
};
this.handleCancelOnEscape = (e)=>{
if (e.key === 'Escape' && this.monitor.isDragging()) {
this._mouseClientOffset = {};
this.uninstallSourceNodeRemovalObserver();
this.actions.endDrag();
}
};
this.options = new OptionsReader(options, context);
this.actions = manager.getActions();
this.monitor = manager.getMonitor();
this.sourceNodes = new Map();
this.sourcePreviewNodes = new Map();
this.sourcePreviewNodeOptions = new Map();
this.targetNodes = new Map();
this.listenerTypes = [];
this._mouseClientOffset = {};
this._isScrolling = false;
if (this.options.enableMouseEvents) {
this.listenerTypes.push(ListenerType.mouse);
}
if (this.options.enableTouchEvents) {
this.listenerTypes.push(ListenerType.touch);
}
if (this.options.enableKeyboardEvents) {
this.listenerTypes.push(ListenerType.keyboard);
}
}
}
//# sourceMappingURL=TouchBackendImpl.js.map