v-react-pivottable/node_modules/react-draggable/lib/Draggable.js
2024-12-16 20:19:32 -06:00

368 lines
12 KiB
JavaScript

// @flow
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
};
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>
//
export default class Draggable extends React.Component<DraggableProps, DraggableState> {
static displayName = 'Draggable';
static 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
};
static 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
};
constructor(props: DraggableProps) {
super(props);
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,
// 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 });
}
}
componentWillReceiveProps(nextProps: Object) {
// Set x/y if position has changed
if (nextProps.position &&
(!this.props.position ||
nextProps.position.x !== this.props.position.x ||
nextProps.position.y !== this.props.position.y
)
) {
this.setState({ x: nextProps.position.x, y: nextProps.position.y });
}
}
componentWillUnmount() {
this.setState({dragging: false}); // prevents invariant if unmounted while dragging
}
onDragStart: DraggableEventHandler = (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});
};
onDrag: DraggableEventHandler = (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);
};
onDragStop: DraggableEventHandler = (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);
};
render(): ReactElement<any> {
let style = {}, svgTransform = null;
// If this is controlled, we don't want to move it - unless it's dragging.
const controlled = Boolean(this.props.position);
const draggable = !controlled || this.state.dragging;
const position = this.props.position || this.props.defaultPosition;
const transformOpts = {
// Set left if horizontal drag is enabled
x: canDragX(this) && draggable ?
this.state.x :
position.x,
// Set top if vertical drag is enabled
y: canDragY(this) && draggable ?
this.state.y :
position.y
};
// If this element was SVG, we use the `transform` attribute.
if (this.state.isElementSVG) {
svgTransform = createSVGTransform(transformOpts, this.props.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, this.props.positionOffset);
}
const {
defaultClassName,
defaultClassNameDragging,
defaultClassNameDragged
} = this.props;
const children = React.Children.only(this.props.children);
// 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 (
<DraggableCore {...this.props} onStart={this.onDragStart} onDrag={this.onDrag} onStop={this.onDragStop}>
{React.cloneElement(children, {
className: className,
style: {...children.props.style, ...style},
transform: svgTransform
})}
</DraggableCore>
);
}
}