From 7e1e8d96ac25c4e97b1d6f5ee36e7cff76eef987 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 1 Nov 2017 16:57:02 -0400 Subject: [PATCH] add copy/paste --- src/components/mode-tools/mode-tools.jsx | 19 ++++++--- src/components/paint-editor/paint-editor.jsx | 7 +++- src/containers/paint-editor.jsx | 44 ++++++++++++++++++-- src/containers/paper-canvas.jsx | 2 +- src/helper/selection.js | 10 ----- src/reducers/clipboard.js | 31 ++++++++++++++ src/reducers/scratch-paint-reducer.js | 4 +- test/unit/clipboard-reducer.test.js | 32 ++++++++++++++ 8 files changed, 127 insertions(+), 22 deletions(-) create mode 100644 src/reducers/clipboard.js create mode 100644 test/unit/clipboard-reducer.test.js diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx index 7c5fbc7b..22c7d47b 100644 --- a/src/components/mode-tools/mode-tools.jsx +++ b/src/components/mode-tools/mode-tools.jsx @@ -1,3 +1,4 @@ +import paper from '@scratch/paper'; import classNames from 'classnames'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; @@ -115,16 +116,16 @@ const ModeToolsComponent = props => {
0)} imgSrc={pasteIcon} title={props.intl.formatMessage(messages.paste)} - onClick={function () {}} + onClick={props.onPasteFromClipboard} /> {/* { ModeToolsComponent.propTypes = { brushValue: PropTypes.number, className: PropTypes.string, + clipboard: PropTypes.arrayOf(PropTypes.array), eraserValue: PropTypes.number, intl: intlShape.isRequired, mode: PropTypes.string.isRequired, onBrushSliderChange: PropTypes.func, - onEraserSliderChange: PropTypes.func + onCopyToClipboard: PropTypes.func.isRequired, + onEraserSliderChange: PropTypes.func, + onPasteFromClipboard: PropTypes.func.isRequired, + selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)) }; const mapStateToProps = state => ({ mode: state.scratchPaint.mode, brushValue: state.scratchPaint.brushMode.brushSize, - eraserValue: state.scratchPaint.eraserMode.brushSize + clipboard: state.scratchPaint.clipboard, + eraserValue: state.scratchPaint.eraserMode.brushSize, + selectedItems: state.scratchPaint.selectedItems }); const mapDispatchToProps = dispatch => ({ onBrushSliderChange: brushSize => { diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 6ea036a1..c010102a 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -247,7 +247,10 @@ class PaintEditorComponent extends React.Component { /> - +
@@ -339,7 +342,9 @@ PaintEditorComponent.propTypes = { canUndo: PropTypes.func.isRequired, intl: intlShape, name: PropTypes.string, + onCopyToClipboard: PropTypes.func.isRequired, onGroup: PropTypes.func.isRequired, + onPasteFromClipboard: PropTypes.func.isRequired, onRedo: PropTypes.func.isRequired, onSendBackward: PropTypes.func.isRequired, onSendForward: PropTypes.func.isRequired, diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 9156b6e6..79803d34 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -5,12 +5,13 @@ 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 {getGuideLayer, getBackgroundGuideLayer} from '../helper/layer'; import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo'; import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order'; import {groupSelection, ungroupSelection} from '../helper/group'; -import {getSelectedLeafItems} from '../helper/selection'; +import {clearSelection, getSelectedLeafItems, getSelectedRootItems} from '../helper/selection'; import {resetZoom, zoomOnSelection} from '../helper/view'; import Modes from '../modes/modes'; @@ -35,7 +36,9 @@ class PaintEditor extends React.Component { 'handleGroup', 'handleUngroup', 'canRedo', - 'canUndo' + 'canUndo', + 'handleCopyToClipboard', + 'handlePasteFromClipboard' ]); } componentDidMount () { @@ -98,6 +101,33 @@ class PaintEditor extends React.Component { handleSendToFront () { bringToFront(this.handleUpdateSvg); } + handleCopyToClipboard () { + const selectedItems = getSelectedRootItems(); + if (selectedItems.length > 0) { + const clipboardItems = []; + for (let i = 0; i < selectedItems.length; i++) { + const jsonItem = selectedItems[i].exportJSON({asString: false}); + clipboardItems.push(jsonItem); + } + this.props.setClipboardItems(clipboardItems); + } + } + 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 (item) { + item.selected = true; + } + paper.project.getActiveLayer().addChild(item); + } + this.props.setSelectedItems(); + paper.project.view.update(); + this.handleUpdateSvg(); + } + } canUndo () { return shouldShowUndo(this.props.undoState); } @@ -123,7 +153,9 @@ class PaintEditor extends React.Component { rotationCenterY={this.props.rotationCenterY} svg={this.props.svg} svgId={this.props.svgId} + onCopyToClipboard={this.handleCopyToClipboard} onGroup={this.handleGroup} + onPasteFromClipboard={this.handlePasteFromClipboard} onRedo={this.handleRedo} onSendBackward={this.handleSendBackward} onSendForward={this.handleSendForward} @@ -143,6 +175,7 @@ class PaintEditor extends React.Component { PaintEditor.propTypes = { clearSelectedItems: PropTypes.func.isRequired, + clipboard: PropTypes.arrayOf(PropTypes.array), name: PropTypes.string, onKeyPress: PropTypes.func.isRequired, onRedo: PropTypes.func.isRequired, @@ -151,6 +184,7 @@ PaintEditor.propTypes = { onUpdateSvg: PropTypes.func.isRequired, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, + setClipboardItems: PropTypes.func.isRequired, setSelectedItems: PropTypes.func.isRequired, svg: PropTypes.string, svgId: PropTypes.string, @@ -163,7 +197,8 @@ PaintEditor.propTypes = { const mapStateToProps = state => ({ selectedItems: state.scratchPaint.selectedItems, - undoState: state.scratchPaint.undo + undoState: state.scratchPaint.undo, + clipboard: state.scratchPaint.clipboard }); const mapDispatchToProps = dispatch => ({ onKeyPress: event => { @@ -191,6 +226,9 @@ const mapDispatchToProps = dispatch => ({ }, undoSnapshot: snapshot => { dispatch(undoSnapshot(snapshot)); + }, + setClipboardItems: items => { + dispatch(setClipboardItems(items)); } }); diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 28efc18b..565f23bf 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -157,7 +157,7 @@ class PaperCanvas extends React.Component { PaperCanvas.propTypes = { canvasRef: PropTypes.func, clearUndo: PropTypes.func.isRequired, - mode: PropTypes.instanceOf(Modes), + mode: PropTypes.oneOf(Object.values(Modes)), onUpdateSvg: PropTypes.func.isRequired, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, diff --git a/src/helper/selection.js b/src/helper/selection.js index 9f11b496..a6ad9230 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -394,14 +394,6 @@ const selectRootItem = function () { } }; -const shouldShowIfSelection = function () { - return getSelectedRootItems().length > 0; -}; - -const shouldShowIfSelectionRecursive = function () { - return getSelectedRootItems().length > 0; -}; - const shouldShowSelectAll = function () { return paper.project.getItems({class: paper.PathItem}).length > 0; }; @@ -419,7 +411,5 @@ export { getSelectedRootItems, processRectangularSelection, selectRootItem, - shouldShowIfSelection, - shouldShowIfSelectionRecursive, shouldShowSelectAll }; diff --git a/src/reducers/clipboard.js b/src/reducers/clipboard.js new file mode 100644 index 00000000..bd5a2c3f --- /dev/null +++ b/src/reducers/clipboard.js @@ -0,0 +1,31 @@ +import log from '../log/log'; + +const SET = 'scratch-paint/clipboard/SET'; +const initialState = []; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET: + if (!action.clipboardItems || !(action.clipboardItems instanceof Array) || action.clipboardItems.length === 0) { + log.warn(`Invalid clipboard item format`); + return state; + } + return action.clipboardItems; + default: + return state; + } +}; + +// Action creators ================================== +const setClipboardItems = function (clipboardItems) { + return { + type: SET, + clipboardItems: clipboardItems + }; +}; + +export { + reducer as default, + setClipboardItems +}; diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index ff0fcff3..7bbe8c6f 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -3,6 +3,7 @@ import modeReducer from './modes'; import brushModeReducer from './brush-mode'; import eraserModeReducer from './eraser-mode'; import colorReducer from './color'; +import clipboardReducer from './clipboard'; import hoverReducer from './hover'; import modalsReducer from './modals'; import selectedItemReducer from './selected-items'; @@ -11,8 +12,9 @@ import undoReducer from './undo'; export default combineReducers({ mode: modeReducer, brushMode: brushModeReducer, - eraserMode: eraserModeReducer, color: colorReducer, + clipboard: clipboardReducer, + eraserMode: eraserModeReducer, hoveredItemId: hoverReducer, modals: modalsReducer, selectedItems: selectedItemReducer, diff --git a/test/unit/clipboard-reducer.test.js b/test/unit/clipboard-reducer.test.js new file mode 100644 index 00000000..7428016a --- /dev/null +++ b/test/unit/clipboard-reducer.test.js @@ -0,0 +1,32 @@ +/* eslint-env jest */ +import clipboardReducer from '../../src/reducers/clipboard'; +import {setClipboardItems} from '../../src/reducers/clipboard'; + +test('initialState', () => { + let defaultState; + + expect(clipboardReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); +}); + +test('setClipboardItems', () => { + let defaultState; + + const newSelected1 = ['selected1', 'selected2']; + const newSelected2 = ['selected1', 'selected3']; + expect(clipboardReducer(defaultState /* state */, setClipboardItems(newSelected1) /* action */)) + .toEqual(newSelected1); + expect(clipboardReducer(newSelected1, setClipboardItems(newSelected2) /* action */)) + .toEqual(newSelected2); +}); + +test('invalidSetClipboardItems', () => { + const origState = ['selected1', 'selected2']; + const nothingSelected = []; + + expect(clipboardReducer(origState /* state */, setClipboardItems() /* action */)) + .toBe(origState); + expect(clipboardReducer(origState /* state */, setClipboardItems('notAnArray') /* action */)) + .toBe(origState); + expect(clipboardReducer(origState /* state */, setClipboardItems(nothingSelected) /* action */)) + .toBe(origState); +});