Skip to content

Repository files navigation

React Shape Editor

Simple shape editor component

shape-editor

npm versionCircleCI

Installation

npm i react-shape-editor

Components

ShapeEditor

The wrapper for the entire editor component. Contains the <svg> element.

PropTypeDefault
Description
childrenrenderable elementsnullWill include components such as wrapShape-wrapped shapes, other library components (SelectionLayer/ImageLayer/DrawLayer) or arbitrary SVG elements
focusOnAddbooltrueIf true, focus on newly created elements.
focusOnDeletebooltrueIf true, focus on the next-closest element after a shape is deleted.
scalenumber1Scale factor of the svg contents. For example, given a vectorWidth of 100 and a scale of 0.5, the rendered DOM element will be 50 px wide.
styleobject{}Style to apply to the <svg> element.
vectorHeightnumber0Height of the <svg> element viewBox.
vectorWidthnumber0Width of the <svg> element viewBox.

wrapShape (HOC)

When used to wrap an SVG element, enables resize and movement functionality.

Usage

constWrappedRect=wrapShape(({ height, width /* ... "wrapShape Props Received" */})=>(<rectfill="blue"height={height}width={width}/>))// later, in render()<WrappedRectshapeId={myId}x={12}y={56}width={20}/* ... "WrappedShape Props" *//>

wrapShape Props Received

PropType
Description
heightnumberHeight of the shape.
widthnumberWidth of the shape.
scalenumberScale of the parent <svg> element, provided so you can render strokes or other components that maintain a constant size at every zoom level.
shapeIdstringUnique identifier for the shape.
xnumberx-axis offset of the shape. NOTE: You should not use this to set the position of your shape, because the <g> that wraps the shape already includes this offset.
ynumbery-axis offset of the shape. NOTE: You should not use this to set the position of your shape, because the <g> that wraps the shape already includes this offset.
disabledboolIf true, the shape cannot be moved or resized, and shows no resize handles.
isBeingChangedboolIf true, the shape is currently being moved or scaled.
activeboolIf true, the shape has HTML-native focus or is selected via a SelectionLayer.
nativeActiveboolIf true, the shape has HTML-native focus (keyboard events will get applied to it).
isInSelectionGroupboolIf you assigned it via a prop on the WrappedShape, this will be available to tell whether or not the shape is in a group of two or more selected shapes (when using the SelectionLayer component). Useful for hiding the resize handles when selected in a group, or adding an outline.
other propsanyAny extra props passed to the WrappedShape are passed down as-is to this component.

WrappedShape Props

PropType
Default
Description
height
(required)
numberHeight of the shape.
shapeId
(required)
stringUnique identifier for the shape, to aid in data handling.
width
(required)
numberWidth of the shape.
x
(required)
numberx-axis offset of the shape.
y
(required)
numbery-axis offset of the shape.
onChangefunc()=>{}Listener for transformation of this shape triggered by interactions with resize handles, panning, or keyboard shortcuts. Required for user-triggered shape transformations to work. Signature: (newRect: { x: number, y: number, height: number, width: number }, WrappedShapeProps: object) => void
onDeletefunc()=>{}Listener for the deletion of this shape via backspace or delete keys. Required for user-triggered shape deletion to work. Signature: (event: Event, WrappedShapeProps: object) => void
activeboolfalseIf true, the shape is rendered as focused (particularly important when using a SelectionLayer). When not using a selection layer, this prop can be left unset, as native HTML focus will handle focus state.
constrainMovefuncnon-constraining functionA callback for restricting movement during shape transformations (e.g., to lock movement to one axis, keeping the shape inside a predefined boundary or snapping it to a grid). Signature: ({ originalX: number, originalY: number, x: number, y: number, width: number, height: number }) => ({ x: number, y: number })
constrainResizefuncnon-constraining functionA callback for restricting resizing during shape transformations (e.g., to lock resizing to one axis, keeping the shape inside a predefined boundary or snapping it to a grid). Signature: ({ originalMovingCorner: { x: number, y: number }, startCorner: { x: number, y: number }, movingCorner: { x: number, y: number }, lockedDimension: one of "x" or "y" }) => ({ x: number, y: number })
disabledboolfalseIf true, the shape cannot be moved or resized, and shows no resize handles.
isInSelectionGroupboolfalseWhether or not the shape is in a group of two or more selected shapes (when using the SelectionLayer component). Prop is merely forwarded to the wrapped component to be used in customized rendering, e.g., hiding the resize handles when selected in a group, or adding an outline.
keyboardTransformMultipliernumber1Multiplier for keyboard-triggered transforms, such as ↑↓←→ keys to move or shift+↑↓←→ keys to resize. For example, with the default setting of 1, pressing would move the shape 1 px to the right. With a setting of 5, it would move 5px.
onKeyDownfunc()=>{}Listener for shape keydown event. If event.preventDefault() is called inside, it will override the default keyboard shortcut behavior. Signature: (event: Event, WrappedShapeProps: object) => void
onBlurfunc()=>{}Listener for shape blur event. Signature: (event: Event, WrappedShapeProps: object) => void
onFocusfunc()=>{}Listener for shape focus event. Signature: (event: Event, WrappedShapeProps: object) => void
ResizeHandleComponentComponentDefaultResizeHandleComponentThe component to use for shape handles.
wrapperPropsobject{}Extra props to add to the SVG <g> element wrapping the shape.
other propsanyAny extra props passed to the WrappedShape are passed down as-is to the component being wrapped.

ImageLayer

Renders an svg image element.

PropTypeDefault
Description
src
(required)
stringURL for the image to display.
onLoadfunc()=>{}Callback for the image load. Signature: ({ naturalWidth: number, naturalHeight: number }) => void

DrawLayer

Creates an invisible layer of the SVG that allows users to draw shapes via mouse click-and-drag.

PropType
Default
Description
onAddShape
(required)
funcCallback for when a shape has finished being drawn. Use it to add the new shape to your data. Signature: (newRect: { x: number, y: number, height: number, width: number }) => void
constrainMovefuncnon-constraining functionA callback for restricting the initial starting location for the drawing (e.g., to lock movement to one axis, keeping the shape inside a predefined boundary or snapping it to a grid). Signature: ({ originalX: number, originalY: number, x: number, y: number, width: number, height: number }) => ({ x: number, y: number })
constrainResizefuncnon-constraining functionA callback for restricting the dragged corner when drawing a shape (e.g., to lock resizing to one axis, keeping the shape inside a predefined boundary or snapping it to a grid). Signature: ({ originalMovingCorner: { x: number, y: number }, startCorner: { x: number, y: number }, movingCorner: { x: number, y: number }, lockedDimension: one of "x" or "y" }) => ({ x: number, y: number })
DrawPreviewComponentwrapShape(Component)DefaultDrawPreviewComponentThe component to preview the shape while dragging. Must be wrapped with wrapShape.

SelectionLayer

Creates an invisible layer of the SVG that allows users to select shapes via mouse click-and-drag.

PropType
Default
Description
selectedShapeIds
(required)
string[]shapeIds that belong to the currently selected group. Should be populated with shapeIds passed back from onSelectionChange.
onSelectionChangefunc()=>{}Listener for the addition or removal of shapes from the selection group (e.g., by shift+clicking extra shape or drawing new selection). Required for user-triggered selection to work. Signature: (selectedShapeIds: string[]) => void
onChangefunc()=>{}Listener for transformation of shapes in the selection triggered by interactions with resize handles, panning, or keyboard shortcuts. Required for user-triggered shape transformations on selections to work. Signature: (newRects: { x: number, y: number, height: number, width: number }[], selectedShapesProps: object[]) => void
onDeletefunc()=>{}Listener for the deletion of all shapes in this selection via backspace or delete keys. Required for user-triggered shape deletion to work. Signature: (event: Event, selectedShapesProps: object[]) => void
SelectionDrawComponentwrapShape(Component)DefaultSelectionDrawComponentThe component to preview the selection while dragging. Must be wrapped with wrapShape.
SelectionComponentwrapShape(Component)DefaultSelectionComponentThe component that visually wraps around the selected shapes (i.e., the selection outline). Must be wrapped with wrapShape.
minimumDistanceForSelectionnumber15Minimum height or width that the drawn selection must be in order to perform the selection.
keyboardTransformMultipliernumber1Multiplier for keyboard-triggered transforms, such as ↑↓←→ keys to move or shift+↑↓←→ keys to resize. For example, with the default setting of 1, pressing would move shapes in the selection 1 px to the right. With a setting of 5, they would move 5px.
selectionComponentPropsobject{}Extra props to pass to the SelectionComponent.
childrenrenderable elementsnullwrapShape-wrapped shapes that are targets for selection by this selection group. Can also include other library components (SelectionLayer/ImageLayer/DrawLayer) or arbitrary SVG elements.

Usage

importReactfrom'react';import{ShapeEditor,ImageLayer,DrawLayer,wrapShape,}from'react-shape-editor';functionarrayReplace(arr,index,item){return[
...arr.slice(0,index),
...(Array.isArray(item) ? item : [item]),
...arr.slice(index+1),];}constRectShape=wrapShape(({ width, height })=>(<rectwidth={width}height={height}fill="rgba(0,0,255,0.5)"/>));letidIterator=1;exportdefaultclassEditorextendsReact.Component{constructor(props){super(props);this.state={items: [{id: '1',x: 20,y: 50,width: 50,height: 25},{id: '2',x: 120,y: 0,width: 20,height: 75},],vectorWidth: 0,vectorHeight: 0,};}render(){const{ items, vectorWidth, vectorHeight }=this.state;return(<divstyle={{height: 400}}><ShapeEditorvectorWidth={vectorWidth}vectorHeight={vectorHeight}><ImageLayersrc="https://raw.githubusercontent.com/fritz-c/react-shape-editor/d8661b46d07d832e316aacc906a0d603a3bb13a2/website/blank.png"onLoad={({ naturalWidth, naturalHeight })=>{this.setState({vectorWidth: naturalWidth,vectorHeight: naturalHeight,});}}/><DrawLayeronAddShape={({ x, y, width, height })=>{this.setState(state=>({items: [
...state.items,{id: `id${idIterator}`, x, y, width, height },],}));idIterator+=1;}}/>{items.map((item,index)=>{const{ id, height, width, x, y }=item;return(<RectShapekey={id}shapeId={id}height={height}width={width}x={x}y={y}onChange={newRect=>{this.setState(state=>({items: arrayReplace(state.items,index,{
...item,
...newRect,}),}));}}onDelete={()=>{this.setState(state=>({items: arrayReplace(state.items,index,[]),}));}}/>);})}</ShapeEditor></div>);}}

Contributing

After cloning the repository and running npm install inside, you can use the following commands to develop and build the project.

# Starts a dev server that hosts a demo page with the component.
npm start
# Runs the library tests
npm test# Lints the code with eslint
npm run lint
# Lints and builds the code, placing the result in the dist directory.# This build is necessary to reflect changes if you're# `npm link`-ed to this repository from another local project.
npm run build

Pull requests are welcome!

License

MIT

About

Simple shape editor component with React and SVG

Resources

Stars

10 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages