diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx index 0f7b0b89f7ea02a0f0e4fd2b6472debc75f82784..04cdc293798d3d2217f9eaed7299505db951c3a1 100644 --- a/src/components/backpack/backpack.jsx +++ b/src/components/backpack/backpack.jsx @@ -17,7 +17,7 @@ const dragTypeMap = { sprite: DragConstants.BACKPACK_SPRITE }; -const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, onToggle, onDelete}) => ( +const Backpack = ({containerRef, contents, dragOver, error, expanded, loading, onToggle, onDelete}) => ( <div className={styles.backpackContainer}> <div className={styles.backpackHeader} @@ -45,7 +45,7 @@ const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, on {expanded ? ( <div className={styles.backpackList} - ref={dropAreaRef} + ref={containerRef} > {error ? ( <div className={styles.statusMessage}> @@ -104,6 +104,7 @@ const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, on ); Backpack.propTypes = { + containerRef: PropTypes.func, contents: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, thumbnailUrl: PropTypes.string, @@ -111,7 +112,6 @@ Backpack.propTypes = { name: PropTypes.string })), dragOver: PropTypes.bool, - dropAreaRef: PropTypes.func, error: PropTypes.bool, expanded: PropTypes.bool, loading: PropTypes.bool, diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx index 50161bf8cbbfbc389ddcbd196201badfea408687..06894ae0187b9ff98c50b475b58246325f5a2098 100644 --- a/src/components/stage-selector/stage-selector.jsx +++ b/src/components/stage-selector/stage-selector.jsx @@ -40,6 +40,8 @@ const messages = defineMessages({ const StageSelector = props => { const { backdropCount, + containerRef, + dragOver, fileInputRef, intl, selected, @@ -60,9 +62,10 @@ const StageSelector = props => { <Box className={classNames(styles.stageSelector, { [styles.isSelected]: selected, - [styles.raised]: raised, + [styles.raised]: raised || dragOver, [styles.receivedBlocks]: receivedBlocks })} + componentRef={containerRef} onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} @@ -128,6 +131,8 @@ const StageSelector = props => { StageSelector.propTypes = { backdropCount: PropTypes.number.isRequired, + containerRef: PropTypes.func, + dragOver: PropTypes.bool, fileInputRef: PropTypes.func, intl: intlShape.isRequired, onBackdropFileUpload: PropTypes.func, diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index aedf2f35da1c84187386aae3e5132d7fbfad9372..e7303c145fb969ff5e226f68ff51b768f3b2da5b 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -11,11 +11,15 @@ import { spritePayload } from '../lib/backpack-api'; import DragConstants from '../lib/drag-constants'; +import DropAreaHOC from '../lib/drop-area-hoc.jsx'; import {connect} from 'react-redux'; import storage from '../lib/storage'; import VM from 'scratch-vm'; +const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND, DragConstants.SPRITE]; +const DroppableBackpack = DropAreaHOC(dragTypes)(BackpackComponent); + class Backpack extends React.Component { constructor (props) { super(props); @@ -23,8 +27,7 @@ class Backpack extends React.Component { 'handleDrop', 'handleToggle', 'handleDelete', - 'refreshContents', - 'setRef' + 'refreshContents' ]); this.state = { dragOver: false, @@ -46,29 +49,6 @@ class Backpack extends React.Component { storage._hasAddedBackpackSource = true; } } - componentWillReceiveProps (newProps) { - const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND, DragConstants.SPRITE]; - // If `dragging` becomes true, record the drop area rectangle - if (newProps.dragInfo.dragging && !this.props.dragInfo.dragging) { - this.dropAreaRect = this.ref && this.ref.getBoundingClientRect(); - // If `dragging` becomes false, call the drop handler - } else if (!newProps.dragInfo.dragging && this.props.dragInfo.dragging && this.state.dragOver) { - this.handleDrop(this.props.dragInfo); - this.setState({dragOver: false}); - } - - // If a drag is in progress (currentOffset) and it matches the relevant drag types, - // test if the drag is within the drop area rect and set the state accordingly. - if (this.dropAreaRect && newProps.dragInfo.currentOffset && dragTypes.includes(newProps.dragInfo.dragType)) { - const {x, y} = newProps.dragInfo.currentOffset; - const {top, right, bottom, left} = this.dropAreaRect; - if (x > left && x < right && y > top && y < bottom) { - this.setState({dragOver: true}); - } else { - this.setState({dragOver: false}); - } - } - } handleToggle () { const newState = !this.state.expanded; this.setState({expanded: newState, offset: 0}); @@ -126,19 +106,15 @@ class Backpack extends React.Component { }); } } - setRef (ref) { - this.ref = ref; - } render () { return ( - <BackpackComponent + <DroppableBackpack contents={this.state.contents} - dragOver={this.state.dragOver} - dropAreaRef={this.setRef} error={this.state.error} expanded={this.state.expanded} loading={this.state.loading} onDelete={this.handleDelete} + onDrop={this.handleDrop} onToggle={this.props.host ? this.handleToggle : null} /> ); @@ -146,15 +122,6 @@ class Backpack extends React.Component { } Backpack.propTypes = { - dragInfo: PropTypes.shape({ - currentOffset: PropTypes.shape({ - x: PropTypes.number, - y: PropTypes.number - }), - dragType: PropTypes.string, - dragging: PropTypes.bool, - index: PropTypes.number - }), host: PropTypes.string, token: PropTypes.string, username: PropTypes.string, @@ -181,7 +148,6 @@ const getTokenAndUsername = state => { const mapStateToProps = state => Object.assign( { - dragInfo: state.scratchGui.assetDrag, vm: state.scratchGui.vm }, getTokenAndUsername(state) diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx index 99c233c7d8e7ea7b8a28f497274fe85da966948d..aeff4f2ad963de136829471b67d2dd551f45a0e4 100644 --- a/src/containers/stage-selector.jsx +++ b/src/containers/stage-selector.jsx @@ -7,6 +7,8 @@ import {connect} from 'react-redux'; import {openBackdropLibrary} from '../reducers/modals'; import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab'; import {setHoveredSprite} from '../reducers/hovered-target'; +import DragConstants from '../lib/drag-constants'; +import DropAreaHOC from '../lib/drop-area-hoc.jsx'; import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx'; @@ -14,6 +16,9 @@ import backdropLibraryContent from '../lib/libraries/backdrops.json'; import costumeLibraryContent from '../lib/libraries/costumes.json'; import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js'; +const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND]; +const DroppableStage = DropAreaHOC(dragTypes)(StageSelectorComponent); + class StageSelector extends React.Component { constructor (props) { super(props); @@ -27,6 +32,7 @@ class StageSelector extends React.Component { 'handleBackdropUpload', 'handleMouseEnter', 'handleMouseLeave', + 'handleDrop', 'setFileInput' ]); } @@ -75,6 +81,13 @@ class StageSelector extends React.Component { handleMouseLeave () { this.props.dispatchSetHoveredSprite(null); } + handleDrop (dragInfo) { + if (dragInfo.dragType === DragConstants.COSTUME) { + this.props.vm.shareCostumeToTarget(dragInfo.index, this.props.id); + } else if (dragInfo.dragType === DragConstants.SOUND) { + this.props.vm.shareSoundToTarget(dragInfo.index, this.props.id); + } + } setFileInput (input) { this.fileInput = input; } @@ -82,16 +95,16 @@ class StageSelector extends React.Component { const componentProps = omit(this.props, [ 'assetId', 'dispatchSetHoveredSprite', 'id', 'onActivateTab', 'onSelect']); return ( - <StageSelectorComponent + <DroppableStage fileInputRef={this.setFileInput} onBackdropFileUpload={this.handleBackdropUpload} onBackdropFileUploadClick={this.handleFileUploadClick} onClick={this.handleClick} + onDrop={this.handleDrop} onEmptyBackdropClick={this.handleEmptyBackdrop} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onSurpriseBackdropClick={this.handleSurpriseBackdrop} - {...componentProps} /> ); diff --git a/src/lib/drop-area-hoc.jsx b/src/lib/drop-area-hoc.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9f44293e88cdaab98dcd80b0a8cdd4fde915b646 --- /dev/null +++ b/src/lib/drop-area-hoc.jsx @@ -0,0 +1,88 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import omit from 'lodash.omit'; +import {connect} from 'react-redux'; + +const DropAreaHOC = function (dragTypes) { + return function (WrappedComponent) { + class DropAreaWrapper extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'setRef' + ]); + + this.state = { + dragOver: false + }; + + this.ref = null; + this.containerBox = null; + } + + componentWillReceiveProps (newProps) { + // If `dragging` becomes true, record the drop area rectangle + if (newProps.dragInfo.dragging && !this.props.dragInfo.dragging) { + this.dropAreaRect = this.ref && this.ref.getBoundingClientRect(); + // If `dragging` becomes false, call the drop handler + } else if (!newProps.dragInfo.dragging && this.props.dragInfo.dragging && this.state.dragOver) { + this.props.onDrop(this.props.dragInfo); + this.setState({dragOver: false}); + } + + // If a drag is in progress (currentOffset) and it matches the relevant drag types, + // test if the drag is within the drop area rect and set the state accordingly. + if (this.dropAreaRect && newProps.dragInfo.currentOffset && + dragTypes.includes(newProps.dragInfo.dragType)) { + const {x, y} = newProps.dragInfo.currentOffset; + const {top, right, bottom, left} = this.dropAreaRect; + if (x > left && x < right && y > top && y < bottom) { + this.setState({dragOver: true}); + } else { + this.setState({dragOver: false}); + } + } + } + setRef (el) { + this.ref = el; + } + render () { + const componentProps = omit(this.props, ['onDrop', 'dragInfo']); + return ( + <WrappedComponent + containerRef={this.setRef} + dragOver={this.state.dragOver} + {...componentProps} + /> + ); + } + } + + DropAreaWrapper.propTypes = { + dragInfo: PropTypes.shape({ + currentOffset: PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number + }), + dragType: PropTypes.string, + dragging: PropTypes.bool, + index: PropTypes.number + }), + onDrop: PropTypes.func + }; + + const mapStateToProps = state => ({ + dragInfo: state.scratchGui.assetDrag + }); + + const mapDispatchToProps = () => ({}); + + return connect( + mapStateToProps, + mapDispatchToProps + )(DropAreaWrapper); + }; +}; + +export default DropAreaHOC;