Spaces:
Runtime error
Runtime error
| // 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'; | |
| /** | |
| * Define a block using extension info which has the ability to dynamically determine (and update) its layout. | |
| * This functionality is used for extension blocks which can change its properties based on different state | |
| * information. For example, the `control_stop` block changes its shape based on which menu item is selected | |
| * and a variable block changes its text to reflect the variable name without using an editable field. | |
| * @param {object} ScratchBlocks - The ScratchBlocks name space. | |
| * @param {object} categoryInfo - Information about this block's extension category, including any menus and icons. | |
| * @param {object} staticBlockInfo - The base block information before any dynamic changes. | |
| * @param {string} extendedOpcode - The opcode for the block (including the extension ID). | |
| */ | |
| // 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']; | |
| } | |
| // initialize the basics of the block, to be overridden & extended later by `domToMutation` | |
| this.jsonInit(blockJson); | |
| // initialize the cached block info used to carry block info from `domToMutation` to `mutationToDom` | |
| this.blockInfoText = '{}'; | |
| // we need a block info update (through `domToMutation`) before we have a completely initialized block | |
| 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(true); | |
| this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND); | |
| if (!blockInfo.disableMonitor) { | |
| this.setCheckboxInFlyout(true); | |
| } | |
| break; | |
| case BlockType.BOOLEAN: | |
| this.setOutput(true); | |
| this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL); | |
| break; | |
| case BlockType.HAT: | |
| case BlockType.EVENT: | |
| this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE); | |
| this.setNextStatement(true); | |
| break; | |
| } | |
| if (blockInfo.color1 || blockInfo.color2 || blockInfo.color3) { | |
| // `setColour` handles undefined parameters by adjusting defined colors | |
| this.setColour(blockInfo.color1, blockInfo.color2, blockInfo.color3); | |
| } | |
| // 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) { | |
| default: // bruh | |
| case ArgumentType.STRING: | |
| args[argName] = { type: 'input_value', name: argName }; | |
| break; | |
| case ArgumentType.BOOLEAN: | |
| args[argName] = { type: 'input_value', name: argName, check: 'Boolean' }; | |
| break; | |
| } | |
| if (arg.menu && !categoryInfo.menuInfo[arg.menu].acceptsReporters) { | |
| args[argName].type = 'field_dropdown'; | |
| args[argName].options = categoryInfo.menuInfo[arg.menu].items; | |
| args[argName].value = categoryInfo.menuInfo[arg.menu].items[0][1]; | |
| } | |
| return `%${++argCount}`; | |
| }); | |
| this.interpolate_(scratchBlocksStyleText, Object.values(args)); | |
| if (this.isInsertionMarker()) return; | |
| for (const name in args) { | |
| if (args[name].type.startsWith('field_')) continue; | |
| const arg = blockInfo.arguments[name]; | |
| const connection = this.getInput(name).connection; | |
| const curBlock = connection.targetConnection?.getParentBlock?.(); | |
| if (curBlock && curBlock.type !== 'text' && !curBlock.type.startsWith('math_')) continue; | |
| if (arg.menu) { | |
| const fieldId = `${categoryInfo.id}_menu_${arg.menu}`; | |
| if (curBlock?.type === fieldId) continue; | |
| if (curBlock) curBlock.dispose(); | |
| const newBlock = this.workspace.newBlock(fieldId); | |
| if (arg.defaultValue) newBlock.getField(arg.menu).setValue(arg.defaultValue); | |
| newBlock.setShadow(true); | |
| newBlock.initSvg(); | |
| newBlock.render(); | |
| continue; | |
| } | |
| switch (arg.type) { | |
| case ArgumentType.STRING: { | |
| if (curBlock?.type === 'text') break; | |
| if (curBlock) curBlock.dispose(); | |
| const newBlock = this.workspace.newBlock('text'); | |
| connection.connect(newBlock.outputConnection); | |
| newBlock.getField('TEXT').setValue(arg.defaultValue ?? ''); | |
| newBlock.setShadow(true); | |
| newBlock.initSvg(); | |
| newBlock.render(); | |
| break; | |
| } | |
| case ArgumentType.NUMBER: { | |
| if (curBlock && !curBlock.type.startsWith('math_')) break; | |
| if (curBlock) curBlock.dispose(); | |
| const newBlock = this.workspace.newBlock('math_number'); | |
| connection.connect(newBlock.outputConnection); | |
| newBlock.getField('NUM').setValue(arg.defaultValue ?? ''); | |
| newBlock.setShadow(true); | |
| newBlock.initSvg(); | |
| newBlock.render(); | |
| break; | |
| } | |
| case ArgumentType.BOOLEAN: { | |
| if (!curBlock) break; | |
| curBlock.dispose(); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| export default defineDynamicBlock; |