-
Paul Kaplan authoredPaul Kaplan authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
blocks.jsx 8.31 KiB
const bindAll = require('lodash.bindall');
const defaultsDeep = require('lodash.defaultsdeep');
const PropTypes = require('prop-types');
const React = require('react');
const VMScratchBlocks = require('../lib/blocks');
const VM = require('scratch-vm');
const Prompt = require('./prompt.jsx');
const BlocksComponent = require('../components/blocks/blocks.jsx');
const addFunctionListener = (object, property, callback) => {
const oldFn = object[property];
object[property] = function () {
const result = oldFn.apply(this, arguments);
callback.apply(this, result);
return result;
};
};
class Blocks extends React.Component {
constructor (props) {
super(props);
this.ScratchBlocks = VMScratchBlocks(props.vm);
bindAll(this, [
'attachVM',
'detachVM',
'handlePromptStart',
'handlePromptCallback',
'handlePromptClose',
'onScriptGlowOn',
'onScriptGlowOff',
'onBlockGlowOn',
'onBlockGlowOff',
'onVisualReport',
'onWorkspaceUpdate',
'onWorkspaceMetricsChange',
'setBlocks'
]);
this.ScratchBlocks.prompt = this.handlePromptStart;
this.state = {
workspaceMetrics: {},
prompt: null
};
}
componentDidMount () {
const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
// @todo change this when blockly supports UI events
addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange);
addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange);
this.attachVM();
}
shouldComponentUpdate (nextProps, nextState) {
return this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible;
}
componentDidUpdate (prevProps) {
if (this.props.isVisible === prevProps.isVisible) {
return;
}
// @todo hack to resize blockly manually in case resize happened while hidden
if (this.props.isVisible) { // Scripts tab
window.dispatchEvent(new Event('resize'));
this.workspace.setVisible(true);
this.workspace.toolbox_.refreshSelection();
} else {
this.workspace.setVisible(false);
}
}
componentWillUnmount () {
this.detachVM();
this.workspace.dispose();
}
attachVM () {
this.workspace.addChangeListener(this.props.vm.blockListener);
this.flyoutWorkspace = this.workspace
.getFlyout()
.getWorkspace();
this.flyoutWorkspace.addChangeListener(this.props.vm.flyoutBlockListener);
this.flyoutWorkspace.addChangeListener(this.props.vm.monitorBlockListener);
this.props.vm.addListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
this.props.vm.addListener('SCRIPT_GLOW_OFF', this.onScriptGlowOff);
this.props.vm.addListener('BLOCK_GLOW_ON', this.onBlockGlowOn);
this.props.vm.addListener('BLOCK_GLOW_OFF', this.onBlockGlowOff);
this.props.vm.addListener('VISUAL_REPORT', this.onVisualReport);
this.props.vm.addListener('workspaceUpdate', this.onWorkspaceUpdate);
}
detachVM () {
this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
this.props.vm.removeListener('SCRIPT_GLOW_OFF', this.onScriptGlowOff);
this.props.vm.removeListener('BLOCK_GLOW_ON', this.onBlockGlowOn);
this.props.vm.removeListener('BLOCK_GLOW_OFF', this.onBlockGlowOff);
this.props.vm.removeListener('VISUAL_REPORT', this.onVisualReport);
this.props.vm.removeListener('workspaceUpdate', this.onWorkspaceUpdate);
}
onWorkspaceMetricsChange () {
const target = this.props.vm.editingTarget;
if (target && target.id) {
const workspaceMetrics = Object.assign({}, this.state.workspaceMetrics, {
[target.id]: {
scrollX: this.workspace.scrollX,
scrollY: this.workspace.scrollY,
scale: this.workspace.scale
}
});
this.setState({workspaceMetrics});
}
}
onScriptGlowOn (data) {
this.workspace.glowStack(data.id, true);
}
onScriptGlowOff (data) {
this.workspace.glowStack(data.id, false);
}
onBlockGlowOn (data) {
this.workspace.glowBlock(data.id, true);
}
onBlockGlowOff (data) {
this.workspace.glowBlock(data.id, false);
}
onVisualReport (data) {
this.workspace.reportValue(data.id, data.value);
}
onWorkspaceUpdate (data) {
if (this.props.vm.editingTarget && !this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
this.onWorkspaceMetricsChange();
}
this.ScratchBlocks.Events.disable();
this.workspace.clear();
const dom = this.ScratchBlocks.Xml.textToDom(data.xml);
this.ScratchBlocks.Xml.domToWorkspace(dom, this.workspace);
this.ScratchBlocks.Events.enable();
this.workspace.toolbox_.refreshSelection();
if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id];
this.workspace.scrollX = scrollX;
this.workspace.scrollY = scrollY;
this.workspace.scale = scale;
this.workspace.resize();
}
}
setBlocks (blocks) {
this.blocks = blocks;
}
handlePromptStart (message, defaultValue, callback) {
this.setState({prompt: {callback, message, defaultValue}});
}
handlePromptCallback (data) {
this.state.prompt.callback(data);
this.props.vm.createVariable(data);
this.handlePromptClose();
}
handlePromptClose () {
this.setState({prompt: null});
}
render () {
const {
options, // eslint-disable-line no-unused-vars
vm, // eslint-disable-line no-unused-vars
isVisible, // eslint-disable-line no-unused-vars
...props
} = this.props;
return (
<div>
<BlocksComponent
componentRef={this.setBlocks}
{...props}
/>
{this.state.prompt ? (
<Prompt
label={this.state.prompt.message}
placeholder={this.state.prompt.defaultValue}
title="New Variable" // @todo the only prompt is for new variables
onCancel={this.handlePromptClose}
onOk={this.handlePromptCallback}
/>
) : null}
</div>
);
}
}
Blocks.propTypes = {
isVisible: PropTypes.bool.isRequired,
options: PropTypes.shape({
media: PropTypes.string,
zoom: PropTypes.shape({
controls: PropTypes.bool,
wheel: PropTypes.bool,
startScale: PropTypes.number
}),
colours: PropTypes.shape({
workspace: PropTypes.string,
flyout: PropTypes.string,
toolbox: PropTypes.string,
toolboxSelected: PropTypes.string,
scrollbar: PropTypes.string,
scrollbarHover: PropTypes.string,
insertionMarker: PropTypes.string,
insertionMarkerOpacity: PropTypes.number,
fieldShadow: PropTypes.string,
dragShadowOpacity: PropTypes.number
})
}),
vm: PropTypes.instanceOf(VM).isRequired
};
Blocks.defaultOptions = {
zoom: {
controls: true,
wheel: true,
startScale: 0.75
},
grid: {
spacing: 40,
length: 2,
colour: '#ddd'
},
colours: {
workspace: '#F9F9F9',
flyout: '#F9F9F9',
toolbox: '#FFFFFF',
toolboxSelected: '#E9EEF2',
scrollbar: '#CECDCE',
scrollbarHover: '#CECDCE',
insertionMarker: '#000000',
insertionMarkerOpacity: 0.2,
fieldShadow: 'rgba(255, 255, 255, 0.3)',
dragShadowOpacity: 0.6
}
};
Blocks.defaultProps = {
options: Blocks.defaultOptions
};
module.exports = Blocks;