diff --git a/README.md b/README.md index 80e22ef2..67348475 100755 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ yarn add react-rnd ## Props -#### `default: { x: number; y: number; width?: number | string; height?: number | string; };` +#### `default: { x: number | string; y: number | string; width?: number | string; height?: number | string; };` The `width` and `height` property is used to set the default size of the component. For example, you can set `300`, `'300px'`, `50%`. @@ -121,10 +121,14 @@ For example, you can set 300, '300px', 50%. Use `size` if you need to control size state by yourself. -#### `position?: { x: number, y: number };` +#### `position?: { x: (number | string), y: (number | string) };` The `position` property is used to set position of the component. -Use `position` if you need to control size state by yourself. +Use `position` if you need to control position state by yourself. +You can pass: +- numbers (treated as pixels), e.g. `x: 100` +- strings with `'px'`, e.g. `'300px'` +- strings with `'%'`, e.g. `'50%'` (percentage of the parent size) see, following example. @@ -464,7 +468,7 @@ class YourComponent extends Component { } ``` -#### `updatePosition({ x: number, y: number }): void` +#### `updatePosition({ x: number | string, y: number | string }): void` Update component position. `grid` `bounds` props is ignored, when this method called. diff --git a/src/index.js.flow b/src/index.js.flow index 8b930c5e..e27bc85a 100755 --- a/src/index.js.flow +++ b/src/index.js.flow @@ -104,6 +104,8 @@ export type HandleComponent = { topLeft?: React.ReactElement; } +export type PositionUnit = 'px' | '%'; + export type Props = { dragGrid?: Grid, default?: { @@ -114,6 +116,7 @@ export type Props = { x: number, y: number, }, + positionUnit?: PositionUnit, size?: Size, resizeGrid?: Grid, bounds?: string, diff --git a/src/index.tsx b/src/index.tsx index 9920e199..e6bce079 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -60,6 +60,7 @@ type State = { }; maxWidth?: number | string; maxHeight?: number | string; + parentSize: { width: number; height: number } | null; }; type MaxSize = { @@ -116,12 +117,12 @@ export type HandleComponent = { export interface Props { dragGrid?: Grid; default?: { - x: number; - y: number; + x: number | string; + y: number | string; } & Size; position?: { - x: number; - y: number; + x: number | string; + y: number | string; }; size?: Size; resizeGrid?: Grid; @@ -181,6 +182,30 @@ const getEnableResizingByFlag = (flag: boolean): Enable => ({ topRight: flag, }); +function parsePositionValue( + value: number | string, + axisSize: number | null, +): number { + if (typeof value === "number") { + return value; + } + const str = value.toString().trim(); + if (str.endsWith("%")) { + const n = Number(str.slice(0, -1)); + if (Number.isNaN(n) || axisSize == null) return 0; + return (n / 100) * axisSize; + } + if (str.endsWith("px")) { + const n = Number(str.slice(0, -2)); + return Number.isNaN(n) ? 0 : n; + } + const n = Number(str); + if (!Number.isNaN(n)) { + return n; + } + return 0; +} + interface DefaultProps { maxWidth: number; maxHeight: number; @@ -224,6 +249,7 @@ export class Rnd extends React.PureComponent { }, maxWidth: props.maxWidth, maxHeight: props.maxHeight, + parentSize: null, }; this.onResizeStart = this.onResizeStart.bind(this); @@ -236,17 +262,55 @@ export class Rnd extends React.PureComponent { } componentDidMount() { + this.updateParentSize(); this.updateOffsetFromParent(); const { left, top } = this.offsetFromParent; - const { x, y } = this.getDraggablePosition(); - this.draggable.setState({ - x: x - left, - y: y - top, - }); + const defaultValue = this.props.default; + let parentSize: { width: number; height: number } | null = null; + if (this.resizable) { + try { + parentSize = this.getParentSize(); + } catch { + // refs may not be ready + } + } + + if (defaultValue && parentSize) { + const x = parsePositionValue(defaultValue.x, parentSize.width); + const y = parsePositionValue(defaultValue.y, parentSize.height); + this.draggable.setState({ + x: x - left, + y: y - top, + }); + } else { + const { x, y } = this.getDraggablePosition(); + this.draggable.setState({ + x: x - left, + y: y - top, + }); + } // HACK: Apply position adjustment this.forceUpdate(); } + componentDidUpdate() { + this.updateParentSize(); + } + + updateParentSize() { + const parent = this.getParent(); + if (!parent || !this.resizable) return; + try { + const { width, height } = this.getParentSize(); + const prev = this.state.parentSize; + if (!prev || prev.width !== width || prev.height !== height) { + this.setState({ parentSize: { width, height } }); + } + } catch { + // getParentSize may throw before refs are ready + } + } + // HACK: To get `react-draggable` state x and y. getDraggablePosition(): { x: number; y: number } { const { x, y } = (this.draggable as any).state; @@ -359,24 +423,42 @@ export class Rnd extends React.PureComponent { onDrag(e: RndDragEvent, data: DraggableData) { if (!this.props.onDrag) return; const { left, top } = this.offsetFromParent; + let pos: Position; + if (!this.props.dragAxis || this.props.dragAxis === "both") { + pos = { x: data.x + left, y: data.y + top }; + } else if (this.props.dragAxis === "x") { + pos = { x: data.x + left, y: this.originalPosition.y + top }; + } else { + pos = { x: this.originalPosition.x + left, y: data.y + top }; + } + const position = pos; if (!this.props.dragAxis || this.props.dragAxis === "both") { - return this.props.onDrag(e, { ...data, x: data.x + left, y: data.y + top }); + return this.props.onDrag(e, { ...data, ...position }); } else if (this.props.dragAxis === "x") { - return this.props.onDrag(e, { ...data, x: data.x + left, y: this.originalPosition.y + top, deltaY: 0 }); + return this.props.onDrag(e, { ...data, ...position, deltaY: 0 }); } else if (this.props.dragAxis === "y") { - return this.props.onDrag(e, { ...data, x: this.originalPosition.x + left, y: data.y + top, deltaX: 0 }); + return this.props.onDrag(e, { ...data, ...position, deltaX: 0 }); } } onDragStop(e: RndDragEvent, data: DraggableData) { if (!this.props.onDragStop) return; const { left, top } = this.offsetFromParent; + let pos: Position; if (!this.props.dragAxis || this.props.dragAxis === "both") { - return this.props.onDragStop(e, { ...data, x: data.x + left, y: data.y + top }); + pos = { x: data.x + left, y: data.y + top }; } else if (this.props.dragAxis === "x") { - return this.props.onDragStop(e, { ...data, x: data.x + left, y: this.originalPosition.y + top, deltaY: 0 }); + pos = { x: data.x + left, y: this.originalPosition.y + top }; + } else { + pos = { x: this.originalPosition.x + left, y: data.y + top }; + } + const position = pos; + if (!this.props.dragAxis || this.props.dragAxis === "both") { + return this.props.onDragStop(e, { ...data, ...position }); + } else if (this.props.dragAxis === "x") { + return this.props.onDragStop(e, { ...data, ...position, deltaY: 0 }); } else if (this.props.dragAxis === "y") { - return this.props.onDragStop(e, { ...data, x: this.originalPosition.x + left, y: data.y + top, deltaX: 0 }); + return this.props.onDragStop(e, { ...data, ...position, deltaX: 0 }); } } @@ -518,10 +600,8 @@ export class Rnd extends React.PureComponent { this.resizingPosition = { x, y }; if (!this.props.onResize) return; - this.props.onResize(e, direction, elementRef, delta, { - x, - y, - }); + const position = { x, y }; + this.props.onResize(e, direction, elementRef, delta, position); } onResizeStop( @@ -536,7 +616,8 @@ export class Rnd extends React.PureComponent { const { maxWidth, maxHeight } = this.getMaxSizesFromProps(); this.setState({ maxWidth, maxHeight }); if (this.props.onResizeStop) { - this.props.onResizeStop(e, direction, elementRef, delta, this.resizingPosition); + const position = this.resizingPosition; + this.props.onResizeStop(e, direction, elementRef, delta, position); } } @@ -545,8 +626,19 @@ export class Rnd extends React.PureComponent { this.resizable.updateSize({ width: size.width, height: size.height }); } - updatePosition(position: Position) { - this.draggable.setState(position); + updatePosition(position: { x: number | string; y: number | string }) { + let parentSize: { width: number; height: number } | null = null; + try { + parentSize = this.getParentSize(); + } catch { + parentSize = null; + } + const axisWidth = parentSize ? parentSize.width : null; + const axisHeight = parentSize ? parentSize.height : null; + const { left, top } = this.offsetFromParent; + const x = parsePositionValue(position.x, axisWidth); + const y = parsePositionValue(position.y, axisHeight); + this.draggable.setState({ x: x - left, y: y - top }); } updateOffsetFromParent() { @@ -615,13 +707,23 @@ export class Rnd extends React.PureComponent { ...style, }; const { left, top } = this.offsetFromParent; - let draggablePosition; + let draggablePosition: { x: number; y: number } | undefined; if (position) { + const parentSize = this.state.parentSize; + const width = parentSize ? parentSize.width : null; + const height = parentSize ? parentSize.height : null; + const xPx = parsePositionValue(position.x, width); + const yPx = parsePositionValue(position.y, height); draggablePosition = { - x: position.x - left, - y: position.y - top, + x: xPx - left, + y: yPx - top, }; } + + const defaultPositionForDraggable = + defaultValue && typeof defaultValue.x === "number" && typeof defaultValue.y === "number" + ? { x: defaultValue.x, y: defaultValue.y } + : undefined; // INFO: Make uncontorolled component when resizing to control position by setPostion. const pos = this.state.resizing ? undefined : draggablePosition; const dragAxisOrUndefined = this.state.resizing ? "both" : dragAxis; @@ -633,7 +735,7 @@ export class Rnd extends React.PureComponent { this.draggable = c; }} handle={dragHandleClassName ? `.${dragHandleClassName}` : undefined} - defaultPosition={defaultValue} + defaultPosition={defaultPositionForDraggable} onMouseDown={onMouseDown} // @ts-expect-error onMouseUp={onMouseUp} diff --git a/stories/index.tsx b/stories/index.tsx index e54836ce..f02d7c4e 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -31,6 +31,9 @@ import BoundsElementUncontrolled from "./bounds/element-uncontrolled"; import SizePercentUncontrolled from "./size/size-percent-uncontrolled"; import SizePercentControlled from "./size/size-percent-controlled"; +import PositionPercentUncontrolled from "./position/position-percent-uncontrolled"; +import PositionPercentControlled from "./position/position-percent-controlled"; + import Callbacks from "./callback/callbacks"; import Cancel from "./cancel/cancel"; @@ -82,6 +85,10 @@ storiesOf("size", module) .add("percent uncontrolled", () => ) .add("percent controlled", () => ); +storiesOf("position", module) + .add("percent uncontrolled", () => ) + .add("percent controlled", () => ); + storiesOf("callbacks", module).add("callback", () => ); storiesOf("cancel", module).add("cancel", () => ); diff --git a/stories/position/position-percent-controlled.tsx b/stories/position/position-percent-controlled.tsx new file mode 100644 index 00000000..b145e63d --- /dev/null +++ b/stories/position/position-percent-controlled.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { Rnd } from "../../src"; +import { style } from "../styles"; + +type State = { + x: number; // percentage of container width + y: number; // percentage of container height + width: number; + height: number; +}; + +const containerStyle: React.CSSProperties = { + width: 400, + height: 300, + position: "relative", + background: "#e8e8e8", + border: "1px solid #ccc", +}; + +export default class Example extends React.Component<{}, State> { + constructor(props: {}) { + super(props); + this.state = { + width: 120, + height: 80, + x: 10, + y: 20, + }; + } + + render() { + return ( +
+ { + const containerWidth = containerStyle.width as number; + const containerHeight = containerStyle.height as number; + const x = (d.x / containerWidth) * 100; + const y = (d.y / containerHeight) * 100; + this.setState({ x, y }); + }} + onResizeStop={(e, direction, ref, delta, position) => { + const containerWidth = containerStyle.width as number; + const containerHeight = containerStyle.height as number; + const x = (position.x / containerWidth) * 100; + const y = (position.y / containerHeight) * 100; + this.setState({ + width: ref.offsetWidth, + height: ref.offsetHeight, + x, + y, + }); + }} + > + Position in % (x: {this.state.x.toFixed(1)}, y: {this.state.y.toFixed(1)}) + +
+ ); + } +} diff --git a/stories/position/position-percent-uncontrolled.tsx b/stories/position/position-percent-uncontrolled.tsx new file mode 100644 index 00000000..2aaba998 --- /dev/null +++ b/stories/position/position-percent-uncontrolled.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Rnd } from "../../src"; +import { style } from "../styles"; + +const containerStyle: React.CSSProperties = { + width: 400, + height: 300, + position: "relative", + background: "#e8e8e8", + border: "1px solid #ccc", +}; + +export default function Example() { + return ( +
+ + Default at 25%, 30% (uncontrolled) + +
+ ); +}