diff --git a/package.json b/package.json index b0564c305de9ac9bdcdd2100c794a7e97dbdb30b..905e371b9dbec7ffbe04970c91393d1682a2487f 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "scratch-render": "0.1.0-prerelease.20190605151415", "scratch-storage": "1.3.2", "scratch-svg-renderer": "0.2.0-prerelease.20190523193400", - "scratch-vm": "0.2.0-prerelease.20190619042313", + "scratch-vm": "0.2.0-prerelease.20190610152034", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0", diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index fb01ce15d5dd321908d8fc38960e7b3ff1d65244..d79c7e9c72bc867b96d77c42f3a2382c4a95c421 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -17,7 +17,6 @@ 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'; @@ -394,50 +393,20 @@ class Blocks extends React.Component { // workspace to be 'undone' here. this.workspace.clearUndo(); } - handleExtensionAdded (categoryInfo) { - const defineBlocks = blockInfoArray => { - if (blockInfoArray && blockInfoArray.length > 0) { - const staticBlocksJson = []; - const dynamicBlocksInfo = []; - blockInfoArray.forEach(blockInfo => { - if (blockInfo.info && blockInfo.info.isDynamic) { - dynamicBlocksInfo.push(blockInfo); - } else if (blockInfo.json) { - staticBlocksJson.push(blockInfo.json); - } - // otherwise it's a non-block entry such as '---' - }); - - this.ScratchBlocks.defineBlocksWithJsonArray(staticBlocksJson); - dynamicBlocksInfo.forEach(blockInfo => { - // This is creating the block factory / constructor -- NOT a specific instance of the block. - // The factory should only know static info about the block: the category info and the opcode. - // Anything else will be picked up from the XML attached to the block instance. - const extendedOpcode = `${categoryInfo.id}_${blockInfo.info.opcode}`; - const blockDefinition = - defineDynamicBlock(this.ScratchBlocks, categoryInfo, blockInfo, extendedOpcode); - this.ScratchBlocks.Blocks[extendedOpcode] = blockDefinition; - }); - } - }; - - // scratch-blocks implements a menu or custom field as a special kind of block ("shadow" block) - // these actually define blocks and MUST run regardless of the UI state - defineBlocks( - Object.getOwnPropertyNames(categoryInfo.customFieldTypes) - .map(fieldTypeName => categoryInfo.customFieldTypes[fieldTypeName].scratchBlocksDefinition)); - defineBlocks(categoryInfo.menus); - defineBlocks(categoryInfo.blocks); + handleExtensionAdded (blocksInfo) { + // select JSON from each block info object then reject the pseudo-blocks which don't have JSON, like separators + // this actually defines blocks and MUST run regardless of the UI state + this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x)); - // Update the toolbox with new blocks if possible + // Update the toolbox with new blocks const toolboxXML = this.getToolboxXML(); if (toolboxXML) { this.props.updateToolboxState(toolboxXML); } } - handleBlocksInfoUpdate (categoryInfo) { + handleBlocksInfoUpdate (blocksInfo) { // @todo Later we should replace this to avoid all the warnings from redefining blocks. - this.handleExtensionAdded(categoryInfo); + this.handleExtensionAdded(blocksInfo); } handleCategorySelected (categoryId) { const extension = extensionData.find(ext => ext.extensionId === categoryId); diff --git a/src/lib/define-dynamic-block.js b/src/lib/define-dynamic-block.js deleted file mode 100644 index 33f1c8732cda4fdb88ae31a366dfcadf51ef3eca..0000000000000000000000000000000000000000 --- a/src/lib/define-dynamic-block.js +++ /dev/null @@ -1,108 +0,0 @@ -// 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) { - 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; diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 79cf35cc25e872931383a3dc3d0ceb955664812a..3443e05d2c0b5f5b763f28a5a146c10940251c5b 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -716,16 +716,13 @@ const xmlClose = '</xml>'; /** * @param {!boolean} isStage - Whether the toolbox is for a stage-type target. * @param {?string} targetId - The current editing target - * @param {?Array.<object>} categoriesXML - optional array of `{id,xml}` for categories. This can include both core - * and other extensions: core extensions will be placed in the normal Scratch order; others will go at the bottom. - * @property {string} id - the extension / category ID. - * @property {string} xml - the `<category>...</category>` XML for this extension / category. + * @param {?string} categoriesXML - null for default toolbox, or an XML string with <category> elements. * @param {?string} costumeName - The name of the default selected costume dropdown. * @param {?string} backdropName - The name of the default selected backdrop dropdown. * @param {?string} soundName - The name of the default selected sound dropdown. * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox. */ -const makeToolboxXML = function (isStage, targetId, categoriesXML = [], +const makeToolboxXML = function (isStage, targetId, categoriesXML, costumeName = '', backdropName = '', soundName = '') { const gap = [categorySeparator]; @@ -733,41 +730,21 @@ const makeToolboxXML = function (isStage, targetId, categoriesXML = [], backdropName = xmlEscape(backdropName); soundName = xmlEscape(soundName); - categoriesXML = categoriesXML.slice(); - const moveCategory = categoryId => { - const index = categoriesXML.findIndex(categoryInfo => categoryInfo.id === categoryId); - if (index >= 0) { - // remove the category from categoriesXML and return its XML - const [categoryInfo] = categoriesXML.splice(index, 1); - return categoryInfo.xml; - } - // return `undefined` - }; - const motionXML = moveCategory('motion') || motion(isStage, targetId); - const looksXML = moveCategory('looks') || looks(isStage, targetId, costumeName, backdropName); - const soundXML = moveCategory('sound') || sound(isStage, targetId, soundName); - const eventsXML = moveCategory('event') || events(isStage, targetId); - const controlXML = moveCategory('control') || control(isStage, targetId); - const sensingXML = moveCategory('sensing') || sensing(isStage, targetId); - const operatorsXML = moveCategory('operators') || operators(isStage, targetId); - const variablesXML = moveCategory('data') || variables(isStage, targetId); - const myBlocksXML = moveCategory('procedures') || myBlocks(isStage, targetId); - const everything = [ xmlOpen, - motionXML, gap, - looksXML, gap, - soundXML, gap, - eventsXML, gap, - controlXML, gap, - sensingXML, gap, - operatorsXML, gap, - variablesXML, gap, - myBlocksXML + motion(isStage, targetId), gap, + looks(isStage, targetId, costumeName, backdropName), gap, + sound(isStage, targetId, soundName), gap, + events(isStage, targetId), gap, + control(isStage, targetId), gap, + sensing(isStage, targetId), gap, + operators(isStage, targetId), gap, + variables(isStage, targetId), gap, + myBlocks(isStage, targetId) ]; - for (const extensionCategory of categoriesXML) { - everything.push(gap, extensionCategory.xml); + if (categoriesXML) { + everything.push(gap, categoriesXML); } everything.push(xmlClose); diff --git a/test/unit/util/define-dynamic-block.test.js b/test/unit/util/define-dynamic-block.test.js deleted file mode 100644 index 257e5529befa67b3163edfefcea230ec72c346b2..0000000000000000000000000000000000000000 --- a/test/unit/util/define-dynamic-block.test.js +++ /dev/null @@ -1,199 +0,0 @@ -import defineDynamicBlock from '../../../src/lib/define-dynamic-block'; - -import BlockType from 'scratch-vm/src/extension-support/block-type'; - -const MockScratchBlocks = { - OUTPUT_SHAPE_HEXAGONAL: 1, - OUTPUT_SHAPE_ROUND: 2, - OUTPUT_SHAPE_SQUARE: 3 -}; - -const categoryInfo = { - name: 'test category', - color1: '#111', - color2: '#222', - color3: '#333' -}; - -const penIconURI = 'data:image/svg+xml;base64,fake_pen_icon_svg_base64_data'; - -const testBlockInfo = { - commandWithIcon: { - blockType: BlockType.COMMAND, - blockIconURI: penIconURI, - text: 'command with icon' - }, - commandWithoutIcon: { - blockType: BlockType.COMMAND, - text: 'command without icon' - }, - terminalCommand: { - blockType: BlockType.COMMAND, - isTerminal: true, - text: 'terminal command' - }, - reporter: { - blockType: BlockType.REPORTER, - text: 'reporter' - }, - boolean: { - blockType: BlockType.BOOLEAN, - text: 'Boolean' - }, - hat: { - blockType: BlockType.HAT, - text: 'hat' - } -}; - -// similar to goog.mixin from the Closure library -const mixin = function (target, source) { - for (const x in source) { - target[x] = source[x]; - } -}; - -class MockBlock { - constructor (blockInfo, extendedOpcode) { - // mimic Closure-style inheritance by mixing in `defineDynamicBlock` output as this instance's prototype - // see also the `Blockly.Block` constructor - const prototype = defineDynamicBlock(MockScratchBlocks, categoryInfo, blockInfo, extendedOpcode); - mixin(this, prototype); - this.init(); - - // bootstrap the mutation<->DOM cycle - this.blockInfoText = JSON.stringify(blockInfo); - const xmlElement = this.mutationToDom(); - - // parse blockInfo from XML to fill dynamic properties - this.domToMutation(xmlElement); - } - - jsonInit (json) { - this.result = Object.assign({}, json); - } - interpolate_ () { - // TODO: add tests for this? - } - setCheckboxInFlyout (isEnabled) { - this.result.checkboxInFlyout_ = isEnabled; - } - setOutput (isEnabled) { - this.result.outputConnection = isEnabled; // Blockly calls `makeConnection_` here - } - setOutputShape (outputShape) { - this.result.outputShape_ = outputShape; - } - setNextStatement (isEnabled) { - this.result.nextConnection = isEnabled; // Blockly calls `makeConnection_` here - } - setPreviousStatement (isEnabled) { - this.result.previousConnection = isEnabled; // Blockly calls `makeConnection_` here - } -} - -describe('defineDynamicBlock', () => { - test('is a function', () => { - expect(typeof defineDynamicBlock).toBe('function'); - }); - test('can define a command block with an icon', () => { - const extendedOpcode = 'test.commandWithIcon'; - const block = new MockBlock(testBlockInfo.commandWithIcon, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - extensions: ['scratch_extension'], - inputsInline: true, - nextConnection: true, - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE, - previousConnection: true, - type: extendedOpcode - }); - }); - test('can define a command block without an icon', () => { - const extendedOpcode = 'test.commandWithoutIcon'; - const block = new MockBlock(testBlockInfo.commandWithoutIcon, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - nextConnection: true, - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE, - previousConnection: true, - type: extendedOpcode - }); - }); - test('can define a terminal command', () => { - const extendedOpcode = 'test.terminal'; - const block = new MockBlock(testBlockInfo.terminalCommand, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - nextConnection: false, // terminal - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE, - previousConnection: true, - type: extendedOpcode - }); - }); - test('can define a reporter', () => { - const extendedOpcode = 'test.reporter'; - const block = new MockBlock(testBlockInfo.reporter, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - checkboxInFlyout_: true, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - // nextConnection: undefined, // reporter - outputConnection: true, // reporter - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_ROUND, // reporter - // previousConnection: undefined, // reporter - type: extendedOpcode - }); - }); - test('can define a Boolean', () => { - const extendedOpcode = 'test.boolean'; - const block = new MockBlock(testBlockInfo.boolean, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - // checkboxInFlyout_: undefined, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - // nextConnection: undefined, // reporter - outputConnection: true, // reporter - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_HEXAGONAL, // Boolean - // previousConnection: undefined, // reporter - type: extendedOpcode - }); - }); - test('can define a hat', () => { - const extendedOpcode = 'test.hat'; - const block = new MockBlock(testBlockInfo.hat, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - nextConnection: true, - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE, - // previousConnection: undefined, // hat - type: extendedOpcode - }); - }); -});