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;