diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index c7c9f1e09..c7db7c835 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -147,6 +147,7 @@ class Blocks extends React.Component { } componentWillUnmount() { this.detachVM() + this.onTargetsUpdate.cancel() this.workspace.dispose() clearTimeout(this.toolboxUpdateTimeout) } diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index 44e2fd13f..fd3e03203 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -299,22 +299,24 @@ class Stage extends React.Component { } this.setState({ mouseDownTimeoutId: null }) } - drawDragCanvas(drawableData) { - const { data, width, height, x, y } = drawableData - this.dragCanvas.width = width - this.dragCanvas.height = height - // Need to convert uint8array from WebGL readPixels into Uint8ClampedArray - // for ImageData constructor. Shares underlying buffer, so it is fast. - const imageData = new ImageData( - new Uint8ClampedArray(data.buffer), - width, - height - ) + drawDragCanvas(drawableData, x, y) { + const { + imageData, + x: boundsX, + y: boundsY, + width: boundsWidth, + height: boundsHeight, + } = drawableData + this.dragCanvas.width = imageData.width + this.dragCanvas.height = imageData.height + // On high-DPI devices the extracted image is larger than its CSS size + this.dragCanvas.style.width = `${boundsWidth}px` + this.dragCanvas.style.height = `${boundsHeight}px` this.dragCanvas.getContext('2d').putImageData(imageData, 0, 0) - // Position so that pick location is at (0, 0) so that positionDragCanvas() + // Position so that pick location is at (0, 0) so that positionDragCanvas() // can use translation to move to mouse position smoothly. - this.dragCanvas.style.left = `${-x}px` - this.dragCanvas.style.top = `${-y}px` + this.dragCanvas.style.left = `${boundsX - x}px` + this.dragCanvas.style.top = `${boundsY - y}px` this.dragCanvas.style.display = 'block' } clearDragCanvas() { @@ -334,28 +336,30 @@ class Stage extends React.Component { if (drawableId === null) { return } - const drawableData = this.renderer.extractDrawable(drawableId, x, y) const targetId = this.props.vm.getTargetIdForDrawableId(drawableId) if (targetId === null) { return } + const target = this.props.vm.runtime.getTargetById(targetId) // Only start drags on non-draggable targets in editor drag mode - if (!this.props.useEditorDragStyle) { - const target = this.props.vm.runtime.getTargetById(targetId) - if (!target.draggable) { - return - } + if (!(this.props.useEditorDragStyle || target.draggable)) { + return } + const [scratchMouseX, scratchMouseY] = this.getScratchCoords(x, y) + const offsetX = target.x - scratchMouseX + const offsetY = -(target.y + scratchMouseY) + this.props.vm.startDrag(targetId) this.setState({ isDragging: true, dragId: targetId, - dragOffset: drawableData.scratchOffset, + dragOffset: [offsetX, offsetY], }) if (this.props.useEditorDragStyle) { - this.drawDragCanvas(drawableData) + const drawableData = this.renderer.extractDrawableScreenSpace(drawableId) + this.drawDragCanvas(drawableData, x, y) this.positionDragCanvas(x, y) this.props.vm.postSpriteInfo({ visible: false }) } diff --git a/src/lib/project-save-hoc.jsx b/src/lib/project-save-hoc.jsx index f2d02a968..17d95948d 100644 --- a/src/lib/project-save-hoc.jsx +++ b/src/lib/project-save-hoc.jsx @@ -1,4 +1,5 @@ import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' import React from 'react' import PropTypes from 'prop-types' import { @@ -225,14 +226,16 @@ const ProjectSaveHOC = (WrappedComponent) => { dispatch: PropTypes.func, } - return connect((state) => ({ + const mapStateToProps = (state, ownProps) => ({ isEduGame: state.scratchGui.eduLayer.enabled, projectId: state.scratchGui.project.id, projectName: state.scratchGui.project.name, - routeProject: (state.router.params || {}).projectId || null, + routeProject: ownProps.match.params.projectId || null, userId: state.scratchGui.project.userId, vm: state.scratchGui.vm, - }))(ProjectSaveComponent) + }) + + return withRouter(connect(mapStateToProps)(ProjectSaveComponent)) } export default ProjectSaveHOC diff --git a/src/lib/unsaved-project-blocker.jsx b/src/lib/unsaved-project-blocker.jsx index 81c82a903..b1e27d044 100644 --- a/src/lib/unsaved-project-blocker.jsx +++ b/src/lib/unsaved-project-blocker.jsx @@ -3,44 +3,56 @@ import { connect } from 'react-redux' import PropTypes from 'prop-types' import { history } from './app-state-hoc.jsx' +const LEAVE_MESSAGE = + 'Möchtest du die Seite wirklich verlassen? Dein Projekt geht ohne Speichern verloren!' + const UnsavedProjectBlockerHOC = (WrappedComponent) => { class UnsavedProjectBlocker extends React.Component { + constructor(props) { + super(props) + this.unblockRouter = null + } stopBrowserNavigation(event) { event.preventDefault() event.returnValue = '' } - routerBlock() { - const unblock = history.block(() => { - return window.confirm( - 'Möchtest du die Seite wirklich verlassen? Dein Projekt geht ohne Speichern verloren!' - ) - }) - - return () => { - unblock() + block() { + if (this.unblockRouter) { + return + } + window.addEventListener('beforeunload', this.stopBrowserNavigation) + // history v4 shows a string prompt via window.confirm + this.unblockRouter = history.block(LEAVE_MESSAGE) + } + unblock() { + window.removeEventListener('beforeunload', this.stopBrowserNavigation) + if (this.unblockRouter) { + this.unblockRouter() + this.unblockRouter = null } } + syncBlocker() { + if (process.env.NODE_ENV !== 'production' || typeof window !== 'object') { + return + } + if (this.props.isProjectUnsaved) { + this.block() + } else { + this.unblock() + } + } + componentDidMount() { + this.syncBlocker() + } componentDidUpdate(oldProps) { if (this.props.isProjectUnsaved !== oldProps.isProjectUnsaved) { - if ( - process.env.NODE_ENV === 'production' && - typeof window === 'object' - ) { - if (this.props.isProjectUnsaved) { - // Warn before navigating away - window.addEventListener('beforeunload', this.stopBrowserNavigation) - this.routerBlock() - } else { - window.removeEventListener( - 'beforeunload', - this.stopBrowserNavigation - ) - } - } + this.syncBlocker() } } componentWillUnmount() { - window.removeEventListener('beforeunload', this.stopBrowserNavigation) + if (typeof window === 'object') { + this.unblock() + } } render() { const {