diff --git a/src/EditorControls.js b/src/EditorControls.js index 0686f2c70..204ca4a9e 100644 --- a/src/EditorControls.js +++ b/src/EditorControls.js @@ -289,6 +289,46 @@ class EditorControls extends Component { } break; + case EDITOR_ACTIONS.MOVE_TO: + // checking if fromIndex and toIndex is a number because + // gives errors if index is 0 (falsy value) + if (payload.path && !isNaN(payload.fromIndex) && !isNaN(payload.toIndex)) { + function move(container) { + const movedEl = container[payload.fromIndex]; + const replacedEl = container[payload.toIndex]; + container[payload.toIndex] = movedEl; + container[payload.fromIndex] = replacedEl; + } + + if (payload.path === 'data') { + move(graphDiv.data); + } + + if (payload.path === 'layout.images') { + move(graphDiv.layout.images); + } + + if (payload.path === 'layout.shapes') { + move(graphDiv.layout.shapes); + } + + if (payload.path === 'layout.annotations') { + move(graphDiv.layout.annotations); + } + + const updatedData = payload.path.startsWith('data') + ? graphDiv.data.slice() + : graphDiv.data; + const updatedLayout = payload.path.startsWith('layout') + ? Object.assign({}, graphDiv.layout) + : graphDiv.layout; + + if (this.props.onUpdate) { + this.props.onUpdate(updatedData, updatedLayout, graphDiv._transitionData._frames); + } + } + break; + default: throw new Error(this.localize('must specify an action type to handleEditorUpdate')); } diff --git a/src/components/containers/AnnotationAccordion.js b/src/components/containers/AnnotationAccordion.js index 6b8a69303..0ce03d26d 100644 --- a/src/components/containers/AnnotationAccordion.js +++ b/src/components/containers/AnnotationAccordion.js @@ -13,7 +13,7 @@ class AnnotationAccordion extends Component { layout: {annotations = [], meta = []}, localize: _, } = this.context; - const {canAdd, children} = this.props; + const {canAdd, children, canReorder} = this.props; const content = annotations.length && @@ -50,7 +50,7 @@ class AnnotationAccordion extends Component { }; return ( - + {content ? ( content ) : ( @@ -76,6 +76,7 @@ AnnotationAccordion.contextTypes = { AnnotationAccordion.propTypes = { children: PropTypes.node, canAdd: PropTypes.bool, + canReorder: PropTypes.bool, }; export default AnnotationAccordion; diff --git a/src/components/containers/ImageAccordion.js b/src/components/containers/ImageAccordion.js index e9a9c488c..329e9f882 100644 --- a/src/components/containers/ImageAccordion.js +++ b/src/components/containers/ImageAccordion.js @@ -13,7 +13,7 @@ class ImageAccordion extends Component { layout: {images = []}, localize: _, } = this.context; - const {canAdd, children} = this.props; + const {canAdd, children, canReorder} = this.props; const content = images.length && @@ -48,7 +48,7 @@ class ImageAccordion extends Component { }; return ( - + {content ? ( content ) : ( @@ -74,6 +74,7 @@ ImageAccordion.contextTypes = { ImageAccordion.propTypes = { children: PropTypes.node, canAdd: PropTypes.bool, + canReorder: PropTypes.bool, }; export default ImageAccordion; diff --git a/src/components/containers/PlotlyFold.js b/src/components/containers/PlotlyFold.js index 87a500778..e7a706877 100644 --- a/src/components/containers/PlotlyFold.js +++ b/src/components/containers/PlotlyFold.js @@ -21,7 +21,7 @@ export class Fold extends Component { if (!this.foldVisible && !this.props.messageIfEmpty) { return null; } - const {deleteContainer} = this.context; + const {deleteContainer, moveContainer} = this.context; const { canDelete, children, @@ -33,6 +33,8 @@ export class Fold extends Component { icon: Icon, messageIfEmpty, name, + canMoveUp, + canMoveDown, } = this.props; const contentClass = classnames('fold__content', { @@ -47,7 +49,7 @@ export class Fold extends Component { 'fold__top__arrow--open': !folded, }); - const arrowIcon = ( + const arrowDownIcon = (
@@ -70,13 +72,50 @@ export class Fold extends Component {
) : null; + const movingControls = (canMoveDown || canMoveUp) && ( +
+ { + // prevents fold toggle to happen when clicking on moving arrow controls + e.stopPropagation(); + + if (canMoveUp) { + if (!moveContainer || typeof moveContainer !== 'function') { + throw new Error('moveContainer must be a function'); + } + moveContainer('up'); + } + }} + > + + + { + // prevents fold toggle to happen when clicking on moving arrow controls + e.stopPropagation(); + if (canMoveDown) { + if (!moveContainer || typeof moveContainer !== 'function') { + throw new Error('moveContainer must be a function'); + } + moveContainer('down'); + } + }} + > + + +
+ ); + const foldHeader = !hideHeader && (
- {arrowIcon} + {arrowDownIcon} {icon}
{striptags(name)}
+ {movingControls} {deleteButton}
); @@ -118,6 +157,8 @@ Fold.propTypes = { icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), messageIfEmpty: PropTypes.string, name: PropTypes.string, + canMoveUp: PropTypes.bool, + canMoveDown: PropTypes.bool, }; Fold.contextTypes = { @@ -175,6 +216,7 @@ PlotlyFold.plotly_editor_traits = { PlotlyFold.contextTypes = Object.assign( { deleteContainer: PropTypes.func, + moveContainer: PropTypes.func, }, containerConnectedContextTypes ); diff --git a/src/components/containers/PlotlyPanel.js b/src/components/containers/PlotlyPanel.js index 2281dd718..e3c20d19e 100644 --- a/src/components/containers/PlotlyPanel.js +++ b/src/components/containers/PlotlyPanel.js @@ -86,6 +86,7 @@ export class Panel extends Component { render() { const {individualFoldStates, hasError} = this.state; + const {canReorder} = this.props; if (hasError) { return ; @@ -97,6 +98,11 @@ export class Panel extends Component { key: index, folded: individualFoldStates[index] || false, toggleFold: () => this.toggleFold(index), + canMoveUp: canReorder && individualFoldStates.length > 1 && index > 0, + canMoveDown: + canReorder && + individualFoldStates.length > 1 && + index !== individualFoldStates.length - 1, }); } return child; @@ -122,6 +128,7 @@ Panel.propTypes = { deleteAction: PropTypes.func, noPadding: PropTypes.bool, showExpandCollapse: PropTypes.bool, + canReorder: PropTypes.bool, }; Panel.defaultProps = { diff --git a/src/components/containers/ShapeAccordion.js b/src/components/containers/ShapeAccordion.js index a321eb643..711fdb3c5 100644 --- a/src/components/containers/ShapeAccordion.js +++ b/src/components/containers/ShapeAccordion.js @@ -14,7 +14,7 @@ class ShapeAccordion extends Component { layout: {shapes = []}, localize: _, } = this.context; - const {canAdd, children} = this.props; + const {canAdd, children, canReorder} = this.props; const content = shapes.length && @@ -48,7 +48,7 @@ class ShapeAccordion extends Component { }; return ( - + {content ? ( content ) : ( @@ -74,6 +74,7 @@ ShapeAccordion.contextTypes = { ShapeAccordion.propTypes = { children: PropTypes.node, canAdd: PropTypes.bool, + canReorder: PropTypes.bool, }; export default ShapeAccordion; diff --git a/src/components/containers/TraceAccordion.js b/src/components/containers/TraceAccordion.js index 062523521..db64c4f3d 100644 --- a/src/components/containers/TraceAccordion.js +++ b/src/components/containers/TraceAccordion.js @@ -130,7 +130,7 @@ class TraceAccordion extends Component { } render() { - const {canAdd, canGroup} = this.props; + const {canAdd, canGroup, canReorder} = this.props; const _ = this.context.localize; if (canAdd) { @@ -146,7 +146,7 @@ class TraceAccordion extends Component { }; const traceFolds = this.renderTraceFolds(); return ( - + {traceFolds ? traceFolds : this.renderTracePanelHelp()} ); @@ -190,6 +190,7 @@ TraceAccordion.contextTypes = { TraceAccordion.propTypes = { canAdd: PropTypes.bool, canGroup: PropTypes.bool, + canReorder: PropTypes.bool, children: PropTypes.node, traceFilterCondition: PropTypes.func, }; diff --git a/src/default_panels/GraphCreatePanel.js b/src/default_panels/GraphCreatePanel.js index 5b6cb1045..87d812688 100644 --- a/src/default_panels/GraphCreatePanel.js +++ b/src/default_panels/GraphCreatePanel.js @@ -25,6 +25,7 @@ const GraphCreatePanel = (props, {localize: _, setPanel}) => { traceFilterCondition={t => !(t.transforms && t.transforms.some(tr => ['fit', 'moving-average'].includes(tr.type))) } + canReorder > diff --git a/src/default_panels/StyleImagesPanel.js b/src/default_panels/StyleImagesPanel.js index e7feae8be..71543ded8 100644 --- a/src/default_panels/StyleImagesPanel.js +++ b/src/default_panels/StyleImagesPanel.js @@ -11,7 +11,7 @@ import { } from '../components'; const StyleImagesPanel = (props, {localize: _}) => ( - + ( - + diff --git a/src/default_panels/StyleShapesPanel.js b/src/default_panels/StyleShapesPanel.js index ff6ca1335..9d671dcfc 100644 --- a/src/default_panels/StyleShapesPanel.js +++ b/src/default_panels/StyleShapesPanel.js @@ -13,7 +13,7 @@ import { } from '../components'; const StyleShapesPanel = (props, {localize: _}) => ( - + ; } @@ -85,6 +102,7 @@ export default function connectAnnotationToLayout(WrappedComponent) { container: PropTypes.object, fullContainer: PropTypes.object, getValObject: PropTypes.func, + moveContainer: PropTypes.func, }; const {plotly_editor_traits} = WrappedComponent; diff --git a/src/lib/connectImageToLayout.js b/src/lib/connectImageToLayout.js index f2517a15f..d9be20265 100644 --- a/src/lib/connectImageToLayout.js +++ b/src/lib/connectImageToLayout.js @@ -10,6 +10,7 @@ export default function connectImageToLayout(WrappedComponent) { this.deleteImage = this.deleteImage.bind(this); this.updateImage = this.updateImage.bind(this); + this.moveImage = this.moveImage.bind(this); this.setLocals(props, context); } @@ -35,6 +36,7 @@ export default function connectImageToLayout(WrappedComponent) { deleteContainer: this.deleteImage, container: this.container, fullContainer: this.fullContainer, + moveContainer: this.moveImage, }; } @@ -57,6 +59,21 @@ export default function connectImageToLayout(WrappedComponent) { } } + moveImage(direction) { + if (this.context.onUpdate) { + const imageIndex = this.props.imageIndex; + const desiredIndex = direction === 'up' ? imageIndex - 1 : imageIndex + 1; + this.context.onUpdate({ + type: EDITOR_ACTIONS.MOVE_TO, + payload: { + fromIndex: imageIndex, + toIndex: desiredIndex, + path: 'layout.images', + }, + }); + } + } + render() { return ; } @@ -83,6 +100,7 @@ export default function connectImageToLayout(WrappedComponent) { container: PropTypes.object, fullContainer: PropTypes.object, getValObject: PropTypes.func, + moveContainer: PropTypes.func, }; const {plotly_editor_traits} = WrappedComponent; diff --git a/src/lib/connectShapeToLayout.js b/src/lib/connectShapeToLayout.js index d093686a9..d76789434 100644 --- a/src/lib/connectShapeToLayout.js +++ b/src/lib/connectShapeToLayout.js @@ -10,6 +10,7 @@ export default function connectShapeToLayout(WrappedComponent) { this.deleteShape = this.deleteShape.bind(this); this.updateShape = this.updateShape.bind(this); + this.moveShape = this.moveShape.bind(this); this.setLocals(props, context); } @@ -35,6 +36,7 @@ export default function connectShapeToLayout(WrappedComponent) { deleteContainer: this.deleteShape, container: this.container, fullContainer: this.fullContainer, + moveContainer: this.moveShape, }; } @@ -57,6 +59,21 @@ export default function connectShapeToLayout(WrappedComponent) { } } + moveShape(direction) { + if (this.context.onUpdate) { + const shapeIndex = this.props.shapeIndex; + const desiredIndex = direction === 'up' ? shapeIndex - 1 : shapeIndex + 1; + this.context.onUpdate({ + type: EDITOR_ACTIONS.MOVE_TO, + payload: { + fromIndex: shapeIndex, + toIndex: desiredIndex, + path: 'layout.shapes', + }, + }); + } + } + render() { return ; } @@ -83,6 +100,7 @@ export default function connectShapeToLayout(WrappedComponent) { container: PropTypes.object, fullContainer: PropTypes.object, getValObject: PropTypes.func, + moveContainer: PropTypes.func, }; const {plotly_editor_traits} = WrappedComponent; diff --git a/src/lib/connectTraceToPlot.js b/src/lib/connectTraceToPlot.js index d3420f40e..c4a72a45b 100644 --- a/src/lib/connectTraceToPlot.js +++ b/src/lib/connectTraceToPlot.js @@ -19,6 +19,7 @@ export default function connectTraceToPlot(WrappedComponent) { this.deleteTrace = this.deleteTrace.bind(this); this.updateTrace = this.updateTrace.bind(this); + this.moveTrace = this.moveTrace.bind(this); this.setLocals(props, context); } @@ -40,6 +41,7 @@ export default function connectTraceToPlot(WrappedComponent) { : plotly.PlotSchema.getTraceValObject(fullTrace, nestedProperty({}, attr).parts), updateContainer: this.updateTrace, deleteContainer: this.deleteTrace, + moveContainer: this.moveTrace, container: trace, fullContainer: fullTrace, traceIndexes: this.props.traceIndexes, @@ -178,6 +180,19 @@ export default function connectTraceToPlot(WrappedComponent) { } } + moveTrace(direction) { + const traceIndex = this.props.traceIndexes[0]; + const desiredIndex = direction === 'up' ? traceIndex - 1 : traceIndex + 1; + this.context.onUpdate({ + type: EDITOR_ACTIONS.MOVE_TO, + payload: { + fromIndex: traceIndex, + toIndex: desiredIndex, + path: 'data', + }, + }); + } + render() { return ; } @@ -206,6 +221,7 @@ export default function connectTraceToPlot(WrappedComponent) { container: PropTypes.object, fullContainer: PropTypes.object, traceIndexes: PropTypes.array, + moveContainer: PropTypes.func, }; const {plotly_editor_traits} = WrappedComponent; diff --git a/src/lib/constants.js b/src/lib/constants.js index 9f08e9fe4..8d31eb5ff 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -38,6 +38,7 @@ export const EDITOR_ACTIONS = { DELETE_IMAGE: 'plotly-editor-delete-image', DELETE_RANGESELECTOR: 'plotly-editor-delete-rangeselector', DELETE_TRANSFORM: 'plotly-editor-delete-transform', + MOVE_TO: 'plotly-editor-move-to', }; export const DEFAULT_FONTS = [ diff --git a/src/styles/components/containers/_fold.scss b/src/styles/components/containers/_fold.scss index 225ecedd9..5876e41b5 100644 --- a/src/styles/components/containers/_fold.scss +++ b/src/styles/components/containers/_fold.scss @@ -13,8 +13,7 @@ height: 15px; border-radius: var(--border-radius); text-shadow: var(--text-shadow-dark-ui); - transition: background-color 0.1s ease-in-out, color 0.1s ease-in-out, - border 0.1s ease-in-out; + transition: background-color 0.1s ease-in-out, color 0.1s ease-in-out, border 0.1s ease-in-out; &:hover { cursor: pointer; @@ -87,6 +86,60 @@ } } + &__moving-controls { + height: 27px; + margin-top: -7px; + margin-right: 5px; + + svg { + font-weight: bold; + opacity: 0.75; + } + + &--up { + height: 13px; + width: 18px; + display: block; + svg { + transform: rotate(-180deg); + transform-origin: center center; + } + svg:hover { + opacity: 1; + } + &--disabled { + @extend .fold__top__moving-controls--up; + svg { + transform: rotate(-180deg); + transform-origin: center center; + opacity: 0.3; + } + svg:hover { + opacity: 0.3; + } + } + } + + &--down { + height: 13px; + width: 18px; + display: block; + margin-top: -2px; + svg:hover { + opacity: 1; + } + &--disabled { + @extend .fold__top__moving-controls--down; + svg { + opacity: 0.3; + } + svg:hover { + opacity: 0.3; + } + } + } + } + &__delete { font-size: 18px; opacity: 0.75;