From e0c628df342529312ef6251a0e7da28788616b4d Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 29 Aug 2017 11:41:21 -0700 Subject: [PATCH 01/11] WIP for demo --- .../sprite-selector/sprite-selector.css | 33 +- .../sprite-selector/sprite-selector.jsx | 32 +- src/components/target-pane/target-pane.jsx | 3 + src/containers/blocks.jsx | 49 +- src/containers/target-pane.jsx | 6 + src/lib/make-toolbox-xml.js | 639 ++++++++++++++++++ src/lib/static-blocks.xml | 578 ++++++++++++++++ src/reducers/gui.js | 2 + src/reducers/toolbox.js | 31 + webpack.config.js | 8 + 10 files changed, 1358 insertions(+), 23 deletions(-) create mode 100644 src/lib/make-toolbox-xml.js create mode 100644 src/lib/static-blocks.xml create mode 100644 src/reducers/toolbox.js diff --git a/src/components/sprite-selector/sprite-selector.css b/src/components/sprite-selector/sprite-selector.css index 8c1e2315a..c35c0194b 100644 --- a/src/components/sprite-selector/sprite-selector.css +++ b/src/components/sprite-selector/sprite-selector.css @@ -2,7 +2,7 @@ .sprite-selector { flex-grow: 1; - position: relative; + position: relative; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; margin-right: calc($space / 2); background-color: #f9f9f9; @@ -16,14 +16,14 @@ /* In prep for renaming sprite-selector-item to sprite */ .sprite { - /* + /* Our goal is to fit sprites evenly in a row without leftover space. - Flexbox's `space between` property gets us close, but doesn't flow + Flexbox's `space between` property gets us close, but doesn't flow well when the # of items per row > 1 and less than the max per row. - Solving by explicitly calc'ing the width of each sprite. Setting - `border-box` simplifies things, because content, padding and - border-width all are included in the width, leaving us only to subtract + Solving by explicitly calc'ing the width of each sprite. Setting + `border-box` simplifies things, because content, padding and + border-width all are included in the width, leaving us only to subtract the left + right margins. @todo: make room for the scrollbar @@ -32,7 +32,7 @@ width: calc((100% / $sprites-per-row ) - $space); min-width: 4rem; min-height: 4rem; /* @todo: calc height same as width */ - margin: calc($space / 2); + margin: calc($space / 2); } @@ -41,11 +41,11 @@ Sets the sprite-selector items as a scrollable pane @todo: Safari: pane doesn't stretch to fill height; - @todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and - also introduces a new bug in Chrome when vertically resizing window down, - then back up, introduces white space in the outside the page container. + @todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and + also introduces a new bug in Chrome when vertically resizing window down, + then back up, introduces white space in the outside the page container. */ - height: calc(100% - $sprite-info-height); + height: calc(100% - $sprite-info-height); overflow-y: scroll; } @@ -57,13 +57,16 @@ padding-top: calc($space / 2); padding-left: calc($space / 2); padding-right: calc($space / 2); - padding-bottom: $space; + padding-bottom: $space; +} + +.add-buttons { + position: absolute; + bottom: 0.75rem; + right: 1rem; } .add-button { font-size: 0.55rem; font-weight: bold; - position: absolute; - bottom: 0.75rem; - right: 1rem; } diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx index 399d8062b..e53f52bb1 100644 --- a/src/components/sprite-selector/sprite-selector.jsx +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -10,6 +10,14 @@ import IconButton from '../icon-button/icon-button.jsx'; import styles from './sprite-selector.css'; import spriteIcon from './icon--sprite.svg'; +const addExtensionMessage = ( + +); + const addSpriteMessage = ( - + + + + ); }; @@ -100,6 +119,7 @@ SpriteSelectorComponent.propTypes = { onChangeSpriteX: PropTypes.func, onChangeSpriteY: PropTypes.func, onDeleteSprite: PropTypes.func, + onNewExtensionClick: PropTypes.func, onNewSpriteClick: PropTypes.func, onSelectSprite: PropTypes.func, selectedId: PropTypes.string, diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index a88a994a0..067569a8b 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -32,6 +32,7 @@ const TargetPane = ({ onChangeSpriteX, onChangeSpriteY, onDeleteSprite, + onNewExtensionClick, onNewSpriteClick, onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, @@ -58,6 +59,7 @@ const TargetPane = ({ onChangeSpriteX={onChangeSpriteX} onChangeSpriteY={onChangeSpriteY} onDeleteSprite={onDeleteSprite} + onNewExtensionClick={onNewExtensionClick} onNewSpriteClick={onNewSpriteClick} onSelectSprite={onSelectSprite} /> @@ -127,6 +129,7 @@ TargetPane.propTypes = { onChangeSpriteX: PropTypes.func, onChangeSpriteY: PropTypes.func, onDeleteSprite: PropTypes.func, + onNewExtensionClick: PropTypes.func, onNewSpriteClick: PropTypes.func, onRequestCloseBackdropLibrary: PropTypes.func, onRequestCloseCostumeLibrary: PropTypes.func, diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 63a762765..d3cd98342 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -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', + 'onExtensionWasAdded', 'onTargetsUpdate', 'onVisualReport', 'onWorkspaceUpdate', @@ -55,9 +60,18 @@ 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); + this.setToolboxSelectedItemByName(selectedCategoryName); + } if (this.props.isVisible === prevProps.isVisible) { return; } @@ -77,6 +91,14 @@ class Blocks extends React.Component { this.detachVM(); this.workspace.dispose(); } + 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 +113,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_WAS_ADDED', this.onExtensionWasAdded); } detachVM () { this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); @@ -100,6 +123,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_WAS_ADDED', this.onExtensionWasAdded); } updateToolboxBlockValue (id, value) { const block = this.workspace @@ -167,6 +191,12 @@ class Blocks extends React.Component { this.workspace.resize(); } } + onExtensionWasAdded (blocksInfo) { + this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json)); + const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(); + const toolboxXML = makeToolboxXML(dynamicBlocksXML); + this.props.updateToolbox(toolboxXML); + } setBlocks (blocks) { this.blocks = blocks; } @@ -230,6 +260,8 @@ Blocks.propTypes = { }), comments: PropTypes.bool }), + toolboxXML: PropTypes.string, + updateToolbox: PropTypes.func, vm: PropTypes.instanceOf(VM).isRequired }; @@ -264,4 +296,17 @@ Blocks.defaultProps = { options: Blocks.defaultOptions }; -export default Blocks; +const mapStateToProps = state => ({ + toolboxXML: state.toolbox.toolboxXML +}); + +const mapDispatchToProps = dispatch => ({ + updateToolbox: toolboxXML => { + dispatch(updateToolbox(toolboxXML)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Blocks); diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index 9b70a79d4..b4bc8f5b4 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -24,6 +24,7 @@ class TargetPane extends React.Component { 'handleChangeSpriteX', 'handleChangeSpriteY', 'handleDeleteSprite', + 'handleNewExtensionClick', 'handleSelectSprite' ]); } @@ -48,6 +49,10 @@ class TargetPane extends React.Component { handleDeleteSprite (id) { this.props.vm.deleteSprite(id); } + handleNewExtensionClick () { + /** @TODO: Replace this with `dispatch(openExtensionLibrary());` in `mapDispatchToProps` below */ + this.props.vm.extensionManager.loadExtensionURL('extensions/example-extension.js'); + } handleSelectSprite (id) { this.props.vm.setEditingTarget(id); } @@ -62,6 +67,7 @@ class TargetPane extends React.Component { onChangeSpriteX={this.handleChangeSpriteX} onChangeSpriteY={this.handleChangeSpriteY} onDeleteSprite={this.handleDeleteSprite} + onNewExtensionClick={this.handleNewExtensionClick} onSelectSprite={this.handleSelectSprite} /> ); diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js new file mode 100644 index 000000000..27a917ff0 --- /dev/null +++ b/src/lib/make-toolbox-xml.js @@ -0,0 +1,639 @@ +const separator = ''; + +const motion = [ + '', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 15', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 15', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 90', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 0', + ' ', + ' ', + ' ', + ' ', + ' 0', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 1', + ' ', + ' ', + ' ', + ' ', + ' 0', + ' ', + ' ', + ' ', + ' ', + ' 0', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 0', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 0', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '' +]; + +const looks = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 100', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 1', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '' +]; + +const sound = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 0.25', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 0.25', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 60', + ' ', + ' ', + ' ', + ' ', + ' 0.5', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 100', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' -10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 100', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 20', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 60', + ' ', + ' ', + ' ', + ' ', + '' +]; + +const pen = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 0', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 50', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 1', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 1', + ' ', + ' ', + ' ', + '' +]; + +const events = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '' +]; + +const control = [ + '', + ' ', + ' ', + ' ', + ' 1', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '' +]; + +const sensing = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '' +]; + +const operators = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 1', + ' ', + ' ', + ' ', + ' ', + ' 10', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' hello', + ' ', + ' ', + ' ', + ' ', + ' world', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' 1', + ' ', + ' ', + ' ', + ' ', + ' world', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' world', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '' +]; + +const data = [ + '', + '' +]; + +const xmlOpen = ''; +const xmlClose = ''; + +/** + * @param {string?} categoriesXML - null for default toolbox, or an XML string with elements. + * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox. + */ +const makeToolboxXML = function (categoriesXML) { + const gap = [separator]; + + const everything = [].concat( + 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; diff --git a/src/lib/static-blocks.xml b/src/lib/static-blocks.xml new file mode 100644 index 000000000..2081e5c8a --- /dev/null +++ b/src/lib/static-blocks.xml @@ -0,0 +1,578 @@ + + + + + 10 + + + + + + + 15 + + + + + + + 15 + + + + + + + 90 + + + + + + + + + + + + + 0 + + + + + 0 + + + + + + + + + + + + + 1 + + + + + 0 + + + + + 0 + + + + + + + 10 + + + + + + + 0 + + + + + + + 10 + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + + + + + + + 10 + + + + + + + + 10 + + + + + + + 100 + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.25 + + + + + + + 0.25 + + + + + + + 60 + + + + + 0.5 + + + + + + + + + + + + 10 + + + + + + + 100 + + + + + + + + -10 + + + + + + + 100 + + + + + + + + 20 + + + + + + + 60 + + + + + + + + + + + + + + + + + + + + 10 + + + + + + + 0 + + + + + + + 10 + + + + + + + 50 + + + + + + + 1 + + + + + + + 1 + + + + + + + + + + + + + + + 10 + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + hello + + + + + world + + + + + + + 1 + + + + + world + + + + + + + world + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/reducers/gui.js b/src/reducers/gui.js index c83dd7fca..dfe05dd84 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -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 }); diff --git a/src/reducers/toolbox.js b/src/reducers/toolbox.js new file mode 100644 index 000000000..4b06a807c --- /dev/null +++ b/src/reducers/toolbox.js @@ -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 +}; diff --git a/webpack.config.js b/webpack.config.js index f39fc81a9..eb077f78f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -99,6 +99,14 @@ module.exports = { new CopyWebpackPlugin([{ from: 'node_modules/scratch-blocks/media', to: 'static/blocks-media' + }]), + new CopyWebpackPlugin([{ + from: 'extension-worker.{js,js.map}', + context: 'node_modules/scratch-vm/dist/web' + }]), + new CopyWebpackPlugin([{ + from: 'extensions/**', + context: 'node_modules/scratch-vm/dist/web' }]) ].concat(process.env.NODE_ENV === 'production' ? [ new webpack.optimize.UglifyJsPlugin({ From 3874a498bffebaece0c38da4553d672c14b87838 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 29 Aug 2017 12:30:06 -0700 Subject: [PATCH 02/11] Don't pass invalid props to `BlocksComponent` Also, delete `static-blocks.xml` which shouldn't have been added in the first place. --- src/containers/blocks.jsx | 8 +- src/containers/target-pane.jsx | 1 - src/lib/static-blocks.xml | 578 --------------------------------- 3 files changed, 5 insertions(+), 582 deletions(-) delete mode 100644 src/lib/static-blocks.xml diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index d3cd98342..9f7831e81 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -195,7 +195,7 @@ class Blocks extends React.Component { this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json)); const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(); const toolboxXML = makeToolboxXML(dynamicBlocksXML); - this.props.updateToolbox(toolboxXML); + this.props.sendToolboxUpdate(toolboxXML); } setBlocks (blocks) { this.blocks = blocks; @@ -215,6 +215,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 + toolboxXML, // eslint-disable-line no-unused-vars + sendToolboxUpdate, // eslint-disable-line no-unused-vars ...props } = this.props; return ( @@ -260,8 +262,8 @@ Blocks.propTypes = { }), comments: PropTypes.bool }), + sendToolboxUpdate: PropTypes.func, toolboxXML: PropTypes.string, - updateToolbox: PropTypes.func, vm: PropTypes.instanceOf(VM).isRequired }; @@ -301,7 +303,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - updateToolbox: toolboxXML => { + sendToolboxUpdate: toolboxXML => { dispatch(updateToolbox(toolboxXML)); } }); diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index b4bc8f5b4..7e87bac9a 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -50,7 +50,6 @@ class TargetPane extends React.Component { this.props.vm.deleteSprite(id); } handleNewExtensionClick () { - /** @TODO: Replace this with `dispatch(openExtensionLibrary());` in `mapDispatchToProps` below */ this.props.vm.extensionManager.loadExtensionURL('extensions/example-extension.js'); } handleSelectSprite (id) { diff --git a/src/lib/static-blocks.xml b/src/lib/static-blocks.xml deleted file mode 100644 index 2081e5c8a..000000000 --- a/src/lib/static-blocks.xml +++ /dev/null @@ -1,578 +0,0 @@ - - - - - 10 - - - - - - - 15 - - - - - - - 15 - - - - - - - 90 - - - - - - - - - - - - - 0 - - - - - 0 - - - - - - - - - - - - - 1 - - - - - 0 - - - - - 0 - - - - - - - 10 - - - - - - - 0 - - - - - - - 10 - - - - - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10 - - - - - - - 10 - - - - - - - - 10 - - - - - - - 100 - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 0.25 - - - - - - - 0.25 - - - - - - - 60 - - - - - 0.5 - - - - - - - - - - - - 10 - - - - - - - 100 - - - - - - - - -10 - - - - - - - 100 - - - - - - - - 20 - - - - - - - 60 - - - - - - - - - - - - - - - - - - - - 10 - - - - - - - 0 - - - - - - - 10 - - - - - - - 50 - - - - - - - 1 - - - - - - - 1 - - - - - - - - - - - - - - - 10 - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - 10 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - 10 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - hello - - - - - world - - - - - - - 1 - - - - - world - - - - - - - world - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 2bc11f8d4e137c834b61ddc36a654da532f7c041 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 29 Aug 2017 15:14:56 -0700 Subject: [PATCH 03/11] Move add-extension button to gui.jsx --- src/components/gui/gui.css | 9 +++++ src/components/gui/gui.jsx | 22 +++++++++++++ .../sprite-selector/sprite-selector.css | 33 +++++++++---------- .../sprite-selector/sprite-selector.jsx | 32 ++++-------------- src/components/target-pane/target-pane.jsx | 3 -- src/containers/target-pane.jsx | 5 --- 6 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 6412c9a88..3f5e7061d 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -99,6 +99,7 @@ .blocks-wrapper { flex-grow: 1; position: relative; + z-index: 0; } .stage-and-target-wrapper { @@ -148,3 +149,11 @@ height: $stage-menu-height; padding: $space; } + +.extensionButton { + font-size: 0.55rem; + font-weight: bold; + position: absolute; + bottom: 0.75rem; + left: 14rem; +} diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 1eb0e6505..25f082cef 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -12,12 +12,23 @@ 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 = ( + +); const GUIComponent = props => { const { @@ -45,6 +56,11 @@ const GUIComponent = props => { tabSelected: classNames(tabStyles.reactTabsTabSelected, styles.isSelected) }; + const onAddExtensionClick = () => { + /** @TODO open modal for the extension library instead */ + vm.extensionManager.loadExtensionURL('extensions/example-extension.js'); + }; + return ( { vm={vm} /> + diff --git a/src/components/sprite-selector/sprite-selector.css b/src/components/sprite-selector/sprite-selector.css index c35c0194b..8c1e2315a 100644 --- a/src/components/sprite-selector/sprite-selector.css +++ b/src/components/sprite-selector/sprite-selector.css @@ -2,7 +2,7 @@ .sprite-selector { flex-grow: 1; - position: relative; + position: relative; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; margin-right: calc($space / 2); background-color: #f9f9f9; @@ -16,14 +16,14 @@ /* In prep for renaming sprite-selector-item to sprite */ .sprite { - /* + /* Our goal is to fit sprites evenly in a row without leftover space. - Flexbox's `space between` property gets us close, but doesn't flow + Flexbox's `space between` property gets us close, but doesn't flow well when the # of items per row > 1 and less than the max per row. - Solving by explicitly calc'ing the width of each sprite. Setting - `border-box` simplifies things, because content, padding and - border-width all are included in the width, leaving us only to subtract + Solving by explicitly calc'ing the width of each sprite. Setting + `border-box` simplifies things, because content, padding and + border-width all are included in the width, leaving us only to subtract the left + right margins. @todo: make room for the scrollbar @@ -32,7 +32,7 @@ width: calc((100% / $sprites-per-row ) - $space); min-width: 4rem; min-height: 4rem; /* @todo: calc height same as width */ - margin: calc($space / 2); + margin: calc($space / 2); } @@ -41,11 +41,11 @@ Sets the sprite-selector items as a scrollable pane @todo: Safari: pane doesn't stretch to fill height; - @todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and - also introduces a new bug in Chrome when vertically resizing window down, - then back up, introduces white space in the outside the page container. + @todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and + also introduces a new bug in Chrome when vertically resizing window down, + then back up, introduces white space in the outside the page container. */ - height: calc(100% - $sprite-info-height); + height: calc(100% - $sprite-info-height); overflow-y: scroll; } @@ -57,16 +57,13 @@ padding-top: calc($space / 2); padding-left: calc($space / 2); padding-right: calc($space / 2); - padding-bottom: $space; -} - -.add-buttons { - position: absolute; - bottom: 0.75rem; - right: 1rem; + padding-bottom: $space; } .add-button { font-size: 0.55rem; font-weight: bold; + position: absolute; + bottom: 0.75rem; + right: 1rem; } diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx index e53f52bb1..399d8062b 100644 --- a/src/components/sprite-selector/sprite-selector.jsx +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -10,14 +10,6 @@ import IconButton from '../icon-button/icon-button.jsx'; import styles from './sprite-selector.css'; import spriteIcon from './icon--sprite.svg'; -const addExtensionMessage = ( - -); - const addSpriteMessage = ( - - - - + ); }; @@ -119,7 +100,6 @@ SpriteSelectorComponent.propTypes = { onChangeSpriteX: PropTypes.func, onChangeSpriteY: PropTypes.func, onDeleteSprite: PropTypes.func, - onNewExtensionClick: PropTypes.func, onNewSpriteClick: PropTypes.func, onSelectSprite: PropTypes.func, selectedId: PropTypes.string, diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index 067569a8b..a88a994a0 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -32,7 +32,6 @@ const TargetPane = ({ onChangeSpriteX, onChangeSpriteY, onDeleteSprite, - onNewExtensionClick, onNewSpriteClick, onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, @@ -59,7 +58,6 @@ const TargetPane = ({ onChangeSpriteX={onChangeSpriteX} onChangeSpriteY={onChangeSpriteY} onDeleteSprite={onDeleteSprite} - onNewExtensionClick={onNewExtensionClick} onNewSpriteClick={onNewSpriteClick} onSelectSprite={onSelectSprite} /> @@ -129,7 +127,6 @@ TargetPane.propTypes = { onChangeSpriteX: PropTypes.func, onChangeSpriteY: PropTypes.func, onDeleteSprite: PropTypes.func, - onNewExtensionClick: PropTypes.func, onNewSpriteClick: PropTypes.func, onRequestCloseBackdropLibrary: PropTypes.func, onRequestCloseCostumeLibrary: PropTypes.func, diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index 7e87bac9a..9b70a79d4 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -24,7 +24,6 @@ class TargetPane extends React.Component { 'handleChangeSpriteX', 'handleChangeSpriteY', 'handleDeleteSprite', - 'handleNewExtensionClick', 'handleSelectSprite' ]); } @@ -49,9 +48,6 @@ class TargetPane extends React.Component { handleDeleteSprite (id) { this.props.vm.deleteSprite(id); } - handleNewExtensionClick () { - this.props.vm.extensionManager.loadExtensionURL('extensions/example-extension.js'); - } handleSelectSprite (id) { this.props.vm.setEditingTarget(id); } @@ -66,7 +62,6 @@ class TargetPane extends React.Component { onChangeSpriteX={this.handleChangeSpriteX} onChangeSpriteY={this.handleChangeSpriteY} onDeleteSprite={this.handleDeleteSprite} - onNewExtensionClick={this.handleNewExtensionClick} onSelectSprite={this.handleSelectSprite} /> ); From aad93b43dc9d9946a43f831fbbf1b235be405466 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 29 Aug 2017 15:27:36 -0700 Subject: [PATCH 04/11] Improve "Add Extension" button visibility --- src/components/gui/gui.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 3f5e7061d..c8208bd8b 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -155,5 +155,7 @@ font-weight: bold; position: absolute; bottom: 0.75rem; - left: 14rem; + left: 1rem; + border: 1px solid lightgray; + background: white; } From 2c4a4a47ced45af80307670b1317567bccfda729 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 29 Aug 2017 15:37:56 -0700 Subject: [PATCH 05/11] Add example extension to this repository --- src/components/gui/gui.jsx | 2 +- src/examples/extensions/.eslintrc.js | 8 + src/examples/extensions/example-extension.js | 229 +++++++++++++++++++ webpack.config.js | 7 +- 4 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/examples/extensions/.eslintrc.js create mode 100644 src/examples/extensions/example-extension.js diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 25f082cef..408db59f9 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -58,7 +58,7 @@ const GUIComponent = props => { const onAddExtensionClick = () => { /** @TODO open modal for the extension library instead */ - vm.extensionManager.loadExtensionURL('extensions/example-extension.js'); + vm.extensionManager.loadExtensionURL('static/extensions/example-extension.js'); }; return ( diff --git a/src/examples/extensions/.eslintrc.js b/src/examples/extensions/.eslintrc.js new file mode 100644 index 000000000..f84dadeb6 --- /dev/null +++ b/src/examples/extensions/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + env: { + worker: true + }, + globals: { + Scratch: true + } +}; diff --git a/src/examples/extensions/example-extension.js b/src/examples/extensions/example-extension.js new file mode 100644 index 000000000..e9aeba53f --- /dev/null +++ b/src/examples/extensions/example-extension.js @@ -0,0 +1,229 @@ +class ExampleExtension { + /** + * @return {object} This extension's metadata. + */ + getInfo () { + 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. + */ + myReporter (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(''); + } + + noop () { + } + + returnTrue () { + return true; + } + + returnFalse () { + return false; + } +} + +Scratch.extensions.register(new ExampleExtension()); diff --git a/webpack.config.js b/webpack.config.js index eb077f78f..6c486cc8a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -101,11 +101,12 @@ module.exports = { to: 'static/blocks-media' }]), new CopyWebpackPlugin([{ - from: 'extension-worker.{js,js.map}', - context: 'node_modules/scratch-vm/dist/web' + from: 'extensions/**', + to: 'static', + context: 'src/examples' }]), new CopyWebpackPlugin([{ - from: 'extensions/**', + from: 'extension-worker.{js,js.map}', context: 'node_modules/scratch-vm/dist/web' }]) ].concat(process.env.NODE_ENV === 'production' ? [ From 98d78fbce8b196a279e5a9e88944305e96bf95db Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 29 Aug 2017 15:46:26 -0700 Subject: [PATCH 06/11] Convert example extension to ES5 --- src/examples/extensions/.eslintrc.js | 1 + src/examples/extensions/example-extension.js | 395 ++++++++++--------- 2 files changed, 199 insertions(+), 197 deletions(-) diff --git a/src/examples/extensions/.eslintrc.js b/src/examples/extensions/.eslintrc.js index f84dadeb6..15e7ebc28 100644 --- a/src/examples/extensions/.eslintrc.js +++ b/src/examples/extensions/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + extends: ['scratch'], // no ES6 env: { worker: true }, diff --git a/src/examples/extensions/example-extension.js b/src/examples/extensions/example-extension.js index e9aeba53f..804faccaa 100644 --- a/src/examples/extensions/example-extension.js +++ b/src/examples/extensions/example-extension.js @@ -1,229 +1,230 @@ -class ExampleExtension { - /** - * @return {object} This extension's metadata. - */ - getInfo () { - return { - // Required: the machine-readable name of this extension. - // Will be used as the extension's namespace. Must not contain a '.' character. - id: 'someBlocks', +var ExampleExtension = function () { +}; - // 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', +/** + * @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: 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: 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: Link to documentation content for this extension. - // If not present, offer no link. - docsURI: 'https://....', + // 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==', - // 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' + // 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 + } }, - { - 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' + 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: 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, + // 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 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, + // 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: 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, + // 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: the default value of the argument + defaultValue: 1 }, - // Optional: a string naming the function implementing this block. - // If this is omitted, use the opcode string. - func: 'myReporter', + // 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: 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'] + // 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. { - opcode: 'example-Boolean', - blockType: Scratch.BlockType.BOOLEAN, - text: 'return true', - func: 'returnTrue' + // 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' }, - { - opcode: 'example-hat', - blockType: Scratch.BlockType.HAT, - text: 'after forever', - func: 'returnFalse' - }, - { - // Another block... - } + + // The simplest form of a list item is a string which will be used as + // both value and text. + 'itemId2' ], - // 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', + // Dynamic menu: a string naming a function which returns an array as above. + // Called each time the menu is opened. + menuB: 'getItemsForMenuB' + }, - // Optional: the human-readable label for this item. - // Use `value` as the text if this is absent. - text: 'Item One' - }, + // Optional: translations + translation_map: { + de: { + 'extensionName': 'Einige Blöcke', + 'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]', + 'myReporter.TEXT_default': 'Text', + 'menuA_item1': 'Artikel eins', - // The simplest form of a list item is a string which will be used as - // both value and text. - 'itemId2' - ], + // Dynamic menus can be translated too + 'menuB_example': 'Beispiel', - // Dynamic menu: a string naming a function which returns an array as above. - // Called each time the menu is opened. - menuB: 'getItemsForMenuB' + // This message contains ICU placeholders (see `myReporter()` below) + 'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.' }, + it: { + // ... + } + }, - // Optional: translations - translation_map: { - de: { - 'extensionName': 'Einige Blöcke', - 'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]', - 'myReporter.TEXT_default': 'Text', - 'menuA_item1': 'Artikel eins', + // Optional: list new target type(s) provided by this extension. + targetTypes: [ + 'wedo2', // automatically transformed to 'someBlocks.wedo2' + 'speech' // automatically transformed to 'someBlocks.speech' + ] + }; +}; - // Dynamic menus can be translated too - 'menuB_example': 'Beispiel', +/** + * 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); - // This message contains ICU placeholders (see `myReporter()` below) - 'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.' - }, - it: { - // ... - } - }, + return ['Letter ', args.LETTER_NUM, ' of ', args.TEXT, ' is ', result, '.'].join(''); +}; - // Optional: list new target type(s) provided by this extension. - targetTypes: [ - 'wedo2', // automatically transformed to 'someBlocks.wedo2' - 'speech' // automatically transformed to 'someBlocks.speech' - ] - }; - } +ExampleExtension.prototype.noop = function () { +}; - /** - * 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. - */ - myReporter (args) { - // Note: this implementation is not Unicode-clean; it's just here as an example. - const result = args.TEXT.charAt(args.LETTER_NUM); +ExampleExtension.prototype.returnTrue = function () { + return true; +}; - return ['Letter ', args.LETTER_NUM, ' of ', args.TEXT, ' is ', result, '.'].join(''); - } - - noop () { - } - - returnTrue () { - return true; - } - - returnFalse () { - return false; - } -} +ExampleExtension.prototype.returnFalse = function () { + return false; +}; Scratch.extensions.register(new ExampleExtension()); From e198fd8ba14b8bfebd6759c375f73036653d43b1 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 29 Aug 2017 16:19:00 -0700 Subject: [PATCH 07/11] Show/hide extension button based on URL query To show the "Add Extension" button, you must now add `?extensions` to the URL used to load the GUI. --- src/components/gui/gui.css | 4 ++++ src/components/gui/gui.jsx | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index c8208bd8b..6fee4427f 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -159,3 +159,7 @@ border: 1px solid lightgray; background: white; } + +.hidden { + display: none; +} diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 408db59f9..c59b28953 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -47,6 +47,9 @@ const GUIComponent = props => { ); } + const enableExtensions = window.location.search.includes('extensions'); + const extensionButtonClasses = `${styles.extensionButton} ${enableExtensions ? '' : styles.hidden}`; + const tabClassNames = { tabs: styles.tabs, tab: classNames(tabStyles.reactTabsTab, styles.tab), @@ -94,7 +97,7 @@ const GUIComponent = props => { /> Date: Thu, 31 Aug 2017 10:55:39 -0700 Subject: [PATCH 08/11] Simplify bulk toolbox XML construction --- src/lib/make-toolbox-xml.js | 1197 ++++++++++++++++++----------------- 1 file changed, 599 insertions(+), 598 deletions(-) diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 27a917ff0..cbdbf771e 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -1,609 +1,609 @@ const separator = ''; -const motion = [ - '', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 15', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 15', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 90', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 0', - ' ', - ' ', - ' ', - ' ', - ' 0', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 1', - ' ', - ' ', - ' ', - ' ', - ' 0', - ' ', - ' ', - ' ', - ' ', - ' 0', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 0', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 0', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '' -]; +const motion = ` + + + + + 10 + + + + + + + 15 + + + + + + + 15 + + + + + + + 90 + + + + + + + + + + + + + 0 + + + + + 0 + + + + + + + + + + + + + 1 + + + + + 0 + + + + + 0 + + + + + + + 10 + + + + + + + 0 + + + + + + + 10 + + + + + + + 0 + + + + + + + + + +`; -const looks = [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 100', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 1', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '' -]; +const looks = ` + + + + + + + + + + + + + + + + + + + + + + + + 10 + + + + + + + 10 + + + + + + + + 10 + + + + + + + 100 + + + + + + + + 1 + + + + + + + + +`; -const sound = [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 0.25', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 0.25', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 60', - ' ', - ' ', - ' ', - ' ', - ' 0.5', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 100', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' -10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 100', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 20', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 60', - ' ', - ' ', - ' ', - ' ', - '' -]; +const sound = ` + + + + + + + + + + + + + + + + + + + 0.25 + + + + + + + 0.25 + + + + + + + 60 + + + + + 0.5 + + + + + + + + + + + + 10 + + + + + + + 100 + + + + + + + + -10 + + + + + + + 100 + + + + + + + + 20 + + + + + + + 60 + + + + + +`; -const pen = [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 0', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 50', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 1', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 1', - ' ', - ' ', - ' ', - '' -]; +const pen = ` + + + + + + + + + + + + + + + 10 + + + + + + + 0 + + + + + + + 10 + + + + + + + 50 + + + + + + + 1 + + + + + + + 1 + + + + +`; -const events = [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '' -]; +const events = ` + + + + + + + + + + + 10 + + + + + + + + + + + + + + + + +`; -const control = [ - '', - ' ', - ' ', - ' ', - ' 1', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '' -]; +const control = ` + + + + + 1 + + + + + + + 10 + + + + + + + + + + + + + + + + + +`; -const sensing = [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '' -]; +const sensing = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; -const operators = [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 1', - ' ', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' hello', - ' ', - ' ', - ' ', - ' ', - ' world', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' 1', - ' ', - ' ', - ' ', - ' ', - ' world', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' world', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '' -]; +const operators = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + hello + + + + + world + + + + + + + 1 + + + + + world + + + + + + + world + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; -const data = [ - '', - '' -]; +const data = ` + + +`; const xmlOpen = ''; const xmlClose = ''; @@ -615,7 +615,7 @@ const xmlClose = ''; const makeToolboxXML = function (categoriesXML) { const gap = [separator]; - const everything = [].concat( + const everything = [ xmlOpen, motion, gap, looks, gap, @@ -625,7 +625,8 @@ const makeToolboxXML = function (categoriesXML) { events, gap, control, gap, sensing, gap, - operators); + operators + ]; if (categoriesXML) { everything.push(gap, categoriesXML); From 84c739c81a3a986f2c1606d70c34104d89aa6cf4 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Thu, 31 Aug 2017 11:15:19 -0700 Subject: [PATCH 09/11] Explain need for `setToolboxSelectedItemByName` --- src/containers/blocks.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 9f7831e81..20b13d3ea 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -70,6 +70,8 @@ class Blocks extends React.Component { 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) { @@ -91,6 +93,12 @@ 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++) { From 8e2eef5d5c3121f2971f21fa774c66beb3cfc88c Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Thu, 31 Aug 2017 11:40:05 -0700 Subject: [PATCH 10/11] Adjust for scratch-gui / React conventions --- src/components/gui/gui.css | 4 ++-- src/components/gui/gui.jsx | 16 +++++++--------- src/containers/blocks.jsx | 16 ++++++++-------- src/containers/gui.jsx | 7 +++++++ 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 6fee4427f..2c4715479 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -99,7 +99,6 @@ .blocks-wrapper { flex-grow: 1; position: relative; - z-index: 0; } .stage-and-target-wrapper { @@ -150,7 +149,7 @@ padding: $space; } -.extensionButton { +.extension-button { font-size: 0.55rem; font-weight: bold; position: absolute; @@ -158,6 +157,7 @@ left: 1rem; border: 1px solid lightgray; background: white; + z-index: 50; /** Force extension button above the ScratchBlocks flyout. */ } .hidden { diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index c59b28953..b7d585281 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -34,7 +34,9 @@ const GUIComponent = props => { const { basePath, children, + enableExtensions, vm, + onAddExtensionClick, onTabSelect, tabIndex, ...componentProps @@ -47,9 +49,6 @@ const GUIComponent = props => { ); } - const enableExtensions = window.location.search.includes('extensions'); - const extensionButtonClasses = `${styles.extensionButton} ${enableExtensions ? '' : styles.hidden}`; - const tabClassNames = { tabs: styles.tabs, tab: classNames(tabStyles.reactTabsTab, styles.tab), @@ -59,11 +58,6 @@ const GUIComponent = props => { tabSelected: classNames(tabStyles.reactTabsTabSelected, styles.isSelected) }; - const onAddExtensionClick = () => { - /** @TODO open modal for the extension library instead */ - vm.extensionManager.loadExtensionURL('static/extensions/example-extension.js'); - }; - return ( { /> { 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 diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 20b13d3ea..fbd4ecb6d 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -35,7 +35,7 @@ class Blocks extends React.Component { 'onScriptGlowOff', 'onBlockGlowOn', 'onBlockGlowOff', - 'onExtensionWasAdded', + 'handleExtensionAdded', 'onTargetsUpdate', 'onVisualReport', 'onWorkspaceUpdate', @@ -121,7 +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_WAS_ADDED', this.onExtensionWasAdded); + this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded); } detachVM () { this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); @@ -131,7 +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_WAS_ADDED', this.onExtensionWasAdded); + this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded); } updateToolboxBlockValue (id, value) { const block = this.workspace @@ -199,11 +199,11 @@ class Blocks extends React.Component { this.workspace.resize(); } } - onExtensionWasAdded (blocksInfo) { + handleExtensionAdded (blocksInfo) { this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json)); const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(); const toolboxXML = makeToolboxXML(dynamicBlocksXML); - this.props.sendToolboxUpdate(toolboxXML); + this.props.onExtensionAdded(toolboxXML); } setBlocks (blocks) { this.blocks = blocks; @@ -223,8 +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 - sendToolboxUpdate, // eslint-disable-line no-unused-vars ...props } = this.props; return ( @@ -249,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({ @@ -270,7 +271,6 @@ Blocks.propTypes = { }), comments: PropTypes.bool }), - sendToolboxUpdate: PropTypes.func, toolboxXML: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired }; @@ -311,7 +311,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - sendToolboxUpdate: toolboxXML => { + onExtensionAdded: toolboxXML => { dispatch(updateToolbox(toolboxXML)); } }); diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index c41f7a9a9..0fe10b4d1 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -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 ( From accd92d9a39184e31b36c8a38cb99478114b31be Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 18 Sep 2017 10:51:58 -0700 Subject: [PATCH 11/11] Adjust `clickText` to search only in body This should fix the integration test failures on the extensions branch. Thanks @paulkaplan for figuring this out! --- test/helpers/selenium-helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js index 390740c3c..b250bcbe5 100644 --- a/test/helpers/selenium-helper.js +++ b/test/helpers/selenium-helper.js @@ -33,7 +33,7 @@ class SeleniumHelper { } clickText (text) { - return this.clickXpath(`//*[contains(text(), '${text}')]`); + return this.clickXpath(`//body//*[contains(text(), '${text}')]`); } clickButton (text) {