Merge pull request #655 from cwillisf/feature/extensions

Feature/extensions
This commit is contained in:
Chris Willis-Ford authored and GitHub committed 2017-09-18 11:03:33 -07:00
commit e79cec4a08
11 files changed
+1024 -3

No files matched your search

+15
View File
@@ -148,3 +148,18 @@
height: $stage-menu-height;
padding: $space;
}
.extension-button {
font-size: 0.55rem;
font-weight: bold;
position: absolute;
bottom: 0.75rem;
left: 1rem;
border: 1px solid lightgray;
background: white;
z-index: 50; /** Force extension button above the ScratchBlocks flyout. */
}
.hidden {
display: none;
}
+23
View File
@@ -12,18 +12,31 @@ import Controls from '../../containers/controls.jsx';
import TargetPane from '../../containers/target-pane.jsx';
import SoundTab from '../../containers/sound-tab.jsx';
import Stage from '../../containers/stage.jsx';
import {FormattedMessage} from 'react-intl';
import Box from '../box/box.jsx';
import IconButton from '../icon-button/icon-button.jsx';
import MenuBar from '../menu-bar/menu-bar.jsx';
import layout from '../../lib/layout-constants.js';
import styles from './gui.css';
import addExtensionIcon from '../sprite-selector/icon--sprite.svg';
const addExtensionMessage = (
<FormattedMessage
defaultMessage="Add Extension"
description="Button to add an extension in the target pane"
id="targetPane.addExtension"
/>
);
const GUIComponent = props => {
const {
basePath,
children,
enableExtensions,
vm,
onAddExtensionClick,
onTabSelect,
tabIndex,
...componentProps
@@ -77,6 +90,14 @@ const GUIComponent = props => {
vm={vm}
/>
</Box>
<IconButton
className={classNames(styles.extensionButton, {
[styles.hidden]: !enableExtensions
})}
img={addExtensionIcon}
title={addExtensionMessage}
onClick={onAddExtensionClick}
/>
</TabPanel>
<TabPanel className={tabClassNames.tabPanel}>
{tabIndex === 1 ? <CostumeTab vm={vm} /> : null}
@@ -115,6 +136,8 @@ const GUIComponent = props => {
GUIComponent.propTypes = {
basePath: PropTypes.string,
children: PropTypes.node,
enableExtensions: PropTypes.bool,
onAddExtensionClick: PropTypes.func,
onTabSelect: PropTypes.func,
tabIndex: PropTypes.number,
vm: PropTypes.instanceOf(VM).isRequired
+57 -2
View File
@@ -1,6 +1,7 @@
import bindAll from 'lodash.bindall';
import debounce from 'lodash.debounce';
import defaultsDeep from 'lodash.defaultsdeep';
import makeToolboxXML from '../lib/make-toolbox-xml';
import PropTypes from 'prop-types';
import React from 'react';
import VMScratchBlocks from '../lib/blocks';
@@ -8,6 +9,9 @@ import VM from 'scratch-vm';
import Prompt from './prompt.jsx';
import BlocksComponent from '../components/blocks/blocks.jsx';
import {connect} from 'react-redux';
import {updateToolbox} from '../reducers/toolbox';
const addFunctionListener = (object, property, callback) => {
const oldFn = object[property];
object[property] = function () {
@@ -31,6 +35,7 @@ class Blocks extends React.Component {
'onScriptGlowOff',
'onBlockGlowOn',
'onBlockGlowOff',
'handleExtensionAdded',
'onTargetsUpdate',
'onVisualReport',
'onWorkspaceUpdate',
@@ -55,9 +60,20 @@ class Blocks extends React.Component {
this.attachVM();
}
shouldComponentUpdate (nextProps, nextState) {
return this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible;
return (
this.state.prompt !== nextState.prompt ||
this.props.isVisible !== nextProps.isVisible ||
this.props.toolboxXML !== nextProps.toolboxXML
);
}
componentDidUpdate (prevProps) {
if (prevProps.toolboxXML !== this.props.toolboxXML) {
const selectedCategoryName = this.workspace.toolbox_.getSelectedItem().name_;
this.workspace.updateToolbox(this.props.toolboxXML);
// Blockly throws if we don't select a category after updating the toolbox.
/** @TODO Find a way to avoid the exception without accessing private properties. */
this.setToolboxSelectedItemByName(selectedCategoryName);
}
if (this.props.isVisible === prevProps.isVisible) {
return;
}
@@ -77,6 +93,20 @@ class Blocks extends React.Component {
this.detachVM();
this.workspace.dispose();
}
/**
* Select a particular category in the toolbox by specifying the category name.
* This is a workaround for a bug: @see {@link componentDidUpdate} above.
* @TODO Remove this or reimplement using only public APIs.
* @param {string} name - the name of the category to select.
*/
setToolboxSelectedItemByName (name) {
const categories = this.workspace.toolbox_.categoryMenu_.categories_;
for (let i = 0; i < categories.length; i++) {
if (categories[i].name_ === name) {
this.workspace.toolbox_.setSelectedItem(categories[i]);
}
}
}
attachVM () {
this.workspace.addChangeListener(this.props.vm.blockListener);
this.flyoutWorkspace = this.workspace
@@ -91,6 +121,7 @@ class Blocks extends React.Component {
this.props.vm.addListener('VISUAL_REPORT', this.onVisualReport);
this.props.vm.addListener('workspaceUpdate', this.onWorkspaceUpdate);
this.props.vm.addListener('targetsUpdate', this.onTargetsUpdate);
this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded);
}
detachVM () {
this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
@@ -100,6 +131,7 @@ class Blocks extends React.Component {
this.props.vm.removeListener('VISUAL_REPORT', this.onVisualReport);
this.props.vm.removeListener('workspaceUpdate', this.onWorkspaceUpdate);
this.props.vm.removeListener('targetsUpdate', this.onTargetsUpdate);
this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded);
}
updateToolboxBlockValue (id, value) {
const block = this.workspace
@@ -167,6 +199,12 @@ class Blocks extends React.Component {
this.workspace.resize();
}
}
handleExtensionAdded (blocksInfo) {
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json));
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
const toolboxXML = makeToolboxXML(dynamicBlocksXML);
this.props.onExtensionAdded(toolboxXML);
}
setBlocks (blocks) {
this.blocks = blocks;
}
@@ -185,6 +223,8 @@ class Blocks extends React.Component {
options, // eslint-disable-line no-unused-vars
vm, // eslint-disable-line no-unused-vars
isVisible, // eslint-disable-line no-unused-vars
onExtensionAdded, // eslint-disable-line no-unused-vars
toolboxXML, // eslint-disable-line no-unused-vars
...props
} = this.props;
return (
@@ -209,6 +249,7 @@ class Blocks extends React.Component {
Blocks.propTypes = {
isVisible: PropTypes.bool,
onExtensionAdded: PropTypes.func,
options: PropTypes.shape({
media: PropTypes.string,
zoom: PropTypes.shape({
@@ -230,6 +271,7 @@ Blocks.propTypes = {
}),
comments: PropTypes.bool
}),
toolboxXML: PropTypes.string,
vm: PropTypes.instanceOf(VM).isRequired
};
@@ -264,4 +306,17 @@ Blocks.defaultProps = {
options: Blocks.defaultOptions
};
export default Blocks;
const mapStateToProps = state => ({
toolboxXML: state.toolbox.toolboxXML
});
const mapDispatchToProps = dispatch => ({
onExtensionAdded: toolboxXML => {
dispatch(updateToolbox(toolboxXML));
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Blocks);
+7
View File
@@ -12,6 +12,7 @@ class GUI extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleAddExtensionClick',
'handleTabSelect'
]);
this.state = {tabIndex: 0};
@@ -34,6 +35,10 @@ class GUI extends React.Component {
handleTabSelect (tabIndex) {
this.setState({tabIndex});
}
handleAddExtensionClick () {
/** @TODO open modal for the extension library instead */
this.props.vm.extensionManager.loadExtensionURL('static/extensions/example-extension.js');
}
render () {
const {
children,
@@ -43,8 +48,10 @@ class GUI extends React.Component {
} = this.props;
return (
<GUIComponent
enableExtensions={window.location.search.includes('extensions')}
tabIndex={this.state.tabIndex}
vm={vm}
onAddExtensionClick={this.handleAddExtensionClick}
onTabSelect={this.handleTabSelect}
{...componentProps}
>
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
extends: ['scratch'], // no ES6
env: {
worker: true
},
globals: {
Scratch: true
}
};
@@ -0,0 +1,230 @@
var ExampleExtension = function () {
};
/**
* @return {object} This extension's metadata.
*/
ExampleExtension.prototype.getInfo = function () {
return {
// Required: the machine-readable name of this extension.
// Will be used as the extension's namespace. Must not contain a '.' character.
id: 'someBlocks',
// Optional: the human-readable name of this extension as string.
// This and any other string to be displayed in the Scratch UI may either be
// a string or a call to `intlDefineMessage`; a plain string will not be
// translated whereas a call to `intlDefineMessage` will connect the string
// to the translation map (see below). The `intlDefineMessage` call is
// similar to `defineMessages` from `react-intl` in form, but will actually
// call some extension support code to do its magic. For example, we will
// internally namespace the messages such that two extensions could have
// messages with the same ID without colliding.
// See also: https://github.com/yahoo/react-intl/wiki/API#definemessages
name: 'Some Blocks',
// Optional: URI for an icon for this extension. Data URI OK.
// If not present, use a generic icon.
// TODO: what file types are OK? All web images? Just PNG?
iconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DE' +
'UIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==',
// Optional: Link to documentation content for this extension.
// If not present, offer no link.
docsURI: 'https://....',
// Required: the list of blocks implemented by this extension,
// in the order intended for display.
blocks: [
{
opcode: 'example-noop',
blockType: Scratch.BlockType.COMMAND,
blockAllThreads: false,
text: 'do nothing',
func: 'noop'
},
{
opcode: 'example-conditional',
blockType: Scratch.BlockType.CONDITIONAL,
branchCount: 4,
isTerminal: true,
blockAllThreads: false,
text: 'choose [BRANCH]',
arguments: {
BRANCH: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 1
}
},
func: 'noop'
},
{
// Required: the machine-readable name of this operation.
// This will appear in project JSON. Must not contain a '.' character.
opcode: 'myReporter', // becomes 'someBlocks.myReporter'
// Required: the kind of block we're defining, from a predefined list:
// 'command' - a normal command block, like "move {} steps"
// 'reporter' - returns a value, like "direction"
// 'Boolean' - same as 'reporter' but returns a Boolean value
// 'hat' - starts a stack if its value is truthy
// 'conditional' - control flow, like "if {}" or "repeat {}"
// A 'conditional' block may return the one-based index of a branch
// to run, or it may return zero/falsy to run no branch. Each time a
// child branch finishes, the block is called again. This is only a
// slight change to the current model for control flow blocks, and is
// also compatible with returning true/false for an "if" or "repeat"
// block.
// TODO: Consider Blockly-like nextStatement, previousStatement, and
// output attributes as an alternative. Those are more flexible, but
// allow bad combinations.
blockType: Scratch.BlockType.REPORTER,
// Required for conditional blocks, ignored for others: the number of
// child branches this block controls. An "if" or "repeat" block would
// specify a branch count of 1; an "if-else" block would specify a
// branch count of 2.
// TODO: should we support dynamic branch count for "switch"-likes?
branchCount: 0,
// Optional, default false: whether or not this block ends a stack.
// The "forever" and "stop all" blocks would specify true here.
isTerminal: true,
// Optional, default false: whether or not to block all threads while
// this block is busy. This is for things like the "touching color"
// block in compatibility mode, and is only needed if the VM runs in a
// worker. We might even consider omitting it from extension docs...
blockAllThreads: false,
// Required: the human-readable text on this block, including argument
// placeholders. Argument placeholders should be in [MACRO_CASE] and
// must be [ENCLOSED_WITHIN_SQUARE_BRACKETS].
text: 'letter [LETTER_NUM] of [TEXT]',
// Required: describe each argument.
// Note that this is an array: the order of arguments will be used
arguments: {
// Required: the ID of the argument, which will be the name in the
// args object passed to the implementation function.
LETTER_NUM: {
// Required: type of the argument / shape of the block input
type: Scratch.ArgumentType.NUMBER,
// Optional: the default value of the argument
defaultValue: 1
},
// Required: the ID of the argument, which will be the name in the
// args object passed to the implementation function.
TEXT: {
// Required: type of the argument / shape of the block input
type: Scratch.ArgumentType.STRING,
// Optional: the default value of the argument
defaultValue: 'text'
}
},
// Optional: a string naming the function implementing this block.
// If this is omitted, use the opcode string.
func: 'myReporter',
// Optional: list of target types for which this block should appear.
// If absent, assume it applies to all builtin targets -- that is:
// ['sprite', 'stage']
filter: ['someBlocks.wedo2', 'sprite', 'stage']
},
{
opcode: 'example-Boolean',
blockType: Scratch.BlockType.BOOLEAN,
text: 'return true',
func: 'returnTrue'
},
{
opcode: 'example-hat',
blockType: Scratch.BlockType.HAT,
text: 'after forever',
func: 'returnFalse'
},
{
// Another block...
}
],
// Optional: define extension-specific menus here.
menus: {
// Required: an identifier for this menu, unique within this extension.
menuA: [
// Static menu: list items which should appear in the menu.
{
// Required: the value of the menu item when it is chosen.
value: 'itemId1',
// Optional: the human-readable label for this item.
// Use `value` as the text if this is absent.
text: 'Item One'
},
// The simplest form of a list item is a string which will be used as
// both value and text.
'itemId2'
],
// Dynamic menu: a string naming a function which returns an array as above.
// Called each time the menu is opened.
menuB: 'getItemsForMenuB'
},
// Optional: translations
translation_map: {
de: {
'extensionName': 'Einige Blöcke',
'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]',
'myReporter.TEXT_default': 'Text',
'menuA_item1': 'Artikel eins',
// Dynamic menus can be translated too
'menuB_example': 'Beispiel',
// This message contains ICU placeholders (see `myReporter()` below)
'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.'
},
it: {
// ...
}
},
// Optional: list new target type(s) provided by this extension.
targetTypes: [
'wedo2', // automatically transformed to 'someBlocks.wedo2'
'speech' // automatically transformed to 'someBlocks.speech'
]
};
};
/**
* Implement myReporter.
* @param {object} args - the block's arguments.
* @property {number} LETTER_NUM - the string value of the argument.
* @property {string} TEXT - the string value of the argument.
* @returns {string} a string which includes the block argument value.
*/
ExampleExtension.prototype.myReporter = function (args) {
// Note: this implementation is not Unicode-clean; it's just here as an example.
const result = args.TEXT.charAt(args.LETTER_NUM);
return ['Letter ', args.LETTER_NUM, ' of ', args.TEXT, ' is ', result, '.'].join('');
};
ExampleExtension.prototype.noop = function () {
};
ExampleExtension.prototype.returnTrue = function () {
return true;
};
ExampleExtension.prototype.returnFalse = function () {
return false;
};
Scratch.extensions.register(new ExampleExtension());
+640
View File
@@ -0,0 +1,640 @@
const separator = '<sep gap="45"/>';
const motion = `
<category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
<block type="motion_movesteps">
<value name="STEPS">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_turnright">
<value name="DEGREES">
<shadow type="math_number">
<field name="NUM">15</field>
</shadow>
</value>
</block>
<block type="motion_turnleft">
<value name="DEGREES">
<shadow type="math_number">
<field name="NUM">15</field>
</shadow>
</value>
</block>
<block type="motion_pointindirection">
<value name="DIRECTION">
<shadow type="math_angle">
<field name="NUM">90</field>
</shadow>
</value>
</block>
<block type="motion_pointtowards">
<value name="TOWARDS">
<shadow type="motion_pointtowards_menu">
</shadow>
</value>
</block>
<block type="motion_gotoxy">
<value name="X">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
<value name="Y">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_goto">
<value name="TO">
<shadow type="motion_goto_menu">
</shadow>
</value>
</block>
<block type="motion_glidesecstoxy">
<value name="SECS">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="X">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
<value name="Y">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_changexby">
<value name="DX">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_setx">
<value name="X">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_changeyby">
<value name="DY">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_sety">
<value name="Y">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_ifonedgebounce"/>
<block type="motion_setrotationstyle"/>
<block type="motion_xposition"/>
<block type="motion_yposition"/>
<block type="motion_direction"/>
</category>
`;
const looks = `
<category name="Looks" colour="#9966FF" secondaryColour="#774DCB">
<block type="looks_show"/>
<block type="looks_hide"/>
<block type="looks_switchcostumeto">
<value name="COSTUME">
<shadow type="looks_costume"/>
</value>
</block>
<block type="looks_nextcostume"/>
<block type="looks_nextbackdrop"/>
<block type="looks_switchbackdropto">
<value name="BACKDROP">
<shadow type="looks_backdrops"/>
</value>
</block>
<block type="looks_switchbackdroptoandwait">
<value name="BACKDROP">
<shadow type="looks_backdrops"/>
</value>
</block>
<block type="looks_changeeffectby">
<value name="CHANGE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="looks_seteffectto">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="looks_cleargraphiceffects"/>
<block type="looks_changesizeby">
<value name="CHANGE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="looks_setsizeto">
<value name="SIZE">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="looks_gotofront"/>
<block type="looks_gobacklayers">
<value name="NUM">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="looks_costumeorder"/>
<block type="looks_backdroporder"/>
<block type="looks_backdropname"/>
<block type="looks_size"/>
</category>
`;
const sound = `
<category name="Sound" colour="#D65CD6" secondaryColour="#BD42BD">
<block type="sound_play">
<value name="SOUND_MENU">
<shadow type="sound_sounds_menu"/>
</value>
</block>
<block type="sound_playuntildone">
<value name="SOUND_MENU">
<shadow type="sound_sounds_menu"/>
</value>
</block>
<block type="sound_stopallsounds"/>
<block type="sound_playdrumforbeats">
<value name="DRUM">
<shadow type="sound_drums_menu"/>
</value>
<value name="BEATS">
<shadow type="math_number">
<field name="NUM">0.25</field>
</shadow>
</value>
</block>
<block type="sound_restforbeats">
<value name="BEATS">
<shadow type="math_number">
<field name="NUM">0.25</field>
</shadow>
</value>
</block>
<block type="sound_playnoteforbeats">
<value name="NOTE">
<shadow type="math_number">
<field name="NUM">60</field>
</shadow>
</value>
<value name="BEATS">
<shadow type="math_number">
<field name="NUM">0.5</field>
</shadow>
</value>
</block>
<block type="sound_setinstrumentto">
<value name="INSTRUMENT">
<shadow type="sound_instruments_menu"/>
</value>
</block>
<block type="sound_changeeffectby">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="sound_seteffectto">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="sound_cleareffects"/>
<block type="sound_changevolumeby">
<value name="VOLUME">
<shadow type="math_number">
<field name="NUM">-10</field>
</shadow>
</value>
</block>
<block type="sound_setvolumeto">
<value name="VOLUME">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="sound_volume"/>
<block type="sound_changetempoby">
<value name="TEMPO">
<shadow type="math_number">
<field name="NUM">20</field>
</shadow>
</value>
</block>
<block type="sound_settempotobpm">
<value name="TEMPO">
<shadow type="math_number">
<field name="NUM">60</field>
</shadow>
</value>
</block>
<block type="sound_tempo"/>
</category>
`;
const pen = `
<category name="Pen" colour="#00B295" secondaryColour="#0B8E69">
<block type="pen_clear"/>
<block type="pen_stamp"/>
<block type="pen_pendown"/>
<block type="pen_penup"/>
<block type="pen_setpencolortocolor">
<value name="COLOR">
<shadow type="colour_picker">
</shadow>
</value>
</block>
<block type="pen_changepencolorby">
<value name="COLOR">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="pen_setpencolortonum">
<value name="COLOR">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="pen_changepenshadeby">
<value name="SHADE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="pen_setpenshadeto">
<value name="SHADE">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
<block type="pen_changepensizeby">
<value name="SIZE">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="pen_setpensizeto">
<value name="SIZE">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
</category>
`;
const events = `
<category name="Events" colour="#FFD500" secondaryColour="#CC9900">
<block type="event_whenflagclicked"/>
<block type="event_whenkeypressed">
</block>
<block type="event_whenthisspriteclicked"/>
<block type="event_whenbackdropswitchesto">
</block>
<block type="event_whengreaterthan">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="event_whenbroadcastreceived">
</block>
<block type="event_broadcast">
<value name="BROADCAST_OPTION">
<shadow type="event_broadcast_menu"/>
</value>
</block>
<block type="event_broadcastandwait">
<value name="BROADCAST_OPTION">
<shadow type="event_broadcast_menu"/>
</value>
</block>
</category>
`;
const control = `
<category name="Control" colour="#FFAB19" secondaryColour="#CF8B17">
<block type="control_wait">
<value name="DURATION">
<shadow type="math_positive_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="control_repeat">
<value name="TIMES">
<shadow type="math_whole_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="control_forever"/>
<block type="control_if"/>
<block type="control_if_else"/>
<block type="control_wait_until"/>
<block type="control_repeat_until"/>
<block type="control_stop"/>
<block type="control_start_as_clone"/>
<block type="control_create_clone_of">
<value name="CLONE_OPTION">
<shadow type="control_create_clone_of_menu"/>
</value>
</block>
<block type="control_delete_this_clone"/>
</category>
`;
const sensing = `
<category name="Sensing" colour="#4CBFE6" secondaryColour="#2E8EB8">
<block type="sensing_touchingobject">
<value name="TOUCHINGOBJECTMENU">
<shadow type="sensing_touchingobjectmenu"/>
</value>
</block>
<block type="sensing_touchingcolor">
<value name="COLOR">
<shadow type="colour_picker"/>
</value>
</block>
<block type="sensing_coloristouchingcolor">
<value name="COLOR">
<shadow type="colour_picker"/>
</value>
<value name="COLOR2">
<shadow type="colour_picker"/>
</value>
</block>
<block type="sensing_distanceto">
<value name="DISTANCETOMENU">
<shadow type="sensing_distancetomenu"/>
</value>
</block>
<block type="sensing_keypressed">
<value name="KEY_OPTION">
<shadow type="sensing_keyoptions"/>
</value>
</block>
<block type="sensing_mousedown"/>
<block type="sensing_mousex"/>
<block type="sensing_mousey"/>
<block type="sensing_loudness"/>
<block type="sensing_timer"/>
<block type="sensing_resettimer"/>
<block type="sensing_of">
<value name="PROPERTY">
<shadow type="sensing_of_property_menu"/>
</value>
<value name="OBJECT">
<shadow type="sensing_of_object_menu"/>
</value>
</block>
<block type="sensing_current">
<value name="CURRENTMENU">
<shadow type="sensing_currentmenu"/>
</value>
</block>
<block type="sensing_dayssince2000"/>
</category>
`;
const operators = `
<category name="Operators" colour="#40BF4A" secondaryColour="#389438">
<block type="operator_add">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_subtract">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_multiply">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_divide">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_random">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="operator_lt">
<value name="OPERAND1">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
<value name="OPERAND2">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
</block>
<block type="operator_equals">
<value name="OPERAND1">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
<value name="OPERAND2">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
</block>
<block type="operator_gt">
<value name="OPERAND1">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
<value name="OPERAND2">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
</block>
<block type="operator_and"/>
<block type="operator_or"/>
<block type="operator_not"/>
<block type="operator_join">
<value name="STRING1">
<shadow type="text">
<field name="TEXT">hello</field>
</shadow>
</value>
<value name="STRING2">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
<block type="operator_letter_of">
<value name="LETTER">
<shadow type="math_whole_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="STRING">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
<block type="operator_length">
<value name="STRING">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
<block type="operator_mod">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_round">
<value name="NUM">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_mathop">
<value name="NUM">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
</category>
`;
const data = `
<category name="Data" colour="#FF8C1A" secondaryColour="#DB6E00" custom="VARIABLE">
</category>
`;
const xmlOpen = '<xml style="display: none">';
const xmlClose = '</xml>';
/**
* @param {string?} categoriesXML - null for default toolbox, or an XML string with <category> elements.
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
*/
const makeToolboxXML = function (categoriesXML) {
const gap = [separator];
const everything = [
xmlOpen,
motion, gap,
looks, gap,
sound, gap,
pen, gap,
data, gap,
events, gap,
control, gap,
sensing, gap,
operators
];
if (categoriesXML) {
everything.push(gap, categoriesXML);
}
everything.push(xmlClose);
return everything.join('\n');
};
export default makeToolboxXML;
+2
View File
@@ -3,6 +3,7 @@ import intlReducer from './intl';
import modalReducer from './modals';
import monitorReducer from './monitors';
import targetReducer from './targets';
import toolboxReducer from './toolbox';
import vmReducer from './vm';
@@ -11,5 +12,6 @@ export default combineReducers({
modals: modalReducer,
monitors: monitorReducer,
targets: targetReducer,
toolbox: toolboxReducer,
vm: vmReducer
});
+31
View File
@@ -0,0 +1,31 @@
const UPDATE_TOOLBOX = 'scratch-gui/toolbox/UPDATE_TOOLBOX';
import makeToolboxXML from '../lib/make-toolbox-xml';
const initialState = {
toolboxXML: makeToolboxXML()
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case UPDATE_TOOLBOX:
return Object.assign({}, state, {
toolboxXML: action.toolboxXML
});
default:
return state;
}
};
const updateToolbox = function (toolboxXML) {
return {
type: UPDATE_TOOLBOX,
toolboxXML: toolboxXML
};
};
export {
reducer as default,
updateToolbox
};
+1 -1
View File
@@ -33,7 +33,7 @@ class SeleniumHelper {
}
clickText (text) {
return this.clickXpath(`//*[contains(text(), '${text}')]`);
return this.clickXpath(`//body//*[contains(text(), '${text}')]`);
}
clickButton (text) {
+9
View File
@@ -99,6 +99,15 @@ module.exports = {
new CopyWebpackPlugin([{
from: 'node_modules/scratch-blocks/media',
to: 'static/blocks-media'
}]),
new CopyWebpackPlugin([{
from: 'extensions/**',
to: 'static',
context: 'src/examples'
}]),
new CopyWebpackPlugin([{
from: 'extension-worker.{js,js.map}',
context: 'node_modules/scratch-vm/dist/web'
}])
].concat(process.env.NODE_ENV === 'production' ? [
new webpack.optimize.UglifyJsPlugin({