2024-12-16 20:19:32 -06:00
|
|
|
// @flow
|
|
|
|
import React from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
|
|
|
import ReactDOM from 'react-dom';
|
|
|
|
import {matchesSelectorAndParentsTo, addEvent, removeEvent, addUserSelectStyles, getTouchIdentifier,
|
|
|
|
removeUserSelectStyles, styleHacks} from './utils/domFns';
|
|
|
|
import {createCoreData, getControlPosition, snapToGrid} from './utils/positionFns';
|
|
|
|
import {dontSetMe} from './utils/shims';
|
|
|
|
import log from './utils/log';
|
|
|
|
|
|
|
|
import type {EventHandler, MouseTouchEvent} from './utils/types';
|
|
|
|
import type {Element as ReactElement} from 'react';
|
|
|
|
|
|
|
|
// Simple abstraction for dragging events names.
|
|
|
|
const eventsFor = {
|
|
|
|
touch: {
|
|
|
|
start: 'touchstart',
|
|
|
|
move: 'touchmove',
|
|
|
|
stop: 'touchend'
|
|
|
|
},
|
|
|
|
mouse: {
|
|
|
|
start: 'mousedown',
|
|
|
|
move: 'mousemove',
|
|
|
|
stop: 'mouseup'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Default to mouse events.
|
|
|
|
let dragEventFor = eventsFor.mouse;
|
|
|
|
|
|
|
|
type DraggableCoreState = {
|
|
|
|
dragging: boolean,
|
|
|
|
lastX: number,
|
|
|
|
lastY: number,
|
|
|
|
touchIdentifier: ?number
|
|
|
|
};
|
|
|
|
|
|
|
|
export type DraggableBounds = {
|
|
|
|
left: number,
|
|
|
|
right: number,
|
|
|
|
top: number,
|
|
|
|
bottom: number,
|
|
|
|
};
|
|
|
|
|
|
|
|
export type DraggableData = {
|
|
|
|
node: HTMLElement,
|
|
|
|
x: number, y: number,
|
|
|
|
deltaX: number, deltaY: number,
|
|
|
|
lastX: number, lastY: number,
|
|
|
|
};
|
|
|
|
|
|
|
|
export type DraggableEventHandler = (e: MouseEvent, data: DraggableData) => void;
|
|
|
|
|
|
|
|
export type ControlPosition = {x: number, y: number};
|
|
|
|
export type PositionOffsetControlPosition = {x: number|string, y: number|string};
|
|
|
|
|
|
|
|
export type DraggableCoreProps = {
|
|
|
|
allowAnyClick: boolean,
|
|
|
|
cancel: string,
|
|
|
|
children: ReactElement<any>,
|
|
|
|
disabled: boolean,
|
|
|
|
enableUserSelectHack: boolean,
|
|
|
|
offsetParent: HTMLElement,
|
|
|
|
grid: [number, number],
|
|
|
|
handle: string,
|
|
|
|
onStart: DraggableEventHandler,
|
|
|
|
onDrag: DraggableEventHandler,
|
|
|
|
onStop: DraggableEventHandler,
|
|
|
|
onMouseDown: (e: MouseEvent) => void,
|
|
|
|
};
|
|
|
|
|
|
|
|
//
|
|
|
|
// Define <DraggableCore>.
|
|
|
|
//
|
|
|
|
// <DraggableCore> is for advanced usage of <Draggable>. It maintains minimal internal state so it can
|
|
|
|
// work well with libraries that require more control over the element.
|
|
|
|
//
|
|
|
|
|
|
|
|
export default class DraggableCore extends React.Component<DraggableCoreProps, DraggableCoreState> {
|
|
|
|
|
|
|
|
static displayName = 'DraggableCore';
|
|
|
|
|
|
|
|
static propTypes = {
|
|
|
|
/**
|
|
|
|
* `allowAnyClick` allows dragging using any mouse button.
|
|
|
|
* By default, we only accept the left button.
|
|
|
|
*
|
|
|
|
* Defaults to `false`.
|
|
|
|
*/
|
|
|
|
allowAnyClick: PropTypes.bool,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* `disabled`, if true, stops the <Draggable> from dragging. All handlers,
|
|
|
|
* with the exception of `onMouseDown`, will not fire.
|
|
|
|
*/
|
|
|
|
disabled: PropTypes.bool,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* By default, we add 'user-select:none' attributes to the document body
|
|
|
|
* to prevent ugly text selection during drag. If this is causing problems
|
|
|
|
* for your app, set this to `false`.
|
|
|
|
*/
|
|
|
|
enableUserSelectHack: PropTypes.bool,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* `offsetParent`, if set, uses the passed DOM node to compute drag offsets
|
|
|
|
* instead of using the parent node.
|
|
|
|
*/
|
|
|
|
offsetParent: function(props: DraggableCoreProps, propName: $Keys<DraggableCoreProps>) {
|
|
|
|
if (props[propName] && props[propName].nodeType !== 1) {
|
|
|
|
throw new Error('Draggable\'s offsetParent must be a DOM Node.');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* `grid` specifies the x and y that dragging should snap to.
|
|
|
|
*/
|
|
|
|
grid: PropTypes.arrayOf(PropTypes.number),
|
|
|
|
|
|
|
|
/**
|
|
|
|
* `scale` specifies the scale of the area you are dragging inside of. It allows
|
|
|
|
* the drag deltas to scale correctly with how far zoomed in/out you are.
|
|
|
|
*/
|
|
|
|
scale: PropTypes.number,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* `handle` specifies a selector to be used as the handle that initiates drag.
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
*
|
|
|
|
* ```jsx
|
|
|
|
* let App = React.createClass({
|
|
|
|
* render: function () {
|
|
|
|
* return (
|
|
|
|
* <Draggable handle=".handle">
|
|
|
|
* <div>
|
|
|
|
* <div className="handle">Click me to drag</div>
|
|
|
|
* <div>This is some other content</div>
|
|
|
|
* </div>
|
|
|
|
* </Draggable>
|
|
|
|
* );
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*/
|
|
|
|
handle: PropTypes.string,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* `cancel` specifies a selector to be used to prevent drag initialization.
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
*
|
|
|
|
* ```jsx
|
|
|
|
* let App = React.createClass({
|
|
|
|
* render: function () {
|
|
|
|
* return(
|
|
|
|
* <Draggable cancel=".cancel">
|
|
|
|
* <div>
|
|
|
|
* <div className="cancel">You can't drag from here</div>
|
|
|
|
* <div>Dragging here works fine</div>
|
|
|
|
* </div>
|
|
|
|
* </Draggable>
|
|
|
|
* );
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*/
|
|
|
|
cancel: PropTypes.string,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when dragging starts.
|
|
|
|
* If this function returns the boolean false, dragging will be canceled.
|
|
|
|
*/
|
|
|
|
onStart: PropTypes.func,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called while dragging.
|
|
|
|
* If this function returns the boolean false, dragging will be canceled.
|
|
|
|
*/
|
|
|
|
onDrag: PropTypes.func,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when dragging stops.
|
|
|
|
* If this function returns the boolean false, the drag will remain active.
|
|
|
|
*/
|
|
|
|
onStop: PropTypes.func,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A workaround option which can be passed if onMouseDown needs to be accessed,
|
|
|
|
* since it'll always be blocked (as there is internal use of onMouseDown)
|
|
|
|
*/
|
|
|
|
onMouseDown: PropTypes.func,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* These properties should be defined on the child, not here.
|
|
|
|
*/
|
|
|
|
className: dontSetMe,
|
|
|
|
style: dontSetMe,
|
|
|
|
transform: dontSetMe
|
|
|
|
};
|
|
|
|
|
|
|
|
static defaultProps = {
|
|
|
|
allowAnyClick: false, // by default only accept left click
|
|
|
|
cancel: null,
|
|
|
|
disabled: false,
|
|
|
|
enableUserSelectHack: true,
|
|
|
|
offsetParent: null,
|
|
|
|
handle: null,
|
|
|
|
grid: null,
|
|
|
|
transform: null,
|
|
|
|
onStart: function(){},
|
|
|
|
onDrag: function(){},
|
|
|
|
onStop: function(){},
|
|
|
|
onMouseDown: function(){}
|
|
|
|
};
|
|
|
|
|
|
|
|
state = {
|
|
|
|
dragging: false,
|
|
|
|
// Used while dragging to determine deltas.
|
|
|
|
lastX: NaN, lastY: NaN,
|
|
|
|
touchIdentifier: null
|
|
|
|
};
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
// Remove any leftover event handlers. Remove both touch and mouse handlers in case
|
|
|
|
// some browser quirk caused a touch event to fire during a mouse move, or vice versa.
|
|
|
|
const thisNode = ReactDOM.findDOMNode(this);
|
|
|
|
if (thisNode) {
|
|
|
|
const {ownerDocument} = thisNode;
|
|
|
|
removeEvent(ownerDocument, eventsFor.mouse.move, this.handleDrag);
|
|
|
|
removeEvent(ownerDocument, eventsFor.touch.move, this.handleDrag);
|
|
|
|
removeEvent(ownerDocument, eventsFor.mouse.stop, this.handleDragStop);
|
|
|
|
removeEvent(ownerDocument, eventsFor.touch.stop, this.handleDragStop);
|
|
|
|
if (this.props.enableUserSelectHack) removeUserSelectStyles(ownerDocument);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleDragStart: EventHandler<MouseTouchEvent> = (e) => {
|
2024-12-17 16:02:39 -06:00
|
|
|
e.stopPropagation();
|
2024-12-16 20:19:32 -06:00
|
|
|
// Make it possible to attach event handlers on top of this one.
|
|
|
|
this.props.onMouseDown(e);
|
|
|
|
|
|
|
|
// Only accept left-clicks.
|
|
|
|
if (!this.props.allowAnyClick && typeof e.button === 'number' && e.button !== 0) return false;
|
|
|
|
|
|
|
|
// Get nodes. Be sure to grab relative document (could be iframed)
|
|
|
|
const thisNode = ReactDOM.findDOMNode(this);
|
|
|
|
if (!thisNode || !thisNode.ownerDocument || !thisNode.ownerDocument.body) {
|
|
|
|
throw new Error('<DraggableCore> not mounted on DragStart!');
|
|
|
|
}
|
|
|
|
const {ownerDocument} = thisNode;
|
|
|
|
|
|
|
|
// Short circuit if handle or cancel prop was provided and selector doesn't match.
|
|
|
|
if (this.props.disabled ||
|
|
|
|
(!(e.target instanceof ownerDocument.defaultView.Node)) ||
|
|
|
|
(this.props.handle && !matchesSelectorAndParentsTo(e.target, this.props.handle, thisNode)) ||
|
|
|
|
(this.props.cancel && matchesSelectorAndParentsTo(e.target, this.props.cancel, thisNode))) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set touch identifier in component state if this is a touch event. This allows us to
|
|
|
|
// distinguish between individual touches on multitouch screens by identifying which
|
|
|
|
// touchpoint was set to this element.
|
|
|
|
const touchIdentifier = getTouchIdentifier(e);
|
|
|
|
this.setState({touchIdentifier});
|
|
|
|
|
|
|
|
// Get the current drag point from the event. This is used as the offset.
|
|
|
|
const position = getControlPosition(e, touchIdentifier, this);
|
|
|
|
if (position == null) return; // not possible but satisfies flow
|
|
|
|
const {x, y} = position;
|
|
|
|
|
|
|
|
// Create an event object with all the data parents need to make a decision here.
|
|
|
|
const coreEvent = createCoreData(this, x, y);
|
|
|
|
|
|
|
|
log('DraggableCore: handleDragStart: %j', coreEvent);
|
|
|
|
|
|
|
|
// Call event handler. If it returns explicit false, cancel.
|
|
|
|
log('calling', this.props.onStart);
|
|
|
|
const shouldUpdate = this.props.onStart(e, coreEvent);
|
|
|
|
if (shouldUpdate === false) return;
|
|
|
|
|
|
|
|
// Add a style to the body to disable user-select. This prevents text from
|
|
|
|
// being selected all over the page.
|
|
|
|
if (this.props.enableUserSelectHack) addUserSelectStyles(ownerDocument);
|
|
|
|
|
|
|
|
// Initiate dragging. Set the current x and y as offsets
|
|
|
|
// so we know how much we've moved during the drag. This allows us
|
|
|
|
// to drag elements around even if they have been moved, without issue.
|
|
|
|
this.setState({
|
|
|
|
dragging: true,
|
|
|
|
|
|
|
|
lastX: x,
|
|
|
|
lastY: y
|
|
|
|
});
|
|
|
|
|
|
|
|
// Add events to the document directly so we catch when the user's mouse/touch moves outside of
|
|
|
|
// this element. We use different events depending on whether or not we have detected that this
|
|
|
|
// is a touch-capable device.
|
|
|
|
addEvent(ownerDocument, dragEventFor.move, this.handleDrag);
|
|
|
|
addEvent(ownerDocument, dragEventFor.stop, this.handleDragStop);
|
|
|
|
};
|
|
|
|
|
|
|
|
handleDrag: EventHandler<MouseTouchEvent> = (e) => {
|
2024-12-17 16:02:39 -06:00
|
|
|
e.stopPropagation();
|
2024-12-16 20:19:32 -06:00
|
|
|
|
|
|
|
// Prevent scrolling on mobile devices, like ipad/iphone.
|
|
|
|
if (e.type === 'touchmove') e.preventDefault();
|
|
|
|
|
|
|
|
// Get the current drag point from the event. This is used as the offset.
|
|
|
|
const position = getControlPosition(e, this.state.touchIdentifier, this);
|
|
|
|
if (position == null) return;
|
|
|
|
let {x, y} = position;
|
|
|
|
|
|
|
|
// Snap to grid if prop has been provided
|
|
|
|
if (Array.isArray(this.props.grid)) {
|
|
|
|
let deltaX = x - this.state.lastX, deltaY = y - this.state.lastY;
|
|
|
|
[deltaX, deltaY] = snapToGrid(this.props.grid, deltaX, deltaY);
|
|
|
|
if (!deltaX && !deltaY) return; // skip useless drag
|
|
|
|
x = this.state.lastX + deltaX, y = this.state.lastY + deltaY;
|
|
|
|
}
|
|
|
|
|
|
|
|
const coreEvent = createCoreData(this, x, y);
|
|
|
|
|
|
|
|
log('DraggableCore: handleDrag: %j', coreEvent);
|
|
|
|
|
|
|
|
// Call event handler. If it returns explicit false, trigger end.
|
|
|
|
const shouldUpdate = this.props.onDrag(e, coreEvent);
|
|
|
|
if (shouldUpdate === false) {
|
|
|
|
try {
|
|
|
|
// $FlowIgnore
|
|
|
|
this.handleDragStop(new MouseEvent('mouseup'));
|
|
|
|
} catch (err) {
|
|
|
|
// Old browsers
|
|
|
|
const event = ((document.createEvent('MouseEvents'): any): MouseTouchEvent);
|
|
|
|
// I see why this insanity was deprecated
|
|
|
|
// $FlowIgnore
|
|
|
|
event.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
|
|
|
this.handleDragStop(event);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
lastX: x,
|
|
|
|
lastY: y
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
handleDragStop: EventHandler<MouseTouchEvent> = (e) => {
|
2024-12-17 16:02:39 -06:00
|
|
|
e.stopPropagation();
|
2024-12-16 20:19:32 -06:00
|
|
|
if (!this.state.dragging) return;
|
|
|
|
|
|
|
|
const position = getControlPosition(e, this.state.touchIdentifier, this);
|
|
|
|
if (position == null) return;
|
|
|
|
const {x, y} = position;
|
|
|
|
const coreEvent = createCoreData(this, x, y);
|
|
|
|
|
|
|
|
const thisNode = ReactDOM.findDOMNode(this);
|
|
|
|
if (thisNode) {
|
|
|
|
// Remove user-select hack
|
|
|
|
if (this.props.enableUserSelectHack) removeUserSelectStyles(thisNode.ownerDocument);
|
|
|
|
}
|
|
|
|
|
|
|
|
log('DraggableCore: handleDragStop: %j', coreEvent);
|
|
|
|
|
|
|
|
// Reset the el.
|
|
|
|
this.setState({
|
|
|
|
dragging: false,
|
|
|
|
lastX: NaN,
|
|
|
|
lastY: NaN
|
|
|
|
});
|
|
|
|
|
|
|
|
// Call event handler
|
|
|
|
this.props.onStop(e, coreEvent);
|
|
|
|
|
|
|
|
if (thisNode) {
|
|
|
|
// Remove event handlers
|
|
|
|
log('DraggableCore: Removing handlers');
|
|
|
|
removeEvent(thisNode.ownerDocument, dragEventFor.move, this.handleDrag);
|
|
|
|
removeEvent(thisNode.ownerDocument, dragEventFor.stop, this.handleDragStop);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
onMouseDown: EventHandler<MouseTouchEvent> = (e) => {
|
|
|
|
dragEventFor = eventsFor.mouse; // on touchscreen laptops we could switch back to mouse
|
|
|
|
|
|
|
|
return this.handleDragStart(e);
|
|
|
|
};
|
|
|
|
|
|
|
|
onMouseUp: EventHandler<MouseTouchEvent> = (e) => {
|
|
|
|
dragEventFor = eventsFor.mouse;
|
|
|
|
|
|
|
|
return this.handleDragStop(e);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Same as onMouseDown (start drag), but now consider this a touch device.
|
|
|
|
onTouchStart: EventHandler<MouseTouchEvent> = (e) => {
|
|
|
|
// We're on a touch device now, so change the event handlers
|
|
|
|
dragEventFor = eventsFor.touch;
|
|
|
|
|
|
|
|
return this.handleDragStart(e);
|
|
|
|
};
|
|
|
|
|
|
|
|
onTouchEnd: EventHandler<MouseTouchEvent> = (e) => {
|
|
|
|
// We're on a touch device now, so change the event handlers
|
|
|
|
dragEventFor = eventsFor.touch;
|
|
|
|
|
|
|
|
return this.handleDragStop(e);
|
|
|
|
};
|
|
|
|
|
|
|
|
render() {
|
|
|
|
// Reuse the child provided
|
|
|
|
// This makes it flexible to use whatever element is wanted (div, ul, etc)
|
|
|
|
return React.cloneElement(React.Children.only(this.props.children), {
|
|
|
|
style: styleHacks(this.props.children.props.style),
|
|
|
|
|
|
|
|
// Note: mouseMove handler is attached to document so it will still function
|
|
|
|
// when the user drags quickly and leaves the bounds of the element.
|
|
|
|
onMouseDown: this.onMouseDown,
|
|
|
|
onTouchStart: this.onTouchStart,
|
|
|
|
onMouseUp: this.onMouseUp,
|
|
|
|
onTouchEnd: this.onTouchEnd
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|