react-draggable
Version:
React draggable component
193 lines (170 loc) • 5.14 kB
JavaScript
import { isNum, int } from './shims';
import ReactDOM from 'react-dom';
import { getTouch, innerWidth, innerHeight, offsetXYFromParent, outerWidth, outerHeight } from './domFns';
/*:: import type Draggable from '../Draggable';*/
/*:: import type {Bounds, ControlPosition, DraggableData, MouseTouchEvent} from './types';*/
/*:: import type DraggableCore from '../DraggableCore';*/
export function getBoundPosition(draggable
/*: Draggable*/
, x
/*: number*/
, y
/*: number*/
)
/*: [number, number]*/
{
// If no bounds, short-circuit and move on
if (!draggable.props.bounds) return [x, y]; // Clone new bounds
let {
bounds
} = draggable.props;
bounds = typeof bounds === 'string' ? bounds : cloneBounds(bounds);
const node = findDOMNode(draggable);
if (typeof bounds === 'string') {
const {
ownerDocument
} = node;
const ownerWindow = ownerDocument.defaultView;
let boundNode;
if (bounds === 'parent') {
boundNode = node.parentNode;
} else {
boundNode = ownerDocument.querySelector(bounds);
}
if (!(boundNode instanceof ownerWindow.HTMLElement)) {
throw new Error('Bounds selector "' + bounds + '" could not find an element.');
}
const nodeStyle = ownerWindow.getComputedStyle(node);
const boundNodeStyle = ownerWindow.getComputedStyle(boundNode); // Compute bounds. This is a pain with padding and offsets but this gets it exactly right.
bounds = {
left: -node.offsetLeft + int(boundNodeStyle.paddingLeft) + int(nodeStyle.marginLeft),
top: -node.offsetTop + int(boundNodeStyle.paddingTop) + int(nodeStyle.marginTop),
right: innerWidth(boundNode) - outerWidth(node) - node.offsetLeft + int(boundNodeStyle.paddingRight) - int(nodeStyle.marginRight),
bottom: innerHeight(boundNode) - outerHeight(node) - node.offsetTop + int(boundNodeStyle.paddingBottom) - int(nodeStyle.marginBottom)
};
} // Keep x and y below right and bottom limits...
if (isNum(bounds.right)) x = Math.min(x, bounds.right);
if (isNum(bounds.bottom)) y = Math.min(y, bounds.bottom); // But above left and top limits.
if (isNum(bounds.left)) x = Math.max(x, bounds.left);
if (isNum(bounds.top)) y = Math.max(y, bounds.top);
return [x, y];
}
export function snapToGrid(grid
/*: [number, number]*/
, pendingX
/*: number*/
, pendingY
/*: number*/
)
/*: [number, number]*/
{
const x = Math.round(pendingX / grid[0]) * grid[0];
const y = Math.round(pendingY / grid[1]) * grid[1];
return [x, y];
}
export function canDragX(draggable
/*: Draggable*/
)
/*: boolean*/
{
return draggable.props.axis === 'both' || draggable.props.axis === 'x';
}
export function canDragY(draggable
/*: Draggable*/
)
/*: boolean*/
{
return draggable.props.axis === 'both' || draggable.props.axis === 'y';
} // Get {x, y} positions from event.
export function getControlPosition(e
/*: MouseTouchEvent*/
, touchIdentifier
/*: ?number*/
, draggableCore
/*: DraggableCore*/
)
/*: ?ControlPosition*/
{
const touchObj = typeof touchIdentifier === 'number' ? getTouch(e, touchIdentifier) : null;
if (typeof touchIdentifier === 'number' && !touchObj) return null; // not the right touch
const node = findDOMNode(draggableCore); // User can provide an offsetParent if desired.
const offsetParent = draggableCore.props.offsetParent || node.offsetParent || node.ownerDocument.body;
return offsetXYFromParent(touchObj || e, offsetParent);
} // Create an data object exposed by <DraggableCore>'s events
export function createCoreData(draggable
/*: DraggableCore*/
, x
/*: number*/
, y
/*: number*/
)
/*: DraggableData*/
{
const state = draggable.state;
const isStart = !isNum(state.lastX);
const node = findDOMNode(draggable);
if (isStart) {
// If this is our first move, use the x and y as last coords.
return {
node,
deltaX: 0,
deltaY: 0,
lastX: x,
lastY: y,
x,
y
};
} else {
// Otherwise calculate proper values.
return {
node,
deltaX: x - state.lastX,
deltaY: y - state.lastY,
lastX: state.lastX,
lastY: state.lastY,
x,
y
};
}
} // Create an data exposed by <Draggable>'s events
export function createDraggableData(draggable
/*: Draggable*/
, coreData
/*: DraggableData*/
)
/*: DraggableData*/
{
const scale = draggable.props.scale;
return {
node: coreData.node,
x: draggable.state.x + coreData.deltaX / scale,
y: draggable.state.y + coreData.deltaY / scale,
deltaX: coreData.deltaX / scale,
deltaY: coreData.deltaY / scale,
lastX: draggable.state.x,
lastY: draggable.state.y
};
} // A lot faster than stringify/parse
function cloneBounds(bounds
/*: Bounds*/
)
/*: Bounds*/
{
return {
left: bounds.left,
top: bounds.top,
right: bounds.right,
bottom: bounds.bottom
};
}
function findDOMNode(draggable
/*: Draggable | DraggableCore*/
)
/*: HTMLElement*/
{
const node = ReactDOM.findDOMNode(draggable);
if (!node) {
throw new Error('<DraggableCore>: Unmounted during event!');
} // $FlowIgnore we can't assert on HTMLElement due to tests... FIXME
return node;
}