Skip to content
Snippets Groups Projects
Commit 6ee0a343 authored by Paul Kaplan's avatar Paul Kaplan
Browse files

Make costume, sound and sprite tiles draggable.

parent 1377e7b8
No related branches found
No related tags found
No related merge requests found
@import "../../css/units.css";
@import "../../css/colors.css";
.drag-layer {
position: fixed;
pointer-events: none;
z-index: 1000; /* Above everything */
left: 0;
top: 0;
width: 100%;
height: 100%
}
.image-wrapper {
/* Absolute allows wrapper to snuggly fit image */
position: absolute;
}
.image {
max-width: 80px;
/* Center the dragging image on the given position */
margin-left: -50%;
margin-top: -50%;
/* Use the same drop shadow as stage dragging */
filter: drop-shadow(5px 5px 5px $ui-black-transparent);
}
import React from 'react';
import PropTypes from 'prop-types';
import styles from './drag-layer.css';
/* eslint no-confusing-arrow: ["error", {"allowParens": true}] */
const DragLayer = ({dragging, img, currentOffset}) => (dragging ? (
<div className={styles.dragLayer}>
<div
className={styles.imageWrapper}
style={{
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`
}}
>
<img
className={styles.image}
src={img}
/>
</div>
</div>
) : null);
DragLayer.propTypes = {
currentOffset: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired
}),
dragging: PropTypes.bool.isRequired,
img: PropTypes.string
};
export default DragLayer;
......@@ -21,6 +21,7 @@ import ImportModal from '../../containers/import-modal.jsx';
import WebGlModal from '../../containers/webgl-modal.jsx';
import TipsLibrary from '../../containers/tips-library.jsx';
import Cards from '../../containers/cards.jsx';
import DragLayer from '../../containers/drag-layer.jsx';
import styles from './gui.css';
import addExtensionIcon from './icon--extensions.svg';
......@@ -215,6 +216,7 @@ const GUIComponent = props => {
</Box>
</Box>
</Box>
<DragLayer />
</Box>
);
};
......
......@@ -20,8 +20,11 @@ const SpriteSelectorItem = props => (
}),
onClick: props.onClick,
onMouseEnter: props.onMouseEnter,
onMouseLeave: props.onMouseLeave
onMouseLeave: props.onMouseLeave,
onMouseDown: props.onMouseDown,
onTouchStart: props.onMouseDown
}}
disable={props.dragging}
id={`${props.name}-${contextMenuId}`}
>
{(props.selected && props.onDeleteButtonClick) ? (
......@@ -77,11 +80,13 @@ SpriteSelectorItem.propTypes = {
className: PropTypes.string,
costumeURL: PropTypes.string,
details: PropTypes.string,
dragging: PropTypes.bool,
name: PropTypes.string.isRequired,
number: PropTypes.number,
onClick: PropTypes.func,
onDeleteButtonClick: PropTypes.func,
onDuplicateButtonClick: PropTypes.func,
onMouseDown: PropTypes.func,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
selected: PropTypes.bool.isRequired
......
import {connect} from 'react-redux';
import DragLayer from '../components/drag-layer/drag-layer.jsx';
const mapStateToProps = state => ({
dragging: state.scratchGui.assetDrag.dragging,
currentOffset: state.scratchGui.assetDrag.currentOffset,
img: state.scratchGui.assetDrag.img
});
export default connect(mapStateToProps)(DragLayer);
......@@ -4,9 +4,13 @@ import React from 'react';
import {connect} from 'react-redux';
import {setHoveredSprite} from '../reducers/hovered-target';
import {updateAssetDrag} from '../reducers/asset-drag';
import {getEventXY} from '../lib/touch-utils';
import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx';
const dragThreshold = 3; // Same as the block drag threshold
class SpriteSelectorItem extends React.Component {
constructor (props) {
super(props);
......@@ -15,9 +19,44 @@ class SpriteSelectorItem extends React.Component {
'handleDelete',
'handleDuplicate',
'handleMouseEnter',
'handleMouseLeave'
'handleMouseLeave',
'handleMouseDown',
'handleMouseMove',
'handleMouseUp'
]);
}
handleMouseUp () {
this.initialOffset = null;
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('touchend', this.handleMouseUp);
window.removeEventListener('touchmove', this.handleMouseMove);
this.props.onDrag({
img: null,
currentOffset: null,
dragging: false
});
}
handleMouseMove (e) {
const currentOffset = getEventXY(e);
const dx = currentOffset.x - this.initialOffset.x;
const dy = currentOffset.y - this.initialOffset.y;
if (Math.sqrt((dx * dx) + (dy * dy)) > dragThreshold) {
this.props.onDrag({
img: this.props.costumeURL,
currentOffset: currentOffset,
dragging: true
});
}
e.preventDefault();
}
handleMouseDown (e) {
this.initialOffset = getEventXY(e);
window.addEventListener('mouseup', this.handleMouseUp);
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('touchend', this.handleMouseUp);
window.addEventListener('touchmove', this.handleMouseMove);
}
handleClick (e) {
e.preventDefault();
this.props.onClick(this.props.id);
......@@ -57,6 +96,7 @@ class SpriteSelectorItem extends React.Component {
onClick={this.handleClick}
onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null}
onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null}
onMouseDown={this.handleMouseDown}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
{...props}
......@@ -73,6 +113,7 @@ SpriteSelectorItem.propTypes = {
name: PropTypes.string,
onClick: PropTypes.func,
onDeleteButtonClick: PropTypes.func,
onDrag: PropTypes.func.isRequired,
onDuplicateButtonClick: PropTypes.func,
receivedBlocks: PropTypes.bool.isRequired,
selected: PropTypes.bool
......@@ -80,13 +121,15 @@ SpriteSelectorItem.propTypes = {
const mapStateToProps = (state, {assetId, costumeURL, id}) => ({
costumeURL: costumeURL || (assetId && state.scratchGui.vm.runtime.storage.get(assetId).encodeDataURI()),
dragging: state.scratchGui.assetDrag.dragging,
receivedBlocks: state.scratchGui.hoveredTarget.receivedBlocks &&
state.scratchGui.hoveredTarget.sprite === id
});
const mapDispatchToProps = dispatch => ({
dispatchSetHoveredSprite: spriteId => {
dispatch(setHoveredSprite(spriteId));
}
},
onDrag: data => dispatch(updateAssetDrag(data))
});
export default connect(
......
const DRAG_UPDATE = 'scratch-gui/asset-drag/DRAG_UPDATE';
const initialState = {
dragging: false,
currentOffset: null,
img: null
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case DRAG_UPDATE:
return Object.assign({}, state, action.state);
default:
return state;
}
};
const updateAssetDrag = function (state) {
return {
type: DRAG_UPDATE,
state: state
};
};
export {
reducer as default,
initialState as assetDragInitialState,
updateAssetDrag
};
import {applyMiddleware, compose, combineReducers} from 'redux';
import assetDragReducer, {assetDragInitialState} from './asset-drag';
import cardsReducer, {cardsInitialState} from './cards';
import colorPickerReducer, {colorPickerInitialState} from './color-picker';
import customProceduresReducer, {customProceduresInitialState} from './custom-procedures';
......@@ -19,6 +20,7 @@ import throttle from 'redux-throttle';
const guiMiddleware = compose(applyMiddleware(throttle(300, {leading: true, trailing: true})));
const guiInitialState = {
assetDrag: assetDragInitialState,
blockDrag: blockDragInitialState,
cards: cardsInitialState,
colorPicker: colorPickerInitialState,
......@@ -57,6 +59,7 @@ const initFullScreen = function (currentState) {
);
};
const guiReducer = combineReducers({
assetDrag: assetDragReducer,
blockDrag: blockDragReducer,
cards: cardsReducer,
colorPicker: colorPickerReducer,
......
......@@ -37,8 +37,9 @@ describe('SpriteSelectorItem Container', () => {
beforeEach(() => {
store = mockStore({scratchGui: {
hoveredTarget: {receivedBlocks: false, sprite: null}}
});
hoveredTarget: {receivedBlocks: false, sprite: null},
assetDrag: {dragging: false}
}});
className = 'ponies';
costumeURL = 'https://scratch.mit.edu/foo/bar/pony';
id = 1337;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment