diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx index 04cdc293798d3d2217f9eaed7299505db951c3a1..f5f4500393bc73d80e888d7b7b90b97ad6cde567 100644 --- a/src/components/backpack/backpack.jsx +++ b/src/components/backpack/backpack.jsx @@ -10,14 +10,26 @@ import styles from './backpack.css'; // TODO make sprite selector item not require onClick const noop = () => {}; -const dragTypeMap = { +const dragTypeMap = { // Keys correspond with the backpack-server item types costume: DragConstants.BACKPACK_COSTUME, sound: DragConstants.BACKPACK_SOUND, - code: DragConstants.BACKPACK_CODE, + script: DragConstants.BACKPACK_CODE, sprite: DragConstants.BACKPACK_SPRITE }; -const Backpack = ({containerRef, contents, dragOver, error, expanded, loading, onToggle, onDelete}) => ( +const Backpack = ({ + blockDragOver, + containerRef, + contents, + dragOver, + error, + expanded, + loading, + onToggle, + onDelete, + onMouseEnter, + onMouseLeave +}) => ( <div className={styles.backpackContainer}> <div className={styles.backpackHeader} @@ -44,8 +56,12 @@ const Backpack = ({containerRef, contents, dragOver, error, expanded, loading, o </div> {expanded ? ( <div - className={styles.backpackList} + className={classNames(styles.backpackList, { + [styles.dragOver]: dragOver || blockDragOver + })} ref={containerRef} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} > {error ? ( <div className={styles.statusMessage}> @@ -66,11 +82,7 @@ const Backpack = ({containerRef, contents, dragOver, error, expanded, loading, o </div> ) : ( contents.length > 0 ? ( - <div - className={classNames(styles.backpackListInner, { - [styles.dragOver]: dragOver - })} - > + <div className={styles.backpackListInner}> {contents.map(item => ( <SpriteSelectorItem className={styles.backpackItem} @@ -104,6 +116,7 @@ const Backpack = ({containerRef, contents, dragOver, error, expanded, loading, o ); Backpack.propTypes = { + blockDragOver: PropTypes.bool, containerRef: PropTypes.func, contents: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, @@ -116,10 +129,13 @@ Backpack.propTypes = { expanded: PropTypes.bool, loading: PropTypes.bool, onDelete: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, onToggle: PropTypes.func }; Backpack.defaultProps = { + blockDragOver: false, contents: [], dragOver: false, expanded: false, diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css index 3d87405893c9268041744d1d5d2aecfa4bd4afc6..e0b07ce26cc7e5053d26a4db770f09f4304b021e 100644 --- a/src/components/blocks/blocks.css +++ b/src/components/blocks/blocks.css @@ -1,6 +1,22 @@ @import "../../css/units.css"; @import "../../css/colors.css"; +.blocks { + height: 100%; +} + +.drag-over:after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.75; + background-color: #8cbcff; + transition: all 0.25s ease; +} + .blocks :global(.injectionDiv){ position: absolute; top: 0; diff --git a/src/components/blocks/blocks.jsx b/src/components/blocks/blocks.jsx index 683da23c1f048eb39d84146cd72959a0593bc8d7..a812b039fb24079de76eac2896415b4286dca845 100644 --- a/src/components/blocks/blocks.jsx +++ b/src/components/blocks/blocks.jsx @@ -1,22 +1,27 @@ import PropTypes from 'prop-types'; +import classNames from 'classnames'; import React from 'react'; import Box from '../box/box.jsx'; import styles from './blocks.css'; const BlocksComponent = props => { const { - componentRef, + containerRef, + dragOver, ...componentProps } = props; return ( <Box - className={styles.blocks} - componentRef={componentRef} + className={classNames(styles.blocks, { + [styles.dragOver]: dragOver + })} {...componentProps} + componentRef={containerRef} /> ); }; BlocksComponent.propTypes = { - componentRef: PropTypes.func + containerRef: PropTypes.func, + dragOver: PropTypes.bool }; export default BlocksComponent; diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index 3f9b2dd5966bd66d0f991e22f28c0be874ff362d..cca9eaab2a62e8f50f4dc6469db8fee9498d5c15 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -8,7 +8,8 @@ import { deleteBackpackObject, soundPayload, costumePayload, - spritePayload + spritePayload, + codePayload } from '../lib/backpack-api'; import DragConstants from '../lib/drag-constants'; import DropAreaHOC from '../lib/drop-area-hoc.jsx'; @@ -28,10 +29,18 @@ class Backpack extends React.Component { 'handleToggle', 'handleDelete', 'getBackpackAssetURL', - 'refreshContents' + 'refreshContents', + 'handleMouseEnter', + 'handleMouseLeave', + 'handleBlockDragEnd', + 'handleBlockDragUpdate' ]); this.state = { - dragOver: false, + // While the DroppableHOC manages drop interactions for asset tiles, + // we still need to micromanage drops coming from the block workspace. + // TODO this may be refactorable with the share-the-love logic in SpriteSelectorItem + blockDragOutsideWorkspace: false, + blockDragOverBackpack: false, error: false, offset: 0, itemsPerPage: 20, @@ -50,12 +59,23 @@ class Backpack extends React.Component { storage._hasAddedBackpackSource = true; } } + componentDidMount () { + this.props.vm.addListener('BLOCK_DRAG_END', this.handleBlockDragEnd); + this.props.vm.addListener('BLOCK_DRAG_UPDATE', this.handleBlockDragUpdate); + } + componentWillUnmount () { + this.props.vm.removeListener('BLOCK_DRAG_END', this.handleBlockDragEnd); + this.props.vm.removeListener('BLOCK_DRAG_UPDATE', this.handleBlockDragUpdate); + } getBackpackAssetURL (asset) { return `${this.props.host}/${asset.assetId}.${asset.dataFormat}`; } handleToggle () { const newState = !this.state.expanded; - this.setState({expanded: newState, offset: 0}); + this.setState({expanded: newState, offset: 0}, () => { + // Emit resize on window to get blocks to resize + window.dispatchEvent(new Event('resize')); + }); if (newState) { this.refreshContents(); } @@ -72,6 +92,9 @@ class Backpack extends React.Component { case DragConstants.SPRITE: payloader = spritePayload; break; + case DragConstants.CODE: + payloader = codePayload; + break; } if (!payloader) return; @@ -110,15 +133,47 @@ class Backpack extends React.Component { }); } } + handleBlockDragUpdate (isOutsideWorkspace) { + this.setState({ + blockDragOutsideWorkspace: isOutsideWorkspace + }); + } + handleMouseEnter () { + if (this.state.blockDragOutsideWorkspace) { + this.setState({ + blockDragOverBackpack: true + }); + } + } + handleMouseLeave () { + this.setState({ + blockDragOverBackpack: false + }); + } + handleBlockDragEnd (blocks) { + if (this.state.blockDragOverBackpack) { + this.handleDrop({ + dragType: DragConstants.CODE, + payload: blocks + }); + } + this.setState({ + blockDragOverBackpack: false, + blockDragOutsideWorkspace: false + }); + } render () { return ( <DroppableBackpack + blockDragOver={this.state.blockDragOverBackpack} contents={this.state.contents} error={this.state.error} expanded={this.state.expanded} loading={this.state.loading} onDelete={this.handleDelete} onDrop={this.handleDrop} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} onToggle={this.props.host ? this.handleToggle : null} /> ); @@ -152,9 +207,13 @@ const getTokenAndUsername = state => { const mapStateToProps = state => Object.assign( { - vm: state.scratchGui.vm + dragInfo: state.scratchGui.assetDrag, + vm: state.scratchGui.vm, + blockDrag: state.scratchGui.blockDrag }, getTokenAndUsername(state) ); -export default connect(mapStateToProps)(Backpack); +const mapDispatchToProps = () => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(Backpack); diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 73a265be9de2c1407fb29b5291e0b55ae89eb938..89e926d3db7360f097b529863cb6b7ce19077ac8 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -16,6 +16,8 @@ import extensionData from '../lib/libraries/extensions/index.jsx'; import CustomProcedures from './custom-procedures.jsx'; import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants'; +import DropAreaHOC from '../lib/drop-area-hoc.jsx'; +import DragConstants from '../lib/drag-constants'; import {connect} from 'react-redux'; import {updateToolbox} from '../reducers/toolbox'; @@ -38,6 +40,10 @@ const addFunctionListener = (object, property, callback) => { }; }; +const DroppableBlocks = DropAreaHOC([ + DragConstants.BACKPACK_CODE +])(BlocksComponent); + class Blocks extends React.Component { constructor (props) { super(props); @@ -47,6 +53,7 @@ class Blocks extends React.Component { 'detachVM', 'handleCategorySelected', 'handleConnectionModalStart', + 'handleDrop', 'handleStatusButtonUpdate', 'handleOpenSoundRecorder', 'handlePromptStart', @@ -404,6 +411,14 @@ class Blocks extends React.Component { ws.refreshToolboxSelection_(); ws.toolbox_.scrollToCategoryById('myBlocks'); } + handleDrop (dragInfo) { + fetch(dragInfo.payload.bodyUrl) + .then(response => response.json()) + .then(blocks => { + this.props.vm.shareBlocksToTarget(blocks, this.props.vm.editingTarget.id); + this.props.vm.refreshWorkspace(); + }); + } render () { /* eslint-disable no-unused-vars */ const { @@ -427,9 +442,10 @@ class Blocks extends React.Component { } = this.props; /* eslint-enable no-unused-vars */ return ( - <div> - <BlocksComponent + <React.Fragment> + <DroppableBlocks componentRef={this.setBlocks} + onDrop={this.handleDrop} {...props} /> {this.state.prompt ? ( @@ -458,7 +474,7 @@ class Blocks extends React.Component { onRequestClose={this.handleCustomProceduresClose} /> ) : null} - </div> + </React.Fragment> ); } } diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js index 9453f197709fee3e1b06dbcb2c98d90f50ab1e49..4a6f7f6b24ccf2e9fba256fbed97d28ccf5c8aa7 100644 --- a/src/lib/backpack-api.js +++ b/src/lib/backpack-api.js @@ -2,6 +2,7 @@ import xhr from 'xhr'; import costumePayload from './backpack/costume-payload'; import soundPayload from './backpack/sound-payload'; import spritePayload from './backpack/sprite-payload'; +import codePayload from './backpack/code-payload'; const getBackpackContents = ({ host, @@ -78,5 +79,6 @@ export { deleteBackpackObject, costumePayload, soundPayload, - spritePayload + spritePayload, + codePayload }; diff --git a/src/lib/backpack/code-payload.js b/src/lib/backpack/code-payload.js new file mode 100644 index 0000000000000000000000000000000000000000..6c190ea08bba98d173ca26c70eb409984142006d --- /dev/null +++ b/src/lib/backpack/code-payload.js @@ -0,0 +1,16 @@ +import codeThumbnail from './code-thumbnail'; + +const codePayload = code => { + const payload = { + type: 'script', // Needs to match backpack-server type name + name: 'code', // All code currently gets the same name + mime: 'application/json', + body: btoa(JSON.stringify(code)), // Base64 encode the json + thumbnail: codeThumbnail // TODO make code thumbnail dynamic + }; + + // Return a promise to make it consistent with other payload constructors like costume-payload + return new Promise(resolve => resolve(payload)); +}; + +export default codePayload; diff --git a/src/lib/backpack/code-thumbnail.js b/src/lib/backpack/code-thumbnail.js new file mode 100644 index 0000000000000000000000000000000000000000..e657144bcb1c5c12a8fe99c6ffbb51a3234779df --- /dev/null +++ b/src/lib/backpack/code-thumbnail.js @@ -0,0 +1,3 @@ +// image/jpeg base64 encoded code thumbnail image +// eslint-disable-next-line max-len +export default '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'; diff --git a/src/lib/drag-constants.js b/src/lib/drag-constants.js index ce4da5af2a8d362feb8ddc81ff171331e3487e63..2fe13b0b0adb12e4f83b6c9da097efa2fcbc2895 100644 --- a/src/lib/drag-constants.js +++ b/src/lib/drag-constants.js @@ -2,6 +2,7 @@ export default { SOUND: 'SOUND', COSTUME: 'COSTUME', SPRITE: 'SPRITE', + CODE: 'CODE', BACKPACK_SOUND: 'BACKPACK_SOUND', BACKPACK_COSTUME: 'BACKPACK_COSTUME', diff --git a/src/lib/drop-area-hoc.jsx b/src/lib/drop-area-hoc.jsx index 9f44293e88cdaab98dcd80b0a8cdd4fde915b646..a5bfc649923c33f9242e2fc58e33c444529b74ed 100644 --- a/src/lib/drop-area-hoc.jsx +++ b/src/lib/drop-area-hoc.jsx @@ -4,7 +4,34 @@ import React from 'react'; import omit from 'lodash.omit'; import {connect} from 'react-redux'; +/** + * Higher Order Component to give components the ability to react to drag overs + * and drops of objects stored in the assetDrag redux state. + * + * Example: You want to enable MyComponent to receive drops from a drag type + * Wrapped = DropAreaHOC([...dragTypes])( + * <MyComponent /> + * ) + * + * MyComponent now receives 2 new props + * containerRef: a ref that must be set on the container element + * dragOver: boolean if an asset is being dragged above the component + * + * Use the wrapped component: + * <Wrapped onDrop={yourDropHandler} /> + * + * NB: This HOC _only_ works with objects that drag using the assetDrag reducer. + * This _does not_ handle drags for blocks coming from the workspace. + * + * @param {Array.<string>} dragTypes Types to respond to, from DragConstants + * @returns {function} The HOC, specialized for those drag types + */ const DropAreaHOC = function (dragTypes) { + /** + * Return the HOC, specialized for the dragTypes + * @param {React.Component} WrappedComponent component to receive drop behaviors + * @returns {React.Component} component with drag over/drop behavior + */ return function (WrappedComponent) { class DropAreaWrapper extends React.Component { constructor (props) { @@ -46,6 +73,9 @@ const DropAreaHOC = function (dragTypes) { } setRef (el) { this.ref = el; + if (this.props.componentRef) { + this.props.componentRef(this.ref); + } } render () { const componentProps = omit(this.props, ['onDrop', 'dragInfo']); @@ -60,6 +90,7 @@ const DropAreaHOC = function (dragTypes) { } DropAreaWrapper.propTypes = { + componentRef: PropTypes.func, dragInfo: PropTypes.shape({ currentOffset: PropTypes.shape({ x: PropTypes.number,