From c2d642cad721a8e2ed9ee45dda52b8cd9ff48ee2 Mon Sep 17 00:00:00 2001 From: Geng Shenghong Date: Sat, 12 Aug 2017 15:05:00 +0800 Subject: [PATCH 01/46] Fixed #604: Load project error --- src/containers/sound-tab.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx index f95f6f709..3edde50a0 100644 --- a/src/containers/sound-tab.jsx +++ b/src/containers/sound-tab.jsx @@ -106,7 +106,7 @@ class SoundTab extends React.Component { onDeleteClick={this.handleDeleteSound} onItemClick={this.handleSelectSound} > - {target.sounds && target.sounds.length > 0 ? ( + {editingTarget && target.sounds && target.sounds.length > 0 ? ( ) : null} {this.props.soundRecorderVisible ? ( From bdae47acff6a22e703354c17910b6bf6221a13d2 Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 18 Aug 2017 16:08:16 +1200 Subject: [PATCH 02/46] Add glideto menu --- src/lib/blocks.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/blocks.js b/src/lib/blocks.js index 662eb245f..8bcad76cd 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -100,6 +100,14 @@ export default function (vm) { this.jsonInit(json); }; + ScratchBlocks.Blocks.motion_glideto_menu.init = function () { + const json = jsonForMenuBlock('TO', spriteMenu, motionColors, [ + ['mouse-pointer', '_mouse_'], + ['random position', '_random_'] + ]); + this.jsonInit(json); + }; + ScratchBlocks.Blocks.sensing_of_object_menu.init = function () { const json = jsonForMenuBlock('OBJECT', spriteMenu, sensingColors, [ ['Stage', '_stage_'] From 1939a5da29a86d41d246bc2c97aadde107f1607e Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 22 Aug 2017 15:41:01 +0000 Subject: [PATCH 03/46] chore(package): update react-draggable to version 3.0.2 Closes #637 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce1f5108b..19b2bfc04 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "react": "15.6.1", "react-contextmenu": "2.6.5", "react-dom": "15.6.1", - "react-draggable": "2.2.6", + "react-draggable": "3.0.2", "react-intl": "2.3.0", "react-intl-redux": "0.6.0", "react-modal": "2.2.4", From cb01ff204406b76fe80ff7b24792c6509a2538e1 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 1 Aug 2017 10:51:59 -0400 Subject: [PATCH 04/46] Check for sprites before trying to update block menus --- src/lib/blocks.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/blocks.js b/src/lib/blocks.js index 662eb245f..7f66b9b4c 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -29,19 +29,24 @@ export default function (vm) { }; const soundsMenu = function () { - const sounds = vm.editingTarget.sprite.sounds; - if (sounds.length === 0) { - return [['', '']]; + if (vm.editingTarget && vm.editingTarget.sprite.sounds.length > 0) { + return vm.editingTarget.sprite.sounds.map(sound => [sound.name, sound.name]); } - return sounds.map(sound => [sound.name, sound.name]); + return [['', '']]; }; const costumesMenu = function () { - return vm.editingTarget.sprite.costumes.map(costume => [costume.name, costume.name]); + if (vm.editingTarget && vm.editingTarget.sprite.costumes.length > 0) { + return vm.editingTarget.sprite.costumes.map(costume => [costume.name, costume.name]); + } + return [['', '']]; }; const backdropsMenu = function () { - return vm.runtime.targets[0].sprite.costumes.map(costume => [costume.name, costume.name]); + if (vm.runtime.targets[0] && vm.runtime.targets[0].sprite.costumes.length > 0) { + return vm.runtime.targets[0].sprite.costumes.map(costume => [costume.name, costume.name]); + } + return [['', '']]; }; const spriteMenu = function () { From a1671226e4aa08167c0881ec13734e2b1706ed27 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 28 Aug 2017 05:24:55 +0000 Subject: [PATCH 05/46] chore(package): update autoprefixer to version 7.1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19b2bfc04..6a91be254 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "react-dom": "^15.6.1" }, "devDependencies": { - "autoprefixer": "7.1.2", + "autoprefixer": "7.1.3", "babel-cli": "6.26.0", "babel-core": "^6.23.1", "babel-eslint": "^7.2.3", From 6ce561245f119242d3b38de75ab70335b69e195d Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 28 Aug 2017 17:30:42 -0400 Subject: [PATCH 06/46] Do not render invisible components --- src/components/gui/gui.jsx | 4 +- src/components/target-pane/target-pane.jsx | 58 ++++++++++++---------- src/containers/backdrop-library.jsx | 3 +- src/containers/costume-library.jsx | 3 +- src/containers/sound-library.jsx | 3 +- src/containers/sprite-library.jsx | 3 +- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 1eb0e6505..9b45665a6 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -79,10 +79,10 @@ const GUIComponent = props => { - + {tabIndex === 1 ? : null} - + {tabIndex === 2 ? : null} diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index a88a994a0..4175cbdb7 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -3,7 +3,6 @@ import React from 'react'; import VM from 'scratch-vm'; -import Box from '../box/box.jsx'; import BackdropLibrary from '../../containers/backdrop-library.jsx'; import CostumeLibrary from '../../containers/costume-library.jsx'; import SoundLibrary from '../../containers/sound-library.jsx'; @@ -43,7 +42,7 @@ const TargetPane = ({ vm, ...componentProps }) => ( - @@ -61,7 +60,7 @@ const TargetPane = ({ onNewSpriteClick={onNewSpriteClick} onSelectSprite={onSelectSprite} /> - +
{stage.id && } - - - - - - - - +
+ {spriteLibraryVisible ? ( + + ) : null} + {costumeLibraryVisible ? ( + + ) : null} + {soundLibraryVisible ? ( + + ) : null} + {backdropLibraryVisible ? ( + + ) : null} + +
+
+ ); const spriteShape = PropTypes.shape({ diff --git a/src/containers/backdrop-library.jsx b/src/containers/backdrop-library.jsx index e455a0a25..4cbc0e022 100644 --- a/src/containers/backdrop-library.jsx +++ b/src/containers/backdrop-library.jsx @@ -27,9 +27,9 @@ class BackdropLibrary extends React.Component { render () { return ( @@ -39,7 +39,6 @@ class BackdropLibrary extends React.Component { BackdropLibrary.propTypes = { onRequestClose: PropTypes.func, - visible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/containers/costume-library.jsx b/src/containers/costume-library.jsx index ee5526752..6888a8cd7 100644 --- a/src/containers/costume-library.jsx +++ b/src/containers/costume-library.jsx @@ -27,9 +27,9 @@ class CostumeLibrary extends React.PureComponent { render () { return ( @@ -39,7 +39,6 @@ class CostumeLibrary extends React.PureComponent { CostumeLibrary.propTypes = { onRequestClose: PropTypes.func, - visible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx index 8fe3f97bd..1a90b0e6a 100644 --- a/src/containers/sound-library.jsx +++ b/src/containers/sound-library.jsx @@ -74,9 +74,9 @@ class SoundLibrary extends React.PureComponent { return ( Date: Tue, 29 Aug 2017 11:41:21 -0700 Subject: [PATCH 07/46] 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 08/46] 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 09/46] 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 10/46] 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 11/46] 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 12/46] 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 13/46] 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: Wed, 30 Aug 2017 21:47:36 +0000 Subject: [PATCH 14/46] chore(package): update css-loader to version 0.28.7 Closes #656 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a91be254..7b86073af 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "chromedriver": "^2.31.0", "classnames": "2.2.5", "copy-webpack-plugin": "4.0.1", - "css-loader": "0.28.5", + "css-loader": "0.28.7", "enzyme": "^2.8.2", "eslint": "^4.4.1", "eslint-config-scratch": "^4.0.0", From e0cadb82095b27b86acaa413adac6214679efd15 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Thu, 31 Aug 2017 09:53:13 -0400 Subject: [PATCH 15/46] Use major versions for build dependencies --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 7b86073af..5038c0ba8 100644 --- a/package.json +++ b/package.json @@ -28,21 +28,21 @@ "react-dom": "^15.6.1" }, "devDependencies": { - "autoprefixer": "7.1.3", - "babel-cli": "6.26.0", + "autoprefixer": "^7.1.3", + "babel-cli": "^6.26.0", "babel-core": "^6.23.1", "babel-eslint": "^7.2.3", "babel-loader": "^7.1.0", - "babel-plugin-react-intl": "2.3.1", - "babel-plugin-syntax-dynamic-import": "6.18.0", + "babel-plugin-react-intl": "^2.3.1", + "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.22.0", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.22.0", "chromedriver": "^2.31.0", "classnames": "2.2.5", - "copy-webpack-plugin": "4.0.1", - "css-loader": "0.28.7", + "copy-webpack-plugin": "^4.0.1", + "css-loader": "^0.28.7", "enzyme": "^2.8.2", "eslint": "^4.4.1", "eslint-config-scratch": "^4.0.0", @@ -50,7 +50,7 @@ "eslint-plugin-react": "^7.2.1", "get-float-time-domain-data": "0.1.0", "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder", - "html-webpack-plugin": "2.30.0", + "html-webpack-plugin": "^2.30.0", "immutable": "3.8.1", "jest": "^20.0.4", "lodash.bindall": "4.4.0", @@ -90,7 +90,7 @@ "startaudiocontext": "1.2.1", "style-loader": "^0.18.0", "svg-to-image": "1.1.3", - "svg-url-loader": "2.1.0", + "svg-url-loader": "^2.1.0", "wav-encoder": "1.3.0", "web-audio-test-api": "^0.5.2", "webpack": "^3.5.4", From d607f03fa0c86fb6abd6d7b1b75730fb128fd43f Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Thu, 31 Aug 2017 10:55:39 -0700 Subject: [PATCH 16/46] 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 17/46] 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 18/46] 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 0e756fe42767dd600a6ce7f124f82ad23a4ed62b Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 31 Aug 2017 20:12:57 +0000 Subject: [PATCH 19/46] chore(package): update react-draggable to version 3.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5038c0ba8..b782c96b6 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "react": "15.6.1", "react-contextmenu": "2.6.5", "react-dom": "15.6.1", - "react-draggable": "3.0.2", + "react-draggable": "3.0.3", "react-intl": "2.3.0", "react-intl-redux": "0.6.0", "react-modal": "2.2.4", From fffec193021701890aa828ca96be4b967bce35d9 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 1 Sep 2017 09:09:08 -0400 Subject: [PATCH 20/46] Remove visible property from modal and library components --- src/components/library/library.jsx | 5 +---- src/components/modal/modal.jsx | 5 ++--- src/components/prompt/prompt.jsx | 1 - src/components/record-modal/record-modal.jsx | 1 - src/containers/backdrop-library.jsx | 1 - src/containers/costume-library.jsx | 1 - src/containers/record-modal.jsx | 1 - src/containers/sound-library.jsx | 6 ++---- src/containers/sprite-library.jsx | 5 ++--- 9 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index f5c0ae30c..165716e5b 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -43,13 +43,11 @@ class LibraryComponent extends React.Component { dataItem.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) !== -1); } render () { - if (!this.props.visible) return null; return ( (this.modal = m)} onRequestClose={this.props.onRequestClose} @@ -68,8 +68,7 @@ ModalComponent.propTypes = { filterQuery: PropTypes.string, onFilterChange: PropTypes.func, onFilterClear: PropTypes.func, - onRequestClose: PropTypes.func, - visible: PropTypes.bool.isRequired + onRequestClose: PropTypes.func }; export default ModalComponent; diff --git a/src/components/prompt/prompt.jsx b/src/components/prompt/prompt.jsx index d43002b41..4fca4cb4a 100644 --- a/src/components/prompt/prompt.jsx +++ b/src/components/prompt/prompt.jsx @@ -7,7 +7,6 @@ import styles from './prompt.css'; const PromptComponent = props => ( ( ({ - visible: state.modals.soundRecorder, vm: state.vm }); diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx index 1a90b0e6a..3949103bb 100644 --- a/src/containers/sound-library.jsx +++ b/src/containers/sound-library.jsx @@ -22,9 +22,8 @@ class SoundLibrary extends React.PureComponent { this.audioEngine = new AudioEngine(); this.player = this.audioEngine.createPlayer(); } - componentWillReceiveProps (newProps) { - // Stop playing sounds if the library closes without a mouseleave (e.g. by using the escape key) - if (this.player && !newProps.visible) this.player.stopAllSounds(); + componentWillUnmount () { + this.player.stopAllSounds(); } handleItemMouseEnter (soundItem) { const md5ext = soundItem._md5; @@ -74,7 +73,6 @@ class SoundLibrary extends React.PureComponent { return ( Date: Mon, 4 Sep 2017 15:30:45 +0000 Subject: [PATCH 21/46] chore(package): update jest to version 21.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5038c0ba8..15eb6208d 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder", "html-webpack-plugin": "^2.30.0", "immutable": "3.8.1", - "jest": "^20.0.4", + "jest": "^21.0.0", "lodash.bindall": "4.4.0", "lodash.debounce": "4.0.8", "lodash.defaultsdeep": "4.6.0", From c52b0afc0036cc903adbe59838965e916c60be42 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 5 Sep 2017 19:19:17 +0000 Subject: [PATCH 22/46] chore(package): update react-modal to version 2.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5038c0ba8..c170d2e1c 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "react-draggable": "3.0.2", "react-intl": "2.3.0", "react-intl-redux": "0.6.0", - "react-modal": "2.2.4", + "react-modal": "2.3.1", "react-redux": "5.0.6", "react-responsive": "1.3.4", "react-style-proptype": "3.0.0", From fdab3c640f1f2ff4353380f96a52f0f10a7e75a2 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 5 Sep 2017 20:53:00 +0000 Subject: [PATCH 23/46] chore(package): update react-tabs to version 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5038c0ba8..07f34e38e 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "react-redux": "5.0.6", "react-responsive": "1.3.4", "react-style-proptype": "3.0.0", - "react-tabs": "1.1.0", + "react-tabs": "2.0.0", "react-test-renderer": "^15.5.4", "redux": "3.7.0", "redux-mock-store": "^1.2.3", From b95ce1b65fe9b0fe3a3d66775c308a6395c9cb75 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 6 Sep 2017 12:08:08 +0000 Subject: [PATCH 24/46] chore(package): update react-intl to version 2.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5038c0ba8..31290d00b 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "react-contextmenu": "2.6.5", "react-dom": "15.6.1", "react-draggable": "3.0.2", - "react-intl": "2.3.0", + "react-intl": "2.4.0", "react-intl-redux": "0.6.0", "react-modal": "2.2.4", "react-redux": "5.0.6", From 326fbee4985cb6ae9695c524feef5784e402ee54 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 14 Sep 2017 02:47:48 +0000 Subject: [PATCH 25/46] chore(package): update react-contextmenu to version 2.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eba4aacc8..1de19f18b 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "postcss-simple-vars": "^4.0.0", "prop-types": "^15.5.10", "react": "15.6.1", - "react-contextmenu": "2.6.5", + "react-contextmenu": "2.7.0", "react-dom": "15.6.1", "react-draggable": "3.0.3", "react-intl": "2.4.0", From 2f31b5fad8984a5085c967c5bb4ef375db043a4b Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 14 Sep 2017 20:03:15 +0000 Subject: [PATCH 26/46] chore(package): update webpack-dev-server to version 2.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eba4aacc8..b217451a2 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "wav-encoder": "1.3.0", "web-audio-test-api": "^0.5.2", "webpack": "^3.5.4", - "webpack-dev-server": "^2.7.0", + "webpack-dev-server": "^2.8.2", "xhr": "2.4.0" }, "jest": { From 0cb1fa7cbb62878a4fdf1d51943808bf8afe909d Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 18 Sep 2017 09:57:38 -0400 Subject: [PATCH 27/46] Fix eslint error from prop types --- src/containers/recording-step.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/containers/recording-step.jsx b/src/containers/recording-step.jsx index d1fc3302b..4e82e6ec8 100644 --- a/src/containers/recording-step.jsx +++ b/src/containers/recording-step.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import bindAll from 'lodash.bindall'; import RecordingStepComponent from '../components/record-modal/recording-step.jsx'; import AudioRecorder from '../lib/audio/audio-recorder.js'; @@ -66,6 +67,10 @@ class RecordingStep extends React.Component { } } -RecordingStep.propTypes = RecordingStepComponent.propTypes; +RecordingStep.propTypes = { + onRecord: PropTypes.func.isRequired, + onStopRecording: PropTypes.func.isRequired, + recording: PropTypes.bool +}; export default RecordingStep; From 4223e03851706536cb4241fd32aec9ca4132ac9b Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 18 Sep 2017 12:03:34 -0400 Subject: [PATCH 28/46] Use --runInBand for integration tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eba4aacc8..32d692ed3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint": "eslint . --ext .js,.jsx", "start": "npm run i18n:msgs && webpack-dev-server", "unit-test": "jest test/unit", - "integration-test": "npm run build && jest test/integration", + "integration-test": "npm run build && jest --runInBand test/integration", "test": "npm run lint && npm run unit-test && npm run integration-test", "watch": "webpack --progress --colors --watch" }, From fe4f0a59739d4d6ea3a7dcf14ce77313b93fe3d6 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 18 Sep 2017 09:22:59 -0400 Subject: [PATCH 29/46] Add minimum trimmable length for audio trimmer --- src/containers/audio-trimmer.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx index f4ba1be3c..9cf6e38f1 100644 --- a/src/containers/audio-trimmer.jsx +++ b/src/containers/audio-trimmer.jsx @@ -4,6 +4,8 @@ import bindAll from 'lodash.bindall'; import AudioTrimmerComponent from '../components/audio-trimmer/audio-trimmer.jsx'; import {getEventXY} from '../lib/touch-utils'; +const MIN_LENGTH = 0.01; // Used to stop sounds being trimmed smaller than 1% + class AudioTrimmer extends React.Component { constructor (props) { super(props); @@ -20,14 +22,14 @@ class AudioTrimmer extends React.Component { handleTrimStartMouseMove (e) { const containerSize = this.containerElement.getBoundingClientRect().width; const dx = (getEventXY(e).x - this.initialX) / containerSize; - const newTrim = Math.max(0, Math.min(this.props.trimEnd, this.initialTrim + dx)); + const newTrim = Math.max(0, Math.min(this.props.trimEnd - MIN_LENGTH, this.initialTrim + dx)); this.props.onSetTrimStart(newTrim); e.preventDefault(); } handleTrimEndMouseMove (e) { const containerSize = this.containerElement.getBoundingClientRect().width; const dx = (getEventXY(e).x - this.initialX) / containerSize; - const newTrim = Math.min(1, Math.max(this.props.trimStart, this.initialTrim + dx)); + const newTrim = Math.min(1, Math.max(this.props.trimStart + MIN_LENGTH, this.initialTrim + dx)); this.props.onSetTrimEnd(newTrim); e.preventDefault(); } From accd92d9a39184e31b36c8a38cb99478114b31be Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 18 Sep 2017 10:51:58 -0700 Subject: [PATCH 30/46] 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) { From c1a80ce893521c1fbace2fc8f1516d5fd9822c91 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 29 Aug 2017 08:58:14 -0400 Subject: [PATCH 31/46] Add basic library and reducers for extensions Alphabetize all the things --- src/components/blocks/blocks.css | 1 + src/components/gui/gui.css | 24 +++++++---- src/components/gui/gui.jsx | 26 ++++++------ src/components/gui/icon--extensions.svg | 18 ++++++++ src/components/target-pane/target-pane.jsx | 13 +++++- src/containers/extension-library.jsx | 48 ++++++++++++++++++++++ src/containers/gui.jsx | 22 ++++++---- src/containers/target-pane.jsx | 7 +++- src/lib/libraries/extensions.json | 7 ++++ src/reducers/modals.js | 10 +++++ 10 files changed, 146 insertions(+), 30 deletions(-) create mode 100644 src/components/gui/icon--extensions.svg create mode 100644 src/containers/extension-library.jsx create mode 100644 src/lib/libraries/extensions.json diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css index 80689e48c..6c5858efd 100644 --- a/src/components/blocks/blocks.css +++ b/src/components/blocks/blocks.css @@ -21,6 +21,7 @@ $border-style: 1px solid $ui-pane-border; border-right: $border-style; border-bottom: $border-style; box-sizing: content-box; + height: calc(100% - 3.25rem) !important; } .blocks :global(.blocklyFlyout) { diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 2c4715479..bb336c40d 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -149,17 +149,23 @@ padding: $space; } +.extension-button-container { + width: 60px; + height: 3.25rem; + position: absolute; + bottom: 0; + left: 0; + z-index: 50; /** Force extension button above the ScratchBlocks flyout. */ + background: white; + + border: 1px solid #ddd; + box-sizing: content-box; /* To match scratch-block vertical toolbox borders */ +} + .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; +.extension-button > div { + margin-top: 0; } diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index d230476b8..2257c2cee 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -20,11 +20,11 @@ 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'; +import addExtensionIcon from './icon--extensions.svg'; const addExtensionMessage = ( @@ -36,7 +36,7 @@ const GUIComponent = props => { children, enableExtensions, vm, - onAddExtensionClick, + onExtensionButtonClick, onTabSelect, tabIndex, ...componentProps @@ -90,14 +90,16 @@ const GUIComponent = props => { vm={vm} /> - + + + {tabIndex === 1 ? : null} @@ -137,7 +139,7 @@ GUIComponent.propTypes = { basePath: PropTypes.string, children: PropTypes.node, enableExtensions: PropTypes.bool, - onAddExtensionClick: PropTypes.func, + onExtensionButtonClick: PropTypes.func, onTabSelect: PropTypes.func, tabIndex: PropTypes.number, vm: PropTypes.instanceOf(VM).isRequired diff --git a/src/components/gui/icon--extensions.svg b/src/components/gui/icon--extensions.svg new file mode 100644 index 000000000..de96e820c --- /dev/null +++ b/src/components/gui/icon--extensions.svg @@ -0,0 +1,18 @@ + + + + add extension + Created with Sketch. + + + + + + + + + + + + + diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index 4175cbdb7..07281756d 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -7,6 +7,8 @@ import BackdropLibrary from '../../containers/backdrop-library.jsx'; import CostumeLibrary from '../../containers/costume-library.jsx'; import SoundLibrary from '../../containers/sound-library.jsx'; import SpriteLibrary from '../../containers/sprite-library.jsx'; +import ExtensionLibrary from '../../containers/extension-library.jsx'; + import SpriteSelectorComponent from '../sprite-selector/sprite-selector.jsx'; import StageSelector from '../../containers/stage-selector.jsx'; @@ -20,6 +22,7 @@ import styles from './target-pane.css'; */ const TargetPane = ({ editingTarget, + extensionLibraryVisible, backdropLibraryVisible, costumeLibraryVisible, soundLibraryVisible, @@ -36,6 +39,7 @@ const TargetPane = ({ onRequestCloseCostumeLibrary, onRequestCloseSoundLibrary, onRequestCloseSpriteLibrary, + onRequestCloseExtensionLibrary, onSelectSprite, stage, sprites, @@ -72,6 +76,12 @@ const TargetPane = ({ onSelect={onSelectSprite} />}
+ {extensionLibraryVisible ? ( + + ) : null} {spriteLibraryVisible ? ( ) : null} -
@@ -124,6 +133,7 @@ TargetPane.propTypes = { backdropLibraryVisible: PropTypes.bool, costumeLibraryVisible: PropTypes.bool, editingTarget: PropTypes.string, + extensionLibraryVisible: PropTypes.bool, onChangeSpriteDirection: PropTypes.func, onChangeSpriteName: PropTypes.func, onChangeSpriteRotationStyle: PropTypes.func, @@ -134,6 +144,7 @@ TargetPane.propTypes = { onNewSpriteClick: PropTypes.func, onRequestCloseBackdropLibrary: PropTypes.func, onRequestCloseCostumeLibrary: PropTypes.func, + onRequestCloseExtensionLibrary: PropTypes.func, onRequestCloseSoundLibrary: PropTypes.func, onRequestCloseSpriteLibrary: PropTypes.func, onSelectSprite: PropTypes.func, diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx new file mode 100644 index 000000000..343d0747d --- /dev/null +++ b/src/containers/extension-library.jsx @@ -0,0 +1,48 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import VM from 'scratch-vm'; + +import extensionLibraryContent from '../lib/libraries/extensions.json'; + +import LibraryComponent from '../components/library/library.jsx'; +import extensionIcon from '../components/sprite-selector/icon--sprite.svg'; + +class ExtensionLibrary extends React.PureComponent { + constructor (props) { + super(props); + bindAll(this, [ + 'handleItemSelect' + ]); + } + handleItemSelect (item) { + // eslint-disable-next-line no-alert + const url = item.extensionURL || prompt('Enter the URL of the extension'); + if (url) { + this.props.vm.extensionManager.loadExtensionURL(url); + } + } + render () { + const extensionLibraryThumbnailData = extensionLibraryContent.map(extension => ({ + rawURL: extension.iconURL || extensionIcon, + ...extension + })); + return ( + + ); + } +} + +ExtensionLibrary.propTypes = { + onRequestClose: PropTypes.func, + visible: PropTypes.bool, + vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types +}; + +export default ExtensionLibrary; diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 0fe10b4d1..9de0e3616 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -3,6 +3,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import VM from 'scratch-vm'; import bindAll from 'lodash.bindall'; +import {connect} from 'react-redux'; + +import {openExtensionLibrary} from '../reducers/modals.js'; import vmListenerHOC from '../lib/vm-listener-hoc.jsx'; @@ -12,7 +15,6 @@ class GUI extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleAddExtensionClick', 'handleTabSelect' ]); this.state = {tabIndex: 0}; @@ -35,10 +37,6 @@ 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, @@ -51,7 +49,6 @@ class GUI extends React.Component { enableExtensions={window.location.search.includes('extensions')} tabIndex={this.state.tabIndex} vm={vm} - onAddExtensionClick={this.handleAddExtensionClick} onTabSelect={this.handleTabSelect} {...componentProps} > @@ -69,4 +66,15 @@ GUI.propTypes = { GUI.defaultProps = GUIComponent.defaultProps; -export default vmListenerHOC(GUI); +const mapStateToProps = () => ({}); + +const mapDispatchToProps = dispatch => ({ + onExtensionButtonClick: () => dispatch(openExtensionLibrary()) +}); + +const ConnectedGUI = connect( + mapStateToProps, + mapDispatchToProps, +)(GUI); + +export default vmListenerHOC(ConnectedGUI); diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index 9b70a79d4..0ec1c43ff 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -7,6 +7,7 @@ import { openSpriteLibrary, closeBackdropLibrary, closeCostumeLibrary, + closeExtensionLibrary, closeSoundLibrary, closeSpriteLibrary } from '../reducers/modals'; @@ -91,7 +92,8 @@ const mapStateToProps = state => ({ soundLibraryVisible: state.modals.soundLibrary, spriteLibraryVisible: state.modals.spriteLibrary, costumeLibraryVisible: state.modals.costumeLibrary, - backdropLibraryVisible: state.modals.backdropLibrary + backdropLibraryVisible: state.modals.backdropLibrary, + extensionLibraryVisible: state.modals.extensionLibrary }); const mapDispatchToProps = dispatch => ({ onNewSpriteClick: e => { @@ -104,6 +106,9 @@ const mapDispatchToProps = dispatch => ({ onRequestCloseCostumeLibrary: () => { dispatch(closeCostumeLibrary()); }, + onRequestCloseExtensionLibrary: () => { + dispatch(closeExtensionLibrary()); + }, onRequestCloseSoundLibrary: () => { dispatch(closeSoundLibrary()); }, diff --git a/src/lib/libraries/extensions.json b/src/lib/libraries/extensions.json new file mode 100644 index 000000000..d23bb965a --- /dev/null +++ b/src/lib/libraries/extensions.json @@ -0,0 +1,7 @@ +[ + { + "name": "Some Blocks", + "extensionURL": "static/extensions/example-extension.js", + "md5": "76f99773dd4eb59f86cd11bba6ad3cb9.svg" + } +] diff --git a/src/reducers/modals.js b/src/reducers/modals.js index e651f0517..55a55e4fe 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -6,10 +6,12 @@ const MODAL_COSTUME_LIBRARY = 'costumeLibrary'; const MODAL_SOUND_LIBRARY = 'soundLibrary'; const MODAL_SPRITE_LIBRARY = 'spriteLibrary'; const MODAL_SOUND_RECORDER = 'soundRecorder'; +const MODAL_EXTENSION_LIBRARY = 'extensionLibrary'; const initialState = { [MODAL_BACKDROP_LIBRARY]: false, [MODAL_COSTUME_LIBRARY]: false, + [MODAL_EXTENSION_LIBRARY]: false, [MODAL_SOUND_LIBRARY]: false, [MODAL_SPRITE_LIBRARY]: false, [MODAL_SOUND_RECORDER]: false @@ -57,12 +59,18 @@ const openSpriteLibrary = function () { const openSoundRecorder = function () { return openModal(MODAL_SOUND_RECORDER); }; +const openExtensionLibrary = function () { + return openModal(MODAL_EXTENSION_LIBRARY); +}; const closeBackdropLibrary = function () { return closeModal(MODAL_BACKDROP_LIBRARY); }; const closeCostumeLibrary = function () { return closeModal(MODAL_COSTUME_LIBRARY); }; +const closeExtensionLibrary = function () { + return closeModal(MODAL_EXTENSION_LIBRARY); +}; const closeSpriteLibrary = function () { return closeModal(MODAL_SPRITE_LIBRARY); }; @@ -74,6 +82,7 @@ const closeSoundRecorder = function () { }; export { reducer as default, + openExtensionLibrary, openBackdropLibrary, openCostumeLibrary, openSoundLibrary, @@ -81,6 +90,7 @@ export { openSoundRecorder, closeBackdropLibrary, closeCostumeLibrary, + closeExtensionLibrary, closeSpriteLibrary, closeSoundLibrary, closeSoundRecorder From 65e01398783a1e53626964c4af7bbbf26b24ba5c Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 28 Aug 2017 10:41:37 -0400 Subject: [PATCH 32/46] WIP initial prototype of eye dropper --- src/components/loupe/loupe.css | 5 ++ src/components/loupe/loupe.jsx | 104 +++++++++++++++++++++++++++++++++ src/components/stage/stage.css | 19 +++++- src/components/stage/stage.jsx | 47 +++++++++++---- src/containers/blocks.jsx | 18 ++++-- src/containers/stage.jsx | 59 +++++++++++++++++-- src/reducers/color-picker.js | 40 +++++++++++++ src/reducers/gui.js | 3 +- 8 files changed, 273 insertions(+), 22 deletions(-) create mode 100644 src/components/loupe/loupe.css create mode 100644 src/components/loupe/loupe.jsx create mode 100644 src/reducers/color-picker.js diff --git a/src/components/loupe/loupe.css b/src/components/loupe/loupe.css new file mode 100644 index 000000000..d0d708c0d --- /dev/null +++ b/src/components/loupe/loupe.css @@ -0,0 +1,5 @@ +.color-picker { + position: absolute; + border-radius: 100%; + border: 1px solid #222; +} diff --git a/src/components/loupe/loupe.jsx b/src/components/loupe/loupe.jsx new file mode 100644 index 000000000..4c2c1b2d6 --- /dev/null +++ b/src/components/loupe/loupe.jsx @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import bindAll from 'lodash.bindall'; + +import Box from '../box/box.jsx'; +import styles from './loupe.css'; + +const zoomScale = 3; + +class LoupeComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'setCanvas' + ]); + } + componentDidUpdate () { + this.draw(); + } + draw () { + const boxSize = 6 / zoomScale; + const boxLineWidth = 1 / zoomScale; + const colorRingWidth = 15 / zoomScale; + + const ctx = this.canvas.getContext('2d'); + const {color, data, width, height} = this.props.colorInfo; + this.canvas.width = zoomScale * width; + this.canvas.height = zoomScale * height; + + // In order to scale the image data, must draw to a tmp canvas first + const tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = width; + tmpCanvas.height = height; + const tmpCtx = tmpCanvas.getContext('2d'); + const imageData = tmpCtx.createImageData(width, height); + imageData.data.set(data); + tmpCtx.putImageData(imageData, 0, 0); + + // Scale the loupe canvas and draw the zoomed image + ctx.save(); + ctx.scale(zoomScale, zoomScale); + ctx.drawImage(tmpCanvas, 0, 0, width, height); + + // Draw an outlined square at the cursor position (cursor is hidden) + ctx.lineWidth = boxLineWidth; + ctx.strokeStyle = 'black'; + ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`; + ctx.beginPath(); + ctx.rect((width / 2) - (boxSize / 2), (height / 2) - (boxSize / 2), boxSize, boxSize); + ctx.fill(); + ctx.stroke(); + + // Draw a thick ring around the loupe showing the current color + ctx.strokeStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`; + ctx.lineWidth = colorRingWidth; + ctx.beginPath(); + ctx.moveTo(width, height / 2); + ctx.arc(width / 2, height / 2, width / 2, 0, 2 * Math.PI); + ctx.stroke(); + ctx.restore(); + } + setCanvas (element) { + this.canvas = element; + } + render () { + const { + colorInfo, + ...boxProps + } = this.props; + return ( + + ); + } +} + +LoupeComponent.propTypes = { + colorInfo: PropTypes.shape({ + color: PropTypes.shape({ + r: PropTypes.number, + g: PropTypes.number, + b: PropTypes.number + }), + data: PropTypes.instanceOf(Uint8Array), + width: PropTypes.number, + height: PropTypes.number, + x: PropTypes.number, + y: PropTypes.number + }) +}; + +export default LoupeComponent; diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css index 4cc5d3092..f13554a02 100644 --- a/src/components/stage/stage.css +++ b/src/components/stage/stage.css @@ -13,15 +13,32 @@ background-color: transparent; } +.with-color-picker { + cursor: none; + z-index: 2001; +} + +.color-picker-background { + position: absolute;; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.55); + display: block; + z-index: 2000; + top: 0; + left: 0; +} + .stage-wrapper { position: relative; } -.monitor-wrapper { +.monitor-wrapper, .color-picker-wrapper { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; + overflow: hidden; } diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx index 92ef79f21..7a23c099d 100644 --- a/src/components/stage/stage.jsx +++ b/src/components/stage/stage.jsx @@ -1,7 +1,9 @@ import PropTypes from 'prop-types'; import React from 'react'; +import classNames from 'classnames'; import Box from '../box/box.jsx'; +import Loupe from '../loupe/loupe.jsx'; import MonitorList from '../../containers/monitor-list.jsx'; import styles from './stage.css'; @@ -10,27 +12,50 @@ const StageComponent = props => { canvasRef, width, height, + colorInfo, + onDeactivateColorPicker, + isColorPicking, ...boxProps } = props; return ( - +
- - + className={classNames(styles.stageWrapper, { + [styles.withColorPicker]: isColorPicking + })} + > + + + + + {isColorPicking && colorInfo ? ( + + + + ) : null} - + {isColorPicking ? ( + + ) : null} +
); }; StageComponent.propTypes = { canvasRef: PropTypes.func, + colorInfo: Loupe.propTypes.colorInfo, height: PropTypes.number, + isColorPicking: PropTypes.bool, + onDeactivateColorPicker: PropTypes.func, width: PropTypes.number }; StageComponent.defaultProps = { diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index fbd4ecb6d..b15718483 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -11,6 +11,7 @@ import BlocksComponent from '../components/blocks/blocks.jsx'; import {connect} from 'react-redux'; import {updateToolbox} from '../reducers/toolbox'; +import {activateColorPicker} from '../reducers/color-picker'; const addFunctionListener = (object, property, callback) => { const oldFn = object[property]; @@ -50,6 +51,8 @@ class Blocks extends React.Component { this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100); } componentDidMount () { + this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; + const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); @@ -219,14 +222,17 @@ class Blocks extends React.Component { this.setState({prompt: null}); } render () { + /* eslint-disable no-unused-vars */ const { - 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 + options, + vm, + isVisible, + onActivateColorPicker, + onExtensionAdded, + toolboxXML, ...props } = this.props; + /* eslint-enable no-unused-vars */ return (
({ }); const mapDispatchToProps = dispatch => ({ + onActivateColorPicker: callback => dispatch(activateColorPicker(callback)), onExtensionAdded: toolboxXML => { dispatch(updateToolbox(toolboxXML)); } diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index 1b5b2ec9c..f45a6628b 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -3,10 +3,19 @@ import PropTypes from 'prop-types'; import React from 'react'; import Renderer from 'scratch-render'; import VM from 'scratch-vm'; +import {connect} from 'react-redux'; + import {getEventXY} from '../lib/touch-utils'; import StageComponent from '../components/stage/stage.jsx'; +import { + activateColorPicker, + deactivateColorPicker +} from '../reducers/color-picker'; + +const colorPickerRadius = 20; + class Stage extends React.Component { constructor (props) { super(props); @@ -28,7 +37,8 @@ class Stage extends React.Component { mouseDownPosition: null, isDragging: false, dragOffset: null, - dragId: null + dragId: null, + colorInfo: null }; } componentDidMount () { @@ -38,8 +48,11 @@ class Stage extends React.Component { this.renderer = new Renderer(this.canvas); this.props.vm.attachRenderer(this.renderer); } - shouldComponentUpdate (nextProps) { - return this.props.width !== nextProps.width || this.props.height !== nextProps.height; + shouldComponentUpdate (nextProps, nextState) { + return this.props.width !== nextProps.width || + this.props.height !== nextProps.height || + this.props.isColorPicking !== nextProps.isColorPicking || + this.state.colorInfo !== nextState.colorInfo; } componentWillUnmount () { this.detachMouseEvents(this.canvas); @@ -79,6 +92,13 @@ class Stage extends React.Component { (nativeSize[1] / this.rect.height) * (y - (this.rect.height / 2)) ]; } + getColorInfo (x, y) { + return { + x: x, + y: y, + ...this.renderer.extractColor(x, y, colorPickerRadius) + }; + } handleDoubleClick (e) { const {x, y} = getEventXY(e); // Set editing target from cursor position, if clicking on a sprite. @@ -113,6 +133,10 @@ class Stage extends React.Component { canvasHeight: this.rect.height }; this.props.vm.postIOData('mouse', coordinates); + + if (this.props.isColorPicking) { + this.setState({colorInfo: this.getColorInfo(mousePosition[0], mousePosition[1])}); + } } onMouseUp (e) { const {x, y} = getEventXY(e); @@ -157,6 +181,16 @@ class Stage extends React.Component { if (e.preventDefault) { e.preventDefault(); } + if (this.props.isColorPicking) { + const {r, g, b} = this.state.colorInfo.color; + const componentToString = c => { + const hex = c.toString(16); + return hex.length === 1 ? `0${hex}` : hex; + }; + const colorString = `#${componentToString(r)}${componentToString(g)}${componentToString(b)}`; + this.props.onDeactivateColorPicker(colorString); + this.setState({colorInfo: null}); + } } cancelMouseDownTimeout () { if (this.state.mouseDownTimeoutId !== null) { @@ -191,11 +225,13 @@ class Stage extends React.Component { render () { const { vm, // eslint-disable-line no-unused-vars + onActivateColorPicker, // eslint-disable-line no-unused-vars ...props } = this.props; return ( @@ -205,8 +241,23 @@ class Stage extends React.Component { Stage.propTypes = { height: PropTypes.number, + isColorPicking: PropTypes.bool, + onActivateColorPicker: PropTypes.func, + onDeactivateColorPicker: PropTypes.func, vm: PropTypes.instanceOf(VM).isRequired, width: PropTypes.number }; -export default Stage; +const mapStateToProps = state => ({ + isColorPicking: state.colorPicker.active +}); + +const mapDispatchToProps = dispatch => ({ + onActivateColorPicker: () => dispatch(activateColorPicker()), + onDeactivateColorPicker: color => dispatch(deactivateColorPicker(color)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Stage); diff --git a/src/reducers/color-picker.js b/src/reducers/color-picker.js new file mode 100644 index 000000000..fc16354a0 --- /dev/null +++ b/src/reducers/color-picker.js @@ -0,0 +1,40 @@ +const ACTIVATE_COLOR_PICKER = 'scratch-gui/color-picker/ACTIVATE_COLOR_PICKER'; +const DEACTIVATE_COLOR_PICKER = 'scratch-gui/color-picker/DEACTIVATE_COLOR_PICKER'; +const SET_CALLBACK = 'scratch-gui/color-picker/SET_CALLBACK'; + +const initialState = { + active: false, + callback: () => { + throw new Error('Color picker callback not initialized'); + } +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case ACTIVATE_COLOR_PICKER: + return Object.assign({}, state, {active: true, callback: action.callback}); + case DEACTIVATE_COLOR_PICKER: + // Can be called without a string to deactivate without setting color + // i.e. when clicking on the modal background + if (typeof action.color === 'string') { + state.callback(action.color); + } + return Object.assign({}, state, {active: false}); + case SET_CALLBACK: + return Object.assign({}, state, {callback: action.callback}); + default: + return state; + } +}; + +const activateColorPicker = callback => ({type: ACTIVATE_COLOR_PICKER, callback: callback}); +const deactivateColorPicker = color => ({type: DEACTIVATE_COLOR_PICKER, color: color}); +const setCallback = callback => ({type: SET_CALLBACK, callback: callback}); + +export { + reducer as default, + activateColorPicker, + deactivateColorPicker, + setCallback +}; diff --git a/src/reducers/gui.js b/src/reducers/gui.js index dfe05dd84..fef037d92 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -1,4 +1,5 @@ import {combineReducers} from 'redux'; +import colorPickerReducer from './color-picker'; import intlReducer from './intl'; import modalReducer from './modals'; import monitorReducer from './monitors'; @@ -6,8 +7,8 @@ import targetReducer from './targets'; import toolboxReducer from './toolbox'; import vmReducer from './vm'; - export default combineReducers({ + colorPicker: colorPickerReducer, intl: intlReducer, modals: modalReducer, monitors: monitorReducer, From 1dee193c1b2a6c6abb78772530863bb9e129d8e3 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 20 Sep 2017 14:31:04 -0400 Subject: [PATCH 33/46] Update webpack to 3.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 298217f51..dd5a73033 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "svg-url-loader": "^2.1.0", "wav-encoder": "1.3.0", "web-audio-test-api": "^0.5.2", - "webpack": "^3.5.4", + "webpack": "^3.6.0", "webpack-dev-server": "^2.7.0", "xhr": "2.4.0" }, From 5955c18faac64b82a832d1a4ebf08c9a72197321 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 20 Sep 2017 14:35:27 -0400 Subject: [PATCH 34/46] Update to eslint 4.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 298217f51..9b8015fa7 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.7", "enzyme": "^2.8.2", - "eslint": "^4.4.1", + "eslint": "^4.7.1", "eslint-config-scratch": "^4.0.0", "eslint-plugin-import": "^2.7.0", "eslint-plugin-react": "^7.2.1", From 34113a63da5122d08d9089d14ff4d5d03c289cf8 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 20 Sep 2017 14:46:25 -0400 Subject: [PATCH 35/46] Update chromedriver --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 298217f51..deff13339 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "babel-plugin-transform-object-rest-spread": "^6.22.0", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.22.0", - "chromedriver": "^2.31.0", + "chromedriver": "^2.32.0", "classnames": "2.2.5", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.7", From 77f99a5c05fa194a6eee92acb38be7f922ed735d Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 20 Sep 2017 15:44:03 -0400 Subject: [PATCH 36/46] Run the color picker in an interval loop --- src/containers/stage.jsx | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index f45a6628b..6d3d58211 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -54,9 +54,25 @@ class Stage extends React.Component { this.props.isColorPicking !== nextProps.isColorPicking || this.state.colorInfo !== nextState.colorInfo; } + componentDidUpdate (prevProps) { + if (this.props.isColorPicking && !prevProps.isColorPicking) { + this.startColorPickingLoop(); + } else if (!this.props.isColorPicking && prevProps.isColorPicking) { + this.stopColorPickingLoop(); + } + } componentWillUnmount () { this.detachMouseEvents(this.canvas); this.detachRectEvents(); + this.stopColorPickingLoop(); + } + startColorPickingLoop () { + this.intervalId = setInterval(() => { + this.setState({colorInfo: this.getColorInfo(this.pickX, this.pickY)}); + }, 30); + } + stopColorPickingLoop () { + clearInterval(this.intervalId); } attachMouseEvents (canvas) { document.addEventListener('mousemove', this.onMouseMove); @@ -112,6 +128,11 @@ class Stage extends React.Component { onMouseMove (e) { const {x, y} = getEventXY(e); const mousePosition = [x - this.rect.left, y - this.rect.top]; + + // Set the pickX/Y for the color picker loop to pick up + this.pickX = mousePosition[0]; + this.pickY = mousePosition[1]; + if (this.state.mouseDownTimeoutId !== null) { this.cancelMouseDownTimeout(); if (this.state.mouseDown && !this.state.isDragging) { @@ -133,10 +154,6 @@ class Stage extends React.Component { canvasHeight: this.rect.height }; this.props.vm.postIOData('mouse', coordinates); - - if (this.props.isColorPicking) { - this.setState({colorInfo: this.getColorInfo(mousePosition[0], mousePosition[1])}); - } } onMouseUp (e) { const {x, y} = getEventXY(e); From f31bb5ac70ee7e8ffa8a395d798669b8e84a9d1e Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 21 Sep 2017 09:02:42 -0400 Subject: [PATCH 37/46] Update to 2.32.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index deff13339..72d0fc530 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "babel-plugin-transform-object-rest-spread": "^6.22.0", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.22.0", - "chromedriver": "^2.32.0", + "chromedriver": "^2.32.3", "classnames": "2.2.5", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.7", From a5166dfd7581a3070634c408354f5cf469c955bc Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Oct 2017 18:08:27 -0400 Subject: [PATCH 38/46] Remove integration test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 188c8e4b3..0d1dcce73 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "start": "npm run i18n:msgs && webpack-dev-server", "unit-test": "jest test/unit", "integration-test": "npm run build && jest --runInBand test/integration", - "test": "npm run lint && npm run unit-test && npm run integration-test", + "test": "npm run lint && npm run unit-test", "watch": "webpack --progress --colors --watch" }, "author": "Massachusetts Institute of Technology", From e01adbaa371502c625cc4bc8f72fa0c4c603b6fd Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Oct 2017 12:19:00 -0400 Subject: [PATCH 39/46] =?UTF-8?q?Load=20the=20gui=E2=80=99s=20toolbox=20on?= =?UTF-8?q?=20mount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To prevent loading scratch-blocks default toolbox --- src/containers/blocks.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index b15718483..f4c381001 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -56,6 +56,9 @@ class Blocks extends React.Component { const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); + // Load the toolbox from the GUI (otherwise we get the scratch-blocks default toolbox) + this.workspace.updateToolbox(makeToolboxXML()); + // @todo change this when blockly supports UI events addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange); addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange); From b24970f271073c58be9fa2f0ec9b71aec5003d7d Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Oct 2017 12:23:29 -0400 Subject: [PATCH 40/46] Re-order categories --- src/lib/make-toolbox-xml.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index cbdbf771e..442a4ad29 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -620,12 +620,12 @@ const makeToolboxXML = function (categoriesXML) { motion, gap, looks, gap, sound, gap, - pen, gap, - data, gap, events, gap, control, gap, sensing, gap, - operators + pen, gap, + operators, gap, + data ]; if (categoriesXML) { From 8f14d313df9d99ea72538fb25af0991ec5b2ed59 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Oct 2017 13:54:59 -0400 Subject: [PATCH 41/46] =?UTF-8?q?Add=20the=20=E2=80=9Cglide=20to=E2=80=9D?= =?UTF-8?q?=20block=20back=20into=20toolbox=20xml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/make-toolbox-xml.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 442a4ad29..7c26eb2bb 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -71,6 +71,17 @@ const motion = ` + + + + 1 + + + + + + + From 8470b080dd28f35741ad412653a22f9c6e11613e Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 3 Oct 2017 11:40:22 -0400 Subject: [PATCH 42/46] Use the existing value of the prop for the toolbox --- src/containers/blocks.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index f4c381001..7a2a4d908 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -57,7 +57,7 @@ class Blocks extends React.Component { this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); // Load the toolbox from the GUI (otherwise we get the scratch-blocks default toolbox) - this.workspace.updateToolbox(makeToolboxXML()); + this.workspace.updateToolbox(this.props.toolboxXML); // @todo change this when blockly supports UI events addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange); From 611a4c2542645a1e9bc4d78b7081bf6bbbbd872f Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 3 Oct 2017 12:34:18 -0400 Subject: [PATCH 43/46] Add pen transparency blocks back into toolbox --- src/lib/make-toolbox-xml.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 7c26eb2bb..598cec673 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -332,6 +332,20 @@ const pen = ` + + + + 10 + + + + + + + 50 + + + `; From 9a73ef60512a005edc7234c94af1e293ee477523 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Oct 2017 12:18:10 -0400 Subject: [PATCH 44/46] =?UTF-8?q?Add=20=E2=80=9Ctop=E2=80=9D=20category=20?= =?UTF-8?q?to=20toolbox=20xml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/make-toolbox-xml.js | 63 +++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index cbdbf771e..5b2d63249 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -1,5 +1,67 @@ const separator = ''; +const top = ` + + + + + + + + + 10 + + + + + + + 15 + + + + + + + + + + + + + 10 + + + + + + + 10 + + + + + + + 1 + + + + + + + 1 + + + + + 10 + + + + +`; + const motion = ` @@ -617,6 +679,7 @@ const makeToolboxXML = function (categoriesXML) { const everything = [ xmlOpen, + top, gap, motion, gap, looks, gap, sound, gap, From c58316caf58bf03264d0b2d0d08d0384e83f8678 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 4 Oct 2017 13:53:27 -0400 Subject: [PATCH 45/46] Add npm run build to the test to cover for removed integration tests --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0d1dcce73..3bd394669 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "lint": "eslint . --ext .js,.jsx", "start": "npm run i18n:msgs && webpack-dev-server", "unit-test": "jest test/unit", - "integration-test": "npm run build && jest --runInBand test/integration", - "test": "npm run lint && npm run unit-test", + "integration-test": "jest --runInBand test/integration", + "test": "npm run lint && npm run unit-test && npm run build", "watch": "webpack --progress --colors --watch" }, "author": "Massachusetts Institute of Technology", From a4b43144a25a2084927b361c6aede7dc8d77f5f9 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 4 Oct 2017 15:07:58 -0400 Subject: [PATCH 46/46] Publish to NPM --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index ec6ccee6c..33c4de439 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ install: - npm --production=false install - npm --production=false update before_deploy: +- npm --no-git-tag-version version 0.1.0-prerelease.$(date +%Y%m%d%H%M%S) - git config --global user.email $(git log --pretty=format:"%ae" -n1) - git config --global user.name $(git log --pretty=format:"%an" -n1) deploy: @@ -32,3 +33,10 @@ deploy: branch: master skip_cleanup: true script: npm run --silent deploy -- -x -a -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git +- provider: npm + on: + branch: master + skip_cleanup: true + email: $NPM_EMAIL + api_key: $NPM_TOKEN +