From 520e11c0cf630a3b43368a209e86a75e448a9b18 Mon Sep 17 00:00:00 2001 From: Ray Schamp <ray@scratch.mit.edu> Date: Thu, 4 May 2017 17:41:35 -0400 Subject: [PATCH] Add Save and Load buttons Use the de/serialization features of the VM to allow downloading and loading project JSON --- src/components/button/button.css | 3 ++ src/components/button/button.jsx | 30 +++++++++++ src/components/load-button/load-button.css | 3 ++ src/components/load-button/load-button.jsx | 36 +++++++++++++ src/components/menu-bar/menu-bar.jsx | 6 ++- src/containers/load-button.jsx | 55 +++++++++++++++++++ src/containers/save-button.jsx | 62 ++++++++++++++++++++++ 7 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/components/button/button.css create mode 100644 src/components/button/button.jsx create mode 100644 src/components/load-button/load-button.css create mode 100644 src/components/load-button/load-button.jsx create mode 100644 src/containers/load-button.jsx create mode 100644 src/containers/save-button.jsx diff --git a/src/components/button/button.css b/src/components/button/button.css new file mode 100644 index 000000000..44243650f --- /dev/null +++ b/src/components/button/button.css @@ -0,0 +1,3 @@ +.button { + cursor: pointer; +} diff --git a/src/components/button/button.jsx b/src/components/button/button.jsx new file mode 100644 index 000000000..34b485154 --- /dev/null +++ b/src/components/button/button.jsx @@ -0,0 +1,30 @@ +const classNames = require('classnames'); +const PropTypes = require('prop-types'); +const React = require('react'); + +const styles = require('./button.css'); + +const ButtonComponent = ({ + className, + onClick, + children, + ...props +}) => ( + <span + className={classNames( + styles.button, + className + )} + onClick={onClick} + {...props} + > + {children} + </span> +); + +ButtonComponent.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + onClick: PropTypes.func.isRequired +}; +module.exports = ButtonComponent; diff --git a/src/components/load-button/load-button.css b/src/components/load-button/load-button.css new file mode 100644 index 000000000..527718028 --- /dev/null +++ b/src/components/load-button/load-button.css @@ -0,0 +1,3 @@ +.file-input { + display: none; +} diff --git a/src/components/load-button/load-button.jsx b/src/components/load-button/load-button.jsx new file mode 100644 index 000000000..29b2962e5 --- /dev/null +++ b/src/components/load-button/load-button.jsx @@ -0,0 +1,36 @@ +const PropTypes = require('prop-types'); +const React = require('react'); + +const ButtonComponent = require('../button/button.jsx'); + +const styles = require('./load-button.css'); + +const LoadButtonComponent = ({ + inputRef, + onChange, + onClick, + title, + ...props +}) => ( + <span {...props}> + <ButtonComponent onClick={onClick}>{title}</ButtonComponent> + <input + className={styles.fileInput} + ref={inputRef} + type="file" + onChange={onChange} + /> + </span> +); + +LoadButtonComponent.propTypes = { + className: PropTypes.string, + inputRef: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + title: PropTypes.string +}; +LoadButtonComponent.defaultProps = { + title: 'Load' +}; +module.exports = LoadButtonComponent; diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index b34a2e190..4397b5002 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -2,6 +2,9 @@ const classNames = require('classnames'); const React = require('react'); const Box = require('../box/box.jsx'); +const LoadButton = require('../../containers/load-button.jsx'); +const SaveButton = require('../../containers/save-button.jsx'); + const styles = require('./menu-bar.css'); const scratchLogo = require('./scratch-logo.svg'); @@ -18,7 +21,8 @@ const MenuBar = function MenuBar () { src={scratchLogo} /> </div> - <div className={styles.menuItem} >Animation Playtest Prototype</div> + <SaveButton className={styles.menuItem} /> + <LoadButton className={styles.menuItem} /> </Box> ); }; diff --git a/src/containers/load-button.jsx b/src/containers/load-button.jsx new file mode 100644 index 000000000..dcbd4fe01 --- /dev/null +++ b/src/containers/load-button.jsx @@ -0,0 +1,55 @@ +const bindAll = require('lodash.bindall'); +const PropTypes = require('prop-types'); +const React = require('react'); +const {connect} = require('react-redux'); + +const LoadButtonComponent = require('../components/load-button/load-button.jsx'); + +class LoadButton extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'setFileInput', + 'handleChange', + 'handleClick' + ]); + } + handleChange (e) { + const reader = new FileReader(); + reader.onload = () => this.props.loadProject(reader.result); + reader.readAsText(e.target.files[0]); + } + handleClick () { + this.fileInput.click(); + } + setFileInput (input) { + this.fileInput = input; + } + render () { + const { + loadProject, // eslint-disable-line no-unused-vars + ...props + } = this.props; + return ( + <LoadButtonComponent + inputRef={this.setFileInput} + onChange={this.handleChange} + onClick={this.handleClick} + {...props} + /> + ); + } +} + +LoadButton.propTypes = { + loadProject: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + loadProject: state.vm.fromJSON.bind(state.vm) +}); + +module.exports = connect( + mapStateToProps, + () => ({}) // omit dispatch prop +)(LoadButton); diff --git a/src/containers/save-button.jsx b/src/containers/save-button.jsx new file mode 100644 index 000000000..f5e7ab018 --- /dev/null +++ b/src/containers/save-button.jsx @@ -0,0 +1,62 @@ +const bindAll = require('lodash.bindall'); +const PropTypes = require('prop-types'); +const React = require('react'); +const {connect} = require('react-redux'); + +const ButtonComponent = require('../components/button/button.jsx'); + +class SaveButton extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleClick' + ]); + } + handleClick () { + const json = this.props.saveProjectSb3(); + + // Download project data into a file - create link element, + // simulate click on it, and then remove it. + const saveLink = document.createElement('a'); + document.body.appendChild(saveLink); + + const data = new Blob([json], {type: 'text'}); + const url = window.URL.createObjectURL(data); + saveLink.href = url; + + // File name: project-DATE-TIME + const date = new Date(); + const timestamp = `${date.toLocaleDateString()}-${date.toLocaleTimeString()}`; + saveLink.download = `project-${timestamp}.json`; + saveLink.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(saveLink); + } + render () { + const { + saveProjectSb3, // eslint-disable-line no-unused-vars + ...props + } = this.props; + return ( + <ButtonComponent + onClick={this.handleClick} + {...props} + > + Save + </ButtonComponent> + ); + } +} + +SaveButton.propTypes = { + saveProjectSb3: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + saveProjectSb3: state.vm.saveProjectSb3.bind(state.vm) +}); + +module.exports = connect( + mapStateToProps, + () => ({}) // omit dispatch prop +)(SaveButton); -- GitLab