diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx index 22c7d47b..844ac05d 100644 --- a/src/components/mode-tools/mode-tools.jsx +++ b/src/components/mode-tools/mode-tools.jsx @@ -122,7 +122,7 @@ const ModeToolsComponent = props => { onClick={props.onCopyToClipboard} /> 0)} + disabled={!(props.clipboardItems.length > 0)} imgSrc={pasteIcon} title={props.intl.formatMessage(messages.paste)} onClick={props.onPasteFromClipboard} @@ -153,7 +153,7 @@ const ModeToolsComponent = props => { ModeToolsComponent.propTypes = { brushValue: PropTypes.number, className: PropTypes.string, - clipboard: PropTypes.arrayOf(PropTypes.array), + clipboardItems: PropTypes.arrayOf(PropTypes.array), eraserValue: PropTypes.number, intl: intlShape.isRequired, mode: PropTypes.string.isRequired, @@ -167,7 +167,7 @@ ModeToolsComponent.propTypes = { const mapStateToProps = state => ({ mode: state.scratchPaint.mode, brushValue: state.scratchPaint.brushMode.brushSize, - clipboard: state.scratchPaint.clipboard, + clipboardItems: state.scratchPaint.clipboard.items, eraserValue: state.scratchPaint.eraserMode.brushSize, selectedItems: state.scratchPaint.selectedItems }); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 79803d34..2cfec304 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -5,7 +5,7 @@ import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx'; import {changeMode} from '../reducers/modes'; import {undo, redo, undoSnapshot} from '../reducers/undo'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; -import {setClipboardItems} from '../reducers/clipboard'; +import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard'; import {getGuideLayer, getBackgroundGuideLayer} from '../helper/layer'; import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo'; @@ -115,14 +115,17 @@ class PaintEditor extends React.Component { handlePasteFromClipboard () { clearSelection(this.props.clearSelectedItems); - if (this.props.clipboard.length > 0) { - for (let i = 0; i < this.props.clipboard.length; i++) { - const item = paper.Base.importJSON(this.props.clipboard[i]); + if (this.props.clipboardItems.length > 0) { + for (let i = 0; i < this.props.clipboardItems.length; i++) { + const item = paper.Base.importJSON(this.props.clipboardItems[i]); if (item) { item.selected = true; } - paper.project.getActiveLayer().addChild(item); + const placedItem = paper.project.getActiveLayer().addChild(item); + placedItem.position.x += 10 * this.props.pasteOffset; + placedItem.position.y += 10 * this.props.pasteOffset; } + this.props.incrementPasteOffset(); this.props.setSelectedItems(); paper.project.view.update(); this.handleUpdateSvg(); @@ -175,13 +178,15 @@ class PaintEditor extends React.Component { PaintEditor.propTypes = { clearSelectedItems: PropTypes.func.isRequired, - clipboard: PropTypes.arrayOf(PropTypes.array), + clipboardItems: PropTypes.arrayOf(PropTypes.array), + incrementPasteOffset: PropTypes.func.isRequired, name: PropTypes.string, onKeyPress: PropTypes.func.isRequired, onRedo: PropTypes.func.isRequired, onUndo: PropTypes.func.isRequired, onUpdateName: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, + pasteOffset: PropTypes.number, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, setClipboardItems: PropTypes.func.isRequired, @@ -198,7 +203,8 @@ PaintEditor.propTypes = { const mapStateToProps = state => ({ selectedItems: state.scratchPaint.selectedItems, undoState: state.scratchPaint.undo, - clipboard: state.scratchPaint.clipboard + clipboardItems: state.scratchPaint.clipboard.items, + pasteOffset: state.scratchPaint.clipboard.pasteOffset }); const mapDispatchToProps = dispatch => ({ onKeyPress: event => { @@ -229,6 +235,9 @@ const mapDispatchToProps = dispatch => ({ }, setClipboardItems: items => { dispatch(setClipboardItems(items)); + }, + incrementPasteOffset: () => { + dispatch(incrementPasteOffset()); } }); diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index bf931e88..0179aee8 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -13,6 +13,7 @@ import {deleteSelection, getSelectedLeafItems} from '../helper/selection'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {pan, resetZoom, zoomOnFixedPoint} from '../helper/view'; import {clearHoveredItem} from '../reducers/hover'; +import {clearPasteOffset} from '../reducers/clipboard'; import styles from './paper-canvas.css'; @@ -50,6 +51,7 @@ class PaperCanvas extends React.Component { this.props.clearUndo(); this.props.clearSelectedItems(); this.props.clearHoveredItem(); + this.props.clearPasteOffset(); if (newProps.svg) { // Store the zoom/pan and restore it after importing a new SVG const oldZoom = paper.project.view.zoom; @@ -160,6 +162,7 @@ class PaperCanvas extends React.Component { PaperCanvas.propTypes = { canvasRef: PropTypes.func, clearHoveredItem: PropTypes.func.isRequired, + clearPasteOffset: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, clearUndo: PropTypes.func.isRequired, mode: PropTypes.oneOf(Object.values(Modes)), @@ -189,6 +192,9 @@ const mapDispatchToProps = dispatch => ({ }, clearHoveredItem: () => { dispatch(clearHoveredItem()); + }, + clearPasteOffset: () => { + dispatch(clearPasteOffset()); } }); diff --git a/src/reducers/clipboard.js b/src/reducers/clipboard.js index bd5a2c3f..7ab32643 100644 --- a/src/reducers/clipboard.js +++ b/src/reducers/clipboard.js @@ -1,7 +1,12 @@ import log from '../log/log'; const SET = 'scratch-paint/clipboard/SET'; -const initialState = []; +const INCREMENT_PASTE_OFFSET = 'scratch-paint/clipboard/INCREMENT_PASTE_OFFSET'; +const CLEAR_PASTE_OFFSET = 'scratch-paint/clipboard/CLEAR_PASTE_OFFSET'; +const initialState = { + items: [], + pasteOffset: 0 +}; const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; @@ -11,7 +16,20 @@ const reducer = function (state, action) { log.warn(`Invalid clipboard item format`); return state; } - return action.clipboardItems; + return { + items: action.clipboardItems, + pasteOffset: 1 + }; + case INCREMENT_PASTE_OFFSET: + return { + items: state.items, + pasteOffset: state.pasteOffset + 1 + }; + case CLEAR_PASTE_OFFSET: + return { + items: state.items, + pasteOffset: 0 + }; default: return state; } @@ -25,7 +43,21 @@ const setClipboardItems = function (clipboardItems) { }; }; +const incrementPasteOffset = function () { + return { + type: INCREMENT_PASTE_OFFSET + }; +}; + +const clearPasteOffset = function () { + return { + type: CLEAR_PASTE_OFFSET + }; +}; + export { reducer as default, - setClipboardItems + setClipboardItems, + incrementPasteOffset, + clearPasteOffset }; diff --git a/test/unit/clipboard-reducer.test.js b/test/unit/clipboard-reducer.test.js index 7428016a..bb8e060b 100644 --- a/test/unit/clipboard-reducer.test.js +++ b/test/unit/clipboard-reducer.test.js @@ -1,11 +1,12 @@ /* eslint-env jest */ import clipboardReducer from '../../src/reducers/clipboard'; -import {setClipboardItems} from '../../src/reducers/clipboard'; +import {clearPasteOffset, incrementPasteOffset, setClipboardItems} from '../../src/reducers/clipboard'; test('initialState', () => { let defaultState; - expect(clipboardReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); + expect(clipboardReducer(defaultState /* state */, {type: 'anything'} /* action */).items).toBeDefined(); + expect(clipboardReducer(defaultState /* state */, {type: 'anything'} /* action */).pasteOffset).toBeDefined(); }); test('setClipboardItems', () => { @@ -13,14 +14,45 @@ test('setClipboardItems', () => { const newSelected1 = ['selected1', 'selected2']; const newSelected2 = ['selected1', 'selected3']; - expect(clipboardReducer(defaultState /* state */, setClipboardItems(newSelected1) /* action */)) + expect(clipboardReducer(defaultState /* state */, setClipboardItems(newSelected1) /* action */).items) .toEqual(newSelected1); - expect(clipboardReducer(newSelected1, setClipboardItems(newSelected2) /* action */)) + expect(clipboardReducer(defaultState /* state */, setClipboardItems(newSelected1) /* action */).pasteOffset) + .toEqual(1); + expect(clipboardReducer(newSelected1, setClipboardItems(newSelected2) /* action */).items) .toEqual(newSelected2); + expect(clipboardReducer(defaultState /* state */, setClipboardItems(newSelected1) /* action */).pasteOffset) + .toEqual(1); +}); + +test('incrementPasteOffset', () => { + const origState = { + items: ['selected1', 'selected2'], + pasteOffset: 1 + }; + + expect(clipboardReducer(origState /* state */, incrementPasteOffset() /* action */).pasteOffset) + .toEqual(2); + expect(clipboardReducer(origState, incrementPasteOffset() /* action */).items) + .toEqual(origState.items); +}); + +test('clearPasteOffset', () => { + const origState = { + items: ['selected1', 'selected2'], + pasteOffset: 1 + }; + + expect(clipboardReducer(origState /* state */, clearPasteOffset() /* action */).pasteOffset) + .toEqual(0); + expect(clipboardReducer(origState, clearPasteOffset() /* action */).items) + .toEqual(origState.items); }); test('invalidSetClipboardItems', () => { - const origState = ['selected1', 'selected2']; + const origState = { + items: ['selected1', 'selected2'], + pasteOffset: 1 + }; const nothingSelected = []; expect(clipboardReducer(origState /* state */, setClipboardItems() /* action */))