import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import paper from '@scratch/paper'; import Modes from '../lib/modes'; import log from '../log/log'; import {performSnapshot} from '../helper/undo'; import {undoSnapshot, clearUndoState} from '../reducers/undo'; import {isGroup, ungroupItems} from '../helper/group'; import {setupLayers} from '../helper/layer'; 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'; class PaperCanvas extends React.Component { constructor (props) { super(props); bindAll(this, [ 'setCanvas', 'importSvg', 'handleKeyDown', 'handleWheel', '_ensureClockwise' ]); } componentDidMount () { document.addEventListener('keydown', this.handleKeyDown); paper.setup(this.canvas); // Don't show handles by default paper.settings.handleSize = 0; // Make layers. setupLayers(); if (this.props.svg) { this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY); } else { performSnapshot(this.props.undoSnapshot); } } componentWillReceiveProps (newProps) { if (this.props.svgId === newProps.svgId) return; for (const layer of paper.project.layers) { if (!layer.data.isBackgroundGuideLayer) { layer.removeChildren(); } } 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; const oldCenter = paper.project.view.center.clone(); resetZoom(); this.importSvg(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY); paper.project.view.zoom = oldZoom; paper.project.view.center = oldCenter; paper.project.view.update(); } } componentWillUnmount () { paper.remove(); document.removeEventListener('keydown', this.handleKeyDown); } handleKeyDown (event) { if (event.target instanceof HTMLInputElement) { // Ignore delete if a text input field is focused return; } // Backspace, delete if (event.key === 'Delete' || event.key === 'Backspace') { if (deleteSelection(this.props.mode, this.props.onUpdateSvg)) { this.props.setSelectedItems(); } } } importSvg (svg, rotationCenterX, rotationCenterY) { const paperCanvas = this; // Pre-process SVG to prevent parsing errors (discussion from #213) // 1. Remove newlines and tab characters, chrome will not load urls with them. // https://www.chromestatus.com/feature/5735596811091968 svg = svg.split(/[\n|\r|\t]/).join(''); // 2. Remove svg: namespace on elements. svg = svg.split(/<\s*svg:/).join('<'); svg = svg.split(/<\/\s*svg:/).join(']*>/); if (svgAttrs && svgAttrs[0].indexOf('xmlns=') === -1) { svg = svg.replace( ' ); } } 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.keys(Modes)), onUpdateSvg: PropTypes.func.isRequired, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, setSelectedItems: PropTypes.func.isRequired, svg: PropTypes.string, svgId: PropTypes.string, undoSnapshot: PropTypes.func.isRequired }; const mapStateToProps = state => ({ mode: state.scratchPaint.mode }); const mapDispatchToProps = dispatch => ({ undoSnapshot: snapshot => { dispatch(undoSnapshot(snapshot)); }, clearUndo: () => { dispatch(clearUndoState()); }, setSelectedItems: () => { dispatch(setSelectedItems(getSelectedLeafItems())); }, clearSelectedItems: () => { dispatch(clearSelectedItems()); }, clearHoveredItem: () => { dispatch(clearHoveredItem()); }, clearPasteOffset: () => { dispatch(clearPasteOffset()); } }); export default connect( mapStateToProps, mapDispatchToProps )(PaperCanvas);