diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 906f3bb65..e6d138087 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -6,8 +6,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import VMScratchBlocks from '../lib/blocks'; import VM from 'scratch-vm'; -import BlockType from 'scratch-vm/src/extension-support/block-type'; // :( -import ArgumentType from 'scratch-vm/src/extension-support/argument-type'; // :( import log from '../lib/log.js'; import Prompt from './prompt.jsx'; @@ -19,6 +17,7 @@ import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants'; import DropAreaHOC from '../lib/drop-area-hoc.jsx'; import DragConstants from '../lib/drag-constants'; +import defineDynamicBlock from '../lib/define-dynamic-block'; import {connect} from 'react-redux'; import {updateToolbox} from '../reducers/toolbox'; @@ -49,91 +48,6 @@ const DroppableBlocks = DropAreaHOC([ DragConstants.BACKPACK_CODE ])(BlocksComponent); -// TODO: grow this until it can fully replace `_convertForScratchBlocks` in the VM runtime -// :( where should this live? -const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extendedOpcode) => ({ - init: function () { - const blockJson = { - type: extendedOpcode, - inputsInline: true, - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3 - }; - // There is a scratch-blocks / Blockly extension called "scratch_extension" which adjusts the styling of - // blocks to allow for an icon, a feature of Scratch extension blocks. However, Scratch "core" extension - // blocks don't have icons and so they should not use 'scratch_extension'. Adding a scratch-blocks / Blockly - // extension after `jsonInit` isn't fully supported (?), so we decide now whether there will be an icon. - if (staticBlockInfo.blockIconURI || categoryInfo.blockIconURI) { - blockJson.extensions = ['scratch_extension']; - } - this.jsonInit(blockJson); - this.blockInfoText = '{}'; - this.needsBlockInfoUpdate = true; - }, - mutationToDom: function () { - const container = document.createElement('mutation'); - container.setAttribute('blockInfo', this.blockInfoText); - return container; - }, - domToMutation: function (xmlElement) { - const blockInfoText = xmlElement.getAttribute('blockInfo'); - if (!blockInfoText) return; - if (!this.needsBlockInfoUpdate) { - throw new Error('Attempted to update block info twice'); - } - delete this.needsBlockInfoUpdate; - this.blockInfoText = blockInfoText; - const blockInfo = JSON.parse(blockInfoText); - - switch (blockInfo.blockType) { - case BlockType.COMMAND: - case BlockType.CONDITIONAL: - case BlockType.LOOP: - this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE); - this.setPreviousStatement(true); - this.setNextStatement(!blockInfo.isTerminal); - break; - case BlockType.REPORTER: - this.setOutput('String'); // TODO: distinguish number & string here? - this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND); - if (!blockInfo.disableMonitor) { - this.setCheckboxInFlyout(true); - } - break; - case BlockType.BOOLEAN: - this.setOutput('Boolean'); - this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL); - break; - case BlockType.HAT: - case BlockType.EVENT: - this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE); - this.setNextStatement(true); - break; - } - - // Layout block arguments - // TODO handle E/C Blocks - const blockText = blockInfo.text; - const args = []; - let argCount = 0; - const scratchBlocksStyleText = blockText.replace(/\[(.+?)]/g, (match, argName) => { - const arg = blockInfo.arguments[argName]; - switch (arg.type) { - case ArgumentType.STRING: - args.push({type: 'input_value', name: argName}); - break; - case ArgumentType.BOOLEAN: - args.push({type: 'input_value', name: argName, check: 'Boolean'}); - break; - } - return `%${++argCount}`; - }); - this.interpolate_(scratchBlocksStyleText, args); - } -}); - class Blocks extends React.Component { constructor (props) { super(props); diff --git a/src/lib/define-dynamic-block.js b/src/lib/define-dynamic-block.js new file mode 100644 index 000000000..3bc19881c --- /dev/null +++ b/src/lib/define-dynamic-block.js @@ -0,0 +1,90 @@ +// TODO: access `BlockType` and `ArgumentType` without reaching into VM +// Should we move these into a new extension support module or something? +import ArgumentType from 'scratch-vm/src/extension-support/argument-type'; +import BlockType from 'scratch-vm/src/extension-support/block-type'; + +// TODO: grow this until it can fully replace `_convertForScratchBlocks` in the VM runtime +const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extendedOpcode) => ({ + init: function () { + const blockJson = { + type: extendedOpcode, + inputsInline: true, + category: categoryInfo.name, + colour: categoryInfo.color1, + colourSecondary: categoryInfo.color2, + colourTertiary: categoryInfo.color3 + }; + // There is a scratch-blocks / Blockly extension called "scratch_extension" which adjusts the styling of + // blocks to allow for an icon, a feature of Scratch extension blocks. However, Scratch "core" extension + // blocks don't have icons and so they should not use 'scratch_extension'. Adding a scratch-blocks / Blockly + // extension after `jsonInit` isn't fully supported (?), so we decide now whether there will be an icon. + if (staticBlockInfo.blockIconURI || categoryInfo.blockIconURI) { + blockJson.extensions = ['scratch_extension']; + } + this.jsonInit(blockJson); + this.blockInfoText = '{}'; + this.needsBlockInfoUpdate = true; + }, + mutationToDom: function () { + const container = document.createElement('mutation'); + container.setAttribute('blockInfo', this.blockInfoText); + return container; + }, + domToMutation: function (xmlElement) { + const blockInfoText = xmlElement.getAttribute('blockInfo'); + if (!blockInfoText) return; + if (!this.needsBlockInfoUpdate) { + throw new Error('Attempted to update block info twice'); + } + delete this.needsBlockInfoUpdate; + this.blockInfoText = blockInfoText; + const blockInfo = JSON.parse(blockInfoText); + + switch (blockInfo.blockType) { + case BlockType.COMMAND: + case BlockType.CONDITIONAL: + case BlockType.LOOP: + this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE); + this.setPreviousStatement(true); + this.setNextStatement(!blockInfo.isTerminal); + break; + case BlockType.REPORTER: + this.setOutput('String'); // TODO: distinguish number & string here? + this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND); + if (!blockInfo.disableMonitor) { + this.setCheckboxInFlyout(true); + } + break; + case BlockType.BOOLEAN: + this.setOutput('Boolean'); + this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL); + break; + case BlockType.HAT: + case BlockType.EVENT: + this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE); + this.setNextStatement(true); + break; + } + + // Layout block arguments + // TODO handle E/C Blocks + const blockText = blockInfo.text; + const args = []; + let argCount = 0; + const scratchBlocksStyleText = blockText.replace(/\[(.+?)]/g, (match, argName) => { + const arg = blockInfo.arguments[argName]; + switch (arg.type) { + case ArgumentType.STRING: + args.push({type: 'input_value', name: argName}); + break; + case ArgumentType.BOOLEAN: + args.push({type: 'input_value', name: argName, check: 'Boolean'}); + break; + } + return `%${++argCount}`; + }); + this.interpolate_(scratchBlocksStyleText, args); + } +}); + +export default defineDynamicBlock;