UNPKG

react-draggable

Version:
390 lines (345 loc) 12.7 kB
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; import { createCSSTransform, createSVGTransform } from './utils/domFns'; import { canDragX, canDragY, createDraggableData, getBoundPosition } from './utils/positionFns'; import { dontSetMe } from './utils/shims'; import DraggableCore from './DraggableCore'; /*:: import type {ControlPosition, PositionOffsetControlPosition, DraggableBounds, DraggableCoreProps} from './DraggableCore';*/ import log from './utils/log'; /*:: import type {DraggableEventHandler} from './utils/types';*/ /*:: import type {Element as ReactElement} from 'react';*/ /*:: type DraggableState = { dragging: boolean, dragged: boolean, x: number, y: number, slackX: number, slackY: number, isElementSVG: boolean, prevPropsPosition: ?ControlPosition, };*/ /*:: export type DraggableProps = { ...$Exact<DraggableCoreProps>, axis: 'both' | 'x' | 'y' | 'none', bounds: DraggableBounds | string | false, defaultClassName: string, defaultClassNameDragging: string, defaultClassNameDragged: string, defaultPosition: ControlPosition, positionOffset: PositionOffsetControlPosition, position: ControlPosition, scale: number };*/ // // Define <Draggable> // class Draggable extends React.Component { // React 16.3+ // Arity (props, state) static getDerivedStateFromProps({ position } /*: DraggableProps*/ , { prevPropsPosition } /*: DraggableState*/ ) { // Set x/y if a new position is provided in props that is different than the previous. if (position && (!prevPropsPosition || position.x !== prevPropsPosition.x || position.y !== prevPropsPosition.y)) { log('Draggable: getDerivedStateFromProps %j', { position, prevPropsPosition }); return { x: position.x, y: position.y, prevPropsPosition: { ...position } }; } return null; } constructor(props /*: DraggableProps*/ ) { super(props); _defineProperty(this, "onDragStart", (e, coreData) => { log('Draggable: onDragStart: %j', coreData); // Short-circuit if user's callback killed it. const shouldStart = this.props.onStart(e, createDraggableData(this, coreData)); // Kills start event on core as well, so move handlers are never bound. if (shouldStart === false) return false; this.setState({ dragging: true, dragged: true }); }); _defineProperty(this, "onDrag", (e, coreData) => { if (!this.state.dragging) return false; log('Draggable: onDrag: %j', coreData); const uiData = createDraggableData(this, coreData); const newState /*: $Shape<DraggableState>*/ = { x: uiData.x, y: uiData.y }; // Keep within bounds. if (this.props.bounds) { // Save original x and y. const { x, y } = newState; // Add slack to the values used to calculate bound position. This will ensure that if // we start removing slack, the element won't react to it right away until it's been // completely removed. newState.x += this.state.slackX; newState.y += this.state.slackY; // Get bound position. This will ceil/floor the x and y within the boundaries. const [newStateX, newStateY] = getBoundPosition(this, newState.x, newState.y); newState.x = newStateX; newState.y = newStateY; // Recalculate slack by noting how much was shaved by the boundPosition handler. newState.slackX = this.state.slackX + (x - newState.x); newState.slackY = this.state.slackY + (y - newState.y); // Update the event we fire to reflect what really happened after bounds took effect. uiData.x = newState.x; uiData.y = newState.y; uiData.deltaX = newState.x - this.state.x; uiData.deltaY = newState.y - this.state.y; } // Short-circuit if user's callback killed it. const shouldUpdate = this.props.onDrag(e, uiData); if (shouldUpdate === false) return false; this.setState(newState); }); _defineProperty(this, "onDragStop", (e, coreData) => { if (!this.state.dragging) return false; // Short-circuit if user's callback killed it. const shouldStop = this.props.onStop(e, createDraggableData(this, coreData)); if (shouldStop === false) return false; log('Draggable: onDragStop: %j', coreData); const newState /*: $Shape<DraggableState>*/ = { dragging: false, slackX: 0, slackY: 0 }; // If this is a controlled component, the result of this operation will be to // revert back to the old position. We expect a handler on `onDragStop`, at the least. const controlled = Boolean(this.props.position); if (controlled) { const { x, y } = this.props.position; newState.x = x; newState.y = y; } this.setState(newState); }); this.state = { // Whether or not we are currently dragging. dragging: false, // Whether or not we have been dragged before. dragged: false, // Current transform x and y. x: props.position ? props.position.x : props.defaultPosition.x, y: props.position ? props.position.y : props.defaultPosition.y, prevPropsPosition: { ...props.position }, // Used for compensating for out-of-bounds drags slackX: 0, slackY: 0, // Can only determine if SVG after mounting isElementSVG: false }; if (props.position && !(props.onDrag || props.onStop)) { // eslint-disable-next-line no-console console.warn('A `position` was applied to this <Draggable>, without drag handlers. This will make this ' + 'component effectively undraggable. Please attach `onDrag` or `onStop` handlers so you can adjust the ' + '`position` of this element.'); } } componentDidMount() { // Check to see if the element passed is an instanceof SVGElement if (typeof window.SVGElement !== 'undefined' && ReactDOM.findDOMNode(this) instanceof window.SVGElement) { this.setState({ isElementSVG: true }); } } componentWillUnmount() { this.setState({ dragging: false }); // prevents invariant if unmounted while dragging } render() /*: ReactElement<any>*/ { const { axis, bounds, children, defaultPosition, defaultClassName, defaultClassNameDragging, defaultClassNameDragged, position, positionOffset, scale, ...draggableCoreProps } = this.props; let style = {}; let svgTransform = null; // If this is controlled, we don't want to move it - unless it's dragging. const controlled = Boolean(position); const draggable = !controlled || this.state.dragging; const validPosition = position || defaultPosition; const transformOpts = { // Set left if horizontal drag is enabled x: canDragX(this) && draggable ? this.state.x : validPosition.x, // Set top if vertical drag is enabled y: canDragY(this) && draggable ? this.state.y : validPosition.y }; // If this element was SVG, we use the `transform` attribute. if (this.state.isElementSVG) { svgTransform = createSVGTransform(transformOpts, positionOffset); } else { // Add a CSS transform to move the element around. This allows us to move the element around // without worrying about whether or not it is relatively or absolutely positioned. // If the item you are dragging already has a transform set, wrap it in a <span> so <Draggable> // has a clean slate. style = createCSSTransform(transformOpts, positionOffset); } // Mark with class while dragging const className = classNames(children.props.className || '', defaultClassName, { [defaultClassNameDragging]: this.state.dragging, [defaultClassNameDragged]: this.state.dragged }); // Reuse the child provided // This makes it flexible to use whatever element is wanted (div, ul, etc) return React.createElement(DraggableCore, _extends({}, draggableCoreProps, { onStart: this.onDragStart, onDrag: this.onDrag, onStop: this.onDragStop }), React.cloneElement(React.Children.only(children), { className: className, style: { ...children.props.style, ...style }, transform: svgTransform })); } } _defineProperty(Draggable, "displayName", 'Draggable'); _defineProperty(Draggable, "propTypes", { // Accepts all props <DraggableCore> accepts. ...DraggableCore.propTypes, /** * `axis` determines which axis the draggable can move. * * Note that all callbacks will still return data as normal. This only * controls flushing to the DOM. * * 'both' allows movement horizontally and vertically. * 'x' limits movement to horizontal axis. * 'y' limits movement to vertical axis. * 'none' limits all movement. * * Defaults to 'both'. */ axis: PropTypes.oneOf(['both', 'x', 'y', 'none']), /** * `bounds` determines the range of movement available to the element. * Available values are: * * 'parent' restricts movement within the Draggable's parent node. * * Alternatively, pass an object with the following properties, all of which are optional: * * {left: LEFT_BOUND, right: RIGHT_BOUND, bottom: BOTTOM_BOUND, top: TOP_BOUND} * * All values are in px. * * Example: * * ```jsx * let App = React.createClass({ * render: function () { * return ( * <Draggable bounds={{right: 300, bottom: 300}}> * <div>Content</div> * </Draggable> * ); * } * }); * ``` */ bounds: PropTypes.oneOfType([PropTypes.shape({ left: PropTypes.number, right: PropTypes.number, top: PropTypes.number, bottom: PropTypes.number }), PropTypes.string, PropTypes.oneOf([false])]), defaultClassName: PropTypes.string, defaultClassNameDragging: PropTypes.string, defaultClassNameDragged: PropTypes.string, /** * `defaultPosition` specifies the x and y that the dragged item should start at * * Example: * * ```jsx * let App = React.createClass({ * render: function () { * return ( * <Draggable defaultPosition={{x: 25, y: 25}}> * <div>I start with transformX: 25px and transformY: 25px;</div> * </Draggable> * ); * } * }); * ``` */ defaultPosition: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), positionOffset: PropTypes.shape({ x: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), y: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) }), /** * `position`, if present, defines the current position of the element. * * This is similar to how form elements in React work - if no `position` is supplied, the component * is uncontrolled. * * Example: * * ```jsx * let App = React.createClass({ * render: function () { * return ( * <Draggable position={{x: 25, y: 25}}> * <div>I start with transformX: 25px and transformY: 25px;</div> * </Draggable> * ); * } * }); * ``` */ position: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), /** * These properties should be defined on the child, not here. */ className: dontSetMe, style: dontSetMe, transform: dontSetMe }); _defineProperty(Draggable, "defaultProps", { ...DraggableCore.defaultProps, axis: 'both', bounds: false, defaultClassName: 'react-draggable', defaultClassNameDragging: 'react-draggable-dragging', defaultClassNameDragged: 'react-draggable-dragged', defaultPosition: { x: 0, y: 0 }, position: null, scale: 1 }); export { Draggable as default, DraggableCore };