Merge pull request #739 from LLK/develop

Pull develop into master
This commit is contained in:
Ray Schamp authored and GitHub committed 2017-10-04 16:55:49 -04:00
commit ee54c35fd9
39 files changed
+1619 -107

No files matched your search

+8
View File
@@ -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
+20 -20
View File
@@ -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 test/integration",
"test": "npm run lint && npm run unit-test && npm run integration-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",
@@ -28,31 +28,31 @@
"react-dom": "^15.6.1"
},
"devDependencies": {
"autoprefixer": "7.1.2",
"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",
"chromedriver": "^2.32.3",
"classnames": "2.2.5",
"copy-webpack-plugin": "4.0.1",
"css-loader": "0.28.5",
"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",
"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",
"jest": "^21.0.0",
"lodash.bindall": "4.4.0",
"lodash.debounce": "4.0.8",
"lodash.defaultsdeep": "4.6.0",
@@ -66,16 +66,16 @@
"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": "2.2.6",
"react-intl": "2.3.0",
"react-draggable": "3.0.3",
"react-intl": "2.4.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",
"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",
@@ -90,11 +90,11 @@
"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",
"webpack-dev-server": "^2.7.0",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.8.2",
"xhr": "2.4.0"
},
"jest": {
+1
View File
@@ -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) {
+21
View File
@@ -148,3 +148,24 @@
height: $stage-menu-height;
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;
}
.extension-button > div {
margin-top: 0;
}
+27 -2
View File
@@ -12,18 +12,31 @@ import Controls from '../../containers/controls.jsx';
import TargetPane from '../../containers/target-pane.jsx';
import SoundTab from '../../containers/sound-tab.jsx';
import Stage from '../../containers/stage.jsx';
import {FormattedMessage} from 'react-intl';
import Box from '../box/box.jsx';
import IconButton from '../icon-button/icon-button.jsx';
import MenuBar from '../menu-bar/menu-bar.jsx';
import layout from '../../lib/layout-constants.js';
import styles from './gui.css';
import addExtensionIcon from './icon--extensions.svg';
const addExtensionMessage = (
<FormattedMessage
defaultMessage="Extensions"
description="Button to add an extension in the target pane"
id="targetPane.addExtension"
/>
);
const GUIComponent = props => {
const {
basePath,
children,
enableExtensions,
vm,
onExtensionButtonClick,
onTabSelect,
tabIndex,
...componentProps
@@ -77,12 +90,22 @@ const GUIComponent = props => {
vm={vm}
/>
</Box>
<Box className={styles.extensionButtonContainer}>
<IconButton
className={classNames(styles.extensionButton, {
[styles.hidden]: !enableExtensions
})}
img={addExtensionIcon}
title={addExtensionMessage}
onClick={onExtensionButtonClick}
/>
</Box>
</TabPanel>
<TabPanel className={tabClassNames.tabPanel}>
<CostumeTab vm={vm} />
{tabIndex === 1 ? <CostumeTab vm={vm} /> : null}
</TabPanel>
<TabPanel className={tabClassNames.tabPanel}>
<SoundTab vm={vm} />
{tabIndex === 2 ? <SoundTab vm={vm} /> : null}
</TabPanel>
</Tabs>
</Box>
@@ -115,6 +138,8 @@ const GUIComponent = props => {
GUIComponent.propTypes = {
basePath: PropTypes.string,
children: PropTypes.node,
enableExtensions: PropTypes.bool,
onExtensionButtonClick: PropTypes.func,
onTabSelect: PropTypes.func,
tabIndex: PropTypes.number,
vm: PropTypes.instanceOf(VM).isRequired
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="34px" height="34px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>add extension</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="add-extension">
<g transform="translate(4.000000, 5.000000)">
<path d="M18,15.51 C18,15.786 17.776,16.01 17.5,16.01 L7.197,16.01 C7.064,16.01 6.937,16.062 6.844,16.156 L6.146,16.854 C6.053,16.947 5.926,17 5.793,17 L3.207,17 C3.074,17 2.947,16.947 2.854,16.854 L2.156,16.156 C2.062,16.062 1.936,16.01 1.803,16.01 L0.5,16.01 C0.224,16.01 0,15.786 0,15.51 L0,11.5 C0,11.224 0.224,11 0.5,11 L1.793,11 C1.926,11 2.053,11.053 2.146,11.146 L2.854,11.854 C2.947,11.947 3.074,12 3.207,12 L5.793,12 C5.926,12 6.053,11.947 6.146,11.854 L6.854,11.146 C6.947,11.053 7.074,11 7.207,11 L17.5,11 C17.776,11 18,11.224 18,11.5 L18,15.51 Z M12.9961,8.51 C12.9961,8.786 12.7721,9.01 12.4961,9.01 L7.1971,9.01 C7.0641,9.01 6.9371,9.062 6.8441,9.156 L6.1461,9.854 C6.0531,9.947 5.9261,10 5.7931,10 L3.2071,10 C3.0741,10 2.9471,9.947 2.8531,9.854 L2.1561,9.156 C2.0621,9.062 1.9351,9.01 1.8031,9.01 L0.5001,9.01 C0.2241,9.01 0.0001,8.786 0.0001,8.51 L0.0001,4.5 C0.0001,4.224 0.2241,4 0.5001,4 L1.7931,4 C1.9261,4 2.0531,4.053 2.1461,4.146 L2.8531,4.854 C2.9471,4.947 3.0741,5 3.2071,5 L5.7931,5 C5.9261,5 6.0531,4.947 6.1461,4.854 L6.8531,4.146 C6.9471,4.053 7.0741,4 7.2071,4 L12.4961,4 C12.7721,4 12.9961,4.224 12.9961,4.5 L12.9961,8.51 Z" id="Combined-Shape" fill="#4C97FF"></path>
<g id="+" transform="translate(16.000000, 0.000000)" stroke="#4C97FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2,0 L2,4" id="Shape"></path>
<path d="M4,2 L0,2" id="Shape"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+1 -4
View File
@@ -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 (
<ModalComponent
className={styles.modalContent}
contentLabel={this.props.title}
filterQuery={this.state.filterQuery}
visible={this.props.visible}
onFilterChange={this.handleFilterChange}
onFilterClear={this.handleFilterClear}
onRequestClose={this.props.onRequestClose}
@@ -93,8 +91,7 @@ LibraryComponent.propTypes = {
onItemMouseLeave: PropTypes.func,
onItemSelected: PropTypes.func,
onRequestClose: PropTypes.func,
title: PropTypes.string.isRequired,
visible: PropTypes.bool.isRequired
title: PropTypes.string.isRequired
};
export default LibraryComponent;
+5
View File
@@ -0,0 +1,5 @@
.color-picker {
position: absolute;
border-radius: 100%;
border: 1px solid #222;
}
+104
View File
@@ -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 (
<Box
{...boxProps}
className={styles.colorPicker}
componentRef={this.setCanvas}
element="canvas"
height={colorInfo.height}
style={{
top: colorInfo.y - ((zoomScale * colorInfo.height) / 2),
left: colorInfo.x - ((zoomScale * colorInfo.width) / 2),
width: colorInfo.width * zoomScale,
height: colorInfo.height * zoomScale
}}
width={colorInfo.width}
/>
);
}
}
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;
+2 -3
View File
@@ -13,9 +13,9 @@ class ModalComponent extends React.Component {
render () {
return (
<ReactModal
isOpen
className={classNames(styles.modalContent, this.props.className)}
contentLabel={this.props.contentLabel}
isOpen={this.props.visible}
overlayClassName={styles.modalOverlay}
ref={m => (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;
-1
View File
@@ -7,7 +7,6 @@ import styles from './prompt.css';
const PromptComponent = props => (
<Modal
visible
className={styles.modalContent}
contentLabel={props.title}
onRequestClose={props.onCancel}
@@ -8,7 +8,6 @@ import styles from './record-modal.css';
const RecordModal = props => (
<Modal
visible
className={styles.modalContent}
contentLabel={'Record Sound'}
onRequestClose={props.onCancel}
+18 -1
View File
@@ -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;
}
+36 -11
View File
@@ -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 (
<Box className={styles.stageWrapper}>
<div>
<Box
className={styles.stage}
componentRef={canvasRef}
element="canvas"
height={height}
width={width}
{...boxProps}
/>
<Box className={styles.monitorWrapper}>
<MonitorList />
className={classNames(styles.stageWrapper, {
[styles.withColorPicker]: isColorPicking
})}
>
<Box
className={styles.stage}
componentRef={canvasRef}
element="canvas"
height={height}
width={width}
{...boxProps}
/>
<Box className={styles.monitorWrapper}>
<MonitorList />
</Box>
{isColorPicking && colorInfo ? (
<Box className={styles.colorPickerWrapper}>
<Loupe colorInfo={colorInfo} />
</Box>
) : null}
</Box>
</Box>
{isColorPicking ? (
<Box
className={styles.colorPickerBackground}
onClick={onDeactivateColorPicker}
/>
) : null}
</div>
);
};
StageComponent.propTypes = {
canvasRef: PropTypes.func,
colorInfo: Loupe.propTypes.colorInfo,
height: PropTypes.number,
isColorPicking: PropTypes.bool,
onDeactivateColorPicker: PropTypes.func,
width: PropTypes.number
};
StageComponent.defaultProps = {
+42 -27
View File
@@ -3,11 +3,12 @@ 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';
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';
@@ -21,6 +22,7 @@ import styles from './target-pane.css';
*/
const TargetPane = ({
editingTarget,
extensionLibraryVisible,
backdropLibraryVisible,
costumeLibraryVisible,
soundLibraryVisible,
@@ -37,13 +39,14 @@ const TargetPane = ({
onRequestCloseCostumeLibrary,
onRequestCloseSoundLibrary,
onRequestCloseSpriteLibrary,
onRequestCloseExtensionLibrary,
onSelectSprite,
stage,
sprites,
vm,
...componentProps
}) => (
<Box
<div
className={styles.targetPane}
{...componentProps}
>
@@ -61,7 +64,7 @@ const TargetPane = ({
onNewSpriteClick={onNewSpriteClick}
onSelectSprite={onSelectSprite}
/>
<Box className={styles.stageSelectorWrapper}>
<div className={styles.stageSelectorWrapper}>
{stage.id && <StageSelector
assetId={
stage.costume &&
@@ -72,30 +75,40 @@ const TargetPane = ({
selected={stage.id === editingTarget}
onSelect={onSelectSprite}
/>}
<Box>
<SpriteLibrary
visible={spriteLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseSpriteLibrary}
/>
<CostumeLibrary
visible={costumeLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseCostumeLibrary}
/>
<SoundLibrary
visible={soundLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseSoundLibrary}
/>
<BackdropLibrary
visible={backdropLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseBackdropLibrary}
/>
</Box>
</Box>
</Box>
<div>
{extensionLibraryVisible ? (
<ExtensionLibrary
vm={vm}
onRequestClose={onRequestCloseExtensionLibrary}
/>
) : null}
{spriteLibraryVisible ? (
<SpriteLibrary
vm={vm}
onRequestClose={onRequestCloseSpriteLibrary}
/>
) : null}
{costumeLibraryVisible ? (
<CostumeLibrary
vm={vm}
onRequestClose={onRequestCloseCostumeLibrary}
/>
) : null}
{soundLibraryVisible ? (
<SoundLibrary
vm={vm}
onRequestClose={onRequestCloseSoundLibrary}
/>
) : null}
{backdropLibraryVisible ? (
<BackdropLibrary
vm={vm}
onRequestClose={onRequestCloseBackdropLibrary}
/>
) : null}
</div>
</div>
</div>
);
const spriteShape = PropTypes.shape({
@@ -120,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,
@@ -130,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,
+4 -2
View File
@@ -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();
}
-2
View File
@@ -29,7 +29,6 @@ class BackdropLibrary extends React.Component {
<LibraryComponent
data={backdropLibraryContent}
title="Backdrop Library"
visible={this.props.visible}
onItemSelected={this.handleItemSelect}
onRequestClose={this.props.onRequestClose}
/>
@@ -39,7 +38,6 @@ class BackdropLibrary extends React.Component {
BackdropLibrary.propTypes = {
onRequestClose: PropTypes.func,
visible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
+71 -5
View File
@@ -1,6 +1,7 @@
import bindAll from 'lodash.bindall';
import debounce from 'lodash.debounce';
import defaultsDeep from 'lodash.defaultsdeep';
import makeToolboxXML from '../lib/make-toolbox-xml';
import PropTypes from 'prop-types';
import React from 'react';
import VMScratchBlocks from '../lib/blocks';
@@ -8,6 +9,10 @@ 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';
import {activateColorPicker} from '../reducers/color-picker';
const addFunctionListener = (object, property, callback) => {
const oldFn = object[property];
object[property] = function () {
@@ -31,6 +36,7 @@ class Blocks extends React.Component {
'onScriptGlowOff',
'onBlockGlowOn',
'onBlockGlowOff',
'handleExtensionAdded',
'onTargetsUpdate',
'onVisualReport',
'onWorkspaceUpdate',
@@ -45,9 +51,14 @@ 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);
// Load the toolbox from the GUI (otherwise we get the scratch-blocks default toolbox)
this.workspace.updateToolbox(this.props.toolboxXML);
// @todo change this when blockly supports UI events
addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange);
addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange);
@@ -55,9 +66,20 @@ class Blocks extends React.Component {
this.attachVM();
}
shouldComponentUpdate (nextProps, nextState) {
return this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible;
return (
this.state.prompt !== nextState.prompt ||
this.props.isVisible !== nextProps.isVisible ||
this.props.toolboxXML !== nextProps.toolboxXML
);
}
componentDidUpdate (prevProps) {
if (prevProps.toolboxXML !== this.props.toolboxXML) {
const selectedCategoryName = this.workspace.toolbox_.getSelectedItem().name_;
this.workspace.updateToolbox(this.props.toolboxXML);
// Blockly throws if we don't select a category after updating the toolbox.
/** @TODO Find a way to avoid the exception without accessing private properties. */
this.setToolboxSelectedItemByName(selectedCategoryName);
}
if (this.props.isVisible === prevProps.isVisible) {
return;
}
@@ -77,6 +99,20 @@ class Blocks extends React.Component {
this.detachVM();
this.workspace.dispose();
}
/**
* Select a particular category in the toolbox by specifying the category name.
* This is a workaround for a bug: @see {@link componentDidUpdate} above.
* @TODO Remove this or reimplement using only public APIs.
* @param {string} name - the name of the category to select.
*/
setToolboxSelectedItemByName (name) {
const categories = this.workspace.toolbox_.categoryMenu_.categories_;
for (let i = 0; i < categories.length; i++) {
if (categories[i].name_ === name) {
this.workspace.toolbox_.setSelectedItem(categories[i]);
}
}
}
attachVM () {
this.workspace.addChangeListener(this.props.vm.blockListener);
this.flyoutWorkspace = this.workspace
@@ -91,6 +127,7 @@ class Blocks extends React.Component {
this.props.vm.addListener('VISUAL_REPORT', this.onVisualReport);
this.props.vm.addListener('workspaceUpdate', this.onWorkspaceUpdate);
this.props.vm.addListener('targetsUpdate', this.onTargetsUpdate);
this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded);
}
detachVM () {
this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
@@ -100,6 +137,7 @@ class Blocks extends React.Component {
this.props.vm.removeListener('VISUAL_REPORT', this.onVisualReport);
this.props.vm.removeListener('workspaceUpdate', this.onWorkspaceUpdate);
this.props.vm.removeListener('targetsUpdate', this.onTargetsUpdate);
this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded);
}
updateToolboxBlockValue (id, value) {
const block = this.workspace
@@ -167,6 +205,12 @@ class Blocks extends React.Component {
this.workspace.resize();
}
}
handleExtensionAdded (blocksInfo) {
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json));
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
const toolboxXML = makeToolboxXML(dynamicBlocksXML);
this.props.onExtensionAdded(toolboxXML);
}
setBlocks (blocks) {
this.blocks = blocks;
}
@@ -181,12 +225,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
options,
vm,
isVisible,
onActivateColorPicker,
onExtensionAdded,
toolboxXML,
...props
} = this.props;
/* eslint-enable no-unused-vars */
return (
<div>
<BlocksComponent
@@ -209,6 +258,8 @@ class Blocks extends React.Component {
Blocks.propTypes = {
isVisible: PropTypes.bool,
onActivateColorPicker: PropTypes.func,
onExtensionAdded: PropTypes.func,
options: PropTypes.shape({
media: PropTypes.string,
zoom: PropTypes.shape({
@@ -230,6 +281,7 @@ Blocks.propTypes = {
}),
comments: PropTypes.bool
}),
toolboxXML: PropTypes.string,
vm: PropTypes.instanceOf(VM).isRequired
};
@@ -264,4 +316,18 @@ Blocks.defaultProps = {
options: Blocks.defaultOptions
};
export default Blocks;
const mapStateToProps = state => ({
toolboxXML: state.toolbox.toolboxXML
});
const mapDispatchToProps = dispatch => ({
onActivateColorPicker: callback => dispatch(activateColorPicker(callback)),
onExtensionAdded: toolboxXML => {
dispatch(updateToolbox(toolboxXML));
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Blocks);
-2
View File
@@ -29,7 +29,6 @@ class CostumeLibrary extends React.PureComponent {
<LibraryComponent
data={costumeLibraryContent}
title="Costume Library"
visible={this.props.visible}
onItemSelected={this.handleItemSelected}
onRequestClose={this.props.onRequestClose}
/>
@@ -39,7 +38,6 @@ class CostumeLibrary extends React.PureComponent {
CostumeLibrary.propTypes = {
onRequestClose: PropTypes.func,
visible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
+48
View File
@@ -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 (
<LibraryComponent
data={extensionLibraryThumbnailData}
title="Extension Library"
visible={this.props.visible}
onItemSelected={this.handleItemSelect}
onRequestClose={this.props.onRequestClose}
/>
);
}
}
ExtensionLibrary.propTypes = {
onRequestClose: PropTypes.func,
visible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types
};
export default ExtensionLibrary;
+16 -1
View File
@@ -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';
@@ -43,6 +46,7 @@ class GUI extends React.Component {
} = this.props;
return (
<GUIComponent
enableExtensions={window.location.search.includes('extensions')}
tabIndex={this.state.tabIndex}
vm={vm}
onTabSelect={this.handleTabSelect}
@@ -62,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);
-1
View File
@@ -130,7 +130,6 @@ RecordModal.propTypes = {
};
const mapStateToProps = state => ({
visible: state.modals.soundRecorder,
vm: state.vm
});
+6 -1
View File
@@ -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;
+2 -5
View File
@@ -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;
@@ -76,7 +75,6 @@ class SoundLibrary extends React.PureComponent {
<LibraryComponent
data={soundLibraryThumbnailData}
title="Sound Library"
visible={this.props.visible}
onItemMouseEnter={this.handleItemMouseEnter}
onItemMouseLeave={this.handleItemMouseLeave}
onItemSelected={this.handleItemSelected}
@@ -88,7 +86,6 @@ class SoundLibrary extends React.PureComponent {
SoundLibrary.propTypes = {
onRequestClose: PropTypes.func,
visible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
+1 -1
View File
@@ -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 ? (
<SoundEditor soundIndex={this.state.selectedSoundIndex} />
) : null}
{this.props.soundRecorderVisible ? (
+2 -4
View File
@@ -24,8 +24,8 @@ class SpriteLibrary extends React.PureComponent {
sprites: spriteLibraryContent
};
}
componentWillReceiveProps (newProps) {
if (!newProps.visible) clearInterval(this.intervalId);
componentWillUnmount () {
clearInterval(this.intervalId);
}
handleItemSelect (item) {
this.props.vm.addSprite2(JSON.stringify(item.json));
@@ -66,7 +66,6 @@ class SpriteLibrary extends React.PureComponent {
<LibraryComponent
data={this.state.sprites}
title="Sprite Library"
visible={this.props.visible}
onItemMouseEnter={this.handleMouseEnter}
onItemMouseLeave={this.handleMouseLeave}
onItemSelected={this.handleItemSelect}
@@ -78,7 +77,6 @@ class SpriteLibrary extends React.PureComponent {
SpriteLibrary.propTypes = {
onRequestClose: PropTypes.func,
visible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
+72 -4
View File
@@ -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,12 +48,31 @@ 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;
}
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);
@@ -79,6 +108,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.
@@ -92,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) {
@@ -157,6 +198,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 +242,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 (
<StageComponent
canvasRef={this.setCanvas}
colorInfo={this.state.colorInfo}
onDoubleClick={this.handleDoubleClick}
{...props}
/>
@@ -205,8 +258,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);
+6 -1
View File
@@ -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());
},
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
extends: ['scratch'], // no ES6
env: {
worker: true
},
globals: {
Scratch: true
}
};
@@ -0,0 +1,230 @@
var ExampleExtension = function () {
};
/**
* @return {object} This extension's metadata.
*/
ExampleExtension.prototype.getInfo = function () {
return {
// Required: the machine-readable name of this extension.
// Will be used as the extension's namespace. Must not contain a '.' character.
id: 'someBlocks',
// Optional: the human-readable name of this extension as string.
// This and any other string to be displayed in the Scratch UI may either be
// a string or a call to `intlDefineMessage`; a plain string will not be
// translated whereas a call to `intlDefineMessage` will connect the string
// to the translation map (see below). The `intlDefineMessage` call is
// similar to `defineMessages` from `react-intl` in form, but will actually
// call some extension support code to do its magic. For example, we will
// internally namespace the messages such that two extensions could have
// messages with the same ID without colliding.
// See also: https://github.com/yahoo/react-intl/wiki/API#definemessages
name: 'Some Blocks',
// Optional: URI for an icon for this extension. Data URI OK.
// If not present, use a generic icon.
// TODO: what file types are OK? All web images? Just PNG?
iconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DE' +
'UIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==',
// Optional: Link to documentation content for this extension.
// If not present, offer no link.
docsURI: 'https://....',
// Required: the list of blocks implemented by this extension,
// in the order intended for display.
blocks: [
{
opcode: 'example-noop',
blockType: Scratch.BlockType.COMMAND,
blockAllThreads: false,
text: 'do nothing',
func: 'noop'
},
{
opcode: 'example-conditional',
blockType: Scratch.BlockType.CONDITIONAL,
branchCount: 4,
isTerminal: true,
blockAllThreads: false,
text: 'choose [BRANCH]',
arguments: {
BRANCH: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 1
}
},
func: 'noop'
},
{
// Required: the machine-readable name of this operation.
// This will appear in project JSON. Must not contain a '.' character.
opcode: 'myReporter', // becomes 'someBlocks.myReporter'
// Required: the kind of block we're defining, from a predefined list:
// 'command' - a normal command block, like "move {} steps"
// 'reporter' - returns a value, like "direction"
// 'Boolean' - same as 'reporter' but returns a Boolean value
// 'hat' - starts a stack if its value is truthy
// 'conditional' - control flow, like "if {}" or "repeat {}"
// A 'conditional' block may return the one-based index of a branch
// to run, or it may return zero/falsy to run no branch. Each time a
// child branch finishes, the block is called again. This is only a
// slight change to the current model for control flow blocks, and is
// also compatible with returning true/false for an "if" or "repeat"
// block.
// TODO: Consider Blockly-like nextStatement, previousStatement, and
// output attributes as an alternative. Those are more flexible, but
// allow bad combinations.
blockType: Scratch.BlockType.REPORTER,
// Required for conditional blocks, ignored for others: the number of
// child branches this block controls. An "if" or "repeat" block would
// specify a branch count of 1; an "if-else" block would specify a
// branch count of 2.
// TODO: should we support dynamic branch count for "switch"-likes?
branchCount: 0,
// Optional, default false: whether or not this block ends a stack.
// The "forever" and "stop all" blocks would specify true here.
isTerminal: true,
// Optional, default false: whether or not to block all threads while
// this block is busy. This is for things like the "touching color"
// block in compatibility mode, and is only needed if the VM runs in a
// worker. We might even consider omitting it from extension docs...
blockAllThreads: false,
// Required: the human-readable text on this block, including argument
// placeholders. Argument placeholders should be in [MACRO_CASE] and
// must be [ENCLOSED_WITHIN_SQUARE_BRACKETS].
text: 'letter [LETTER_NUM] of [TEXT]',
// Required: describe each argument.
// Note that this is an array: the order of arguments will be used
arguments: {
// Required: the ID of the argument, which will be the name in the
// args object passed to the implementation function.
LETTER_NUM: {
// Required: type of the argument / shape of the block input
type: Scratch.ArgumentType.NUMBER,
// Optional: the default value of the argument
defaultValue: 1
},
// Required: the ID of the argument, which will be the name in the
// args object passed to the implementation function.
TEXT: {
// Required: type of the argument / shape of the block input
type: Scratch.ArgumentType.STRING,
// Optional: the default value of the argument
defaultValue: 'text'
}
},
// Optional: a string naming the function implementing this block.
// If this is omitted, use the opcode string.
func: 'myReporter',
// Optional: list of target types for which this block should appear.
// If absent, assume it applies to all builtin targets -- that is:
// ['sprite', 'stage']
filter: ['someBlocks.wedo2', 'sprite', 'stage']
},
{
opcode: 'example-Boolean',
blockType: Scratch.BlockType.BOOLEAN,
text: 'return true',
func: 'returnTrue'
},
{
opcode: 'example-hat',
blockType: Scratch.BlockType.HAT,
text: 'after forever',
func: 'returnFalse'
},
{
// Another block...
}
],
// Optional: define extension-specific menus here.
menus: {
// Required: an identifier for this menu, unique within this extension.
menuA: [
// Static menu: list items which should appear in the menu.
{
// Required: the value of the menu item when it is chosen.
value: 'itemId1',
// Optional: the human-readable label for this item.
// Use `value` as the text if this is absent.
text: 'Item One'
},
// The simplest form of a list item is a string which will be used as
// both value and text.
'itemId2'
],
// Dynamic menu: a string naming a function which returns an array as above.
// Called each time the menu is opened.
menuB: 'getItemsForMenuB'
},
// Optional: translations
translation_map: {
de: {
'extensionName': 'Einige Blöcke',
'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]',
'myReporter.TEXT_default': 'Text',
'menuA_item1': 'Artikel eins',
// Dynamic menus can be translated too
'menuB_example': 'Beispiel',
// This message contains ICU placeholders (see `myReporter()` below)
'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.'
},
it: {
// ...
}
},
// Optional: list new target type(s) provided by this extension.
targetTypes: [
'wedo2', // automatically transformed to 'someBlocks.wedo2'
'speech' // automatically transformed to 'someBlocks.speech'
]
};
};
/**
* Implement myReporter.
* @param {object} args - the block's arguments.
* @property {number} LETTER_NUM - the string value of the argument.
* @property {string} TEXT - the string value of the argument.
* @returns {string} a string which includes the block argument value.
*/
ExampleExtension.prototype.myReporter = function (args) {
// Note: this implementation is not Unicode-clean; it's just here as an example.
const result = args.TEXT.charAt(args.LETTER_NUM);
return ['Letter ', args.LETTER_NUM, ' of ', args.TEXT, ' is ', result, '.'].join('');
};
ExampleExtension.prototype.noop = function () {
};
ExampleExtension.prototype.returnTrue = function () {
return true;
};
ExampleExtension.prototype.returnFalse = function () {
return false;
};
Scratch.extensions.register(new ExampleExtension());
+19 -6
View File
@@ -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 () {
@@ -100,6 +105,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_']
+7
View File
@@ -0,0 +1,7 @@
[
{
"name": "Some Blocks",
"extensionURL": "static/extensions/example-extension.js",
"md5": "76f99773dd4eb59f86cd11bba6ad3cb9.svg"
}
]
+728
View File
@@ -0,0 +1,728 @@
const separator = '<sep gap="45"/>';
const top = `
<category name="Top" colour="#FFFFFF" secondaryColour="#CCCCCC">
<block type="event_whenflagclicked"/>
<block type="event_whenkeypressed">
</block>
<block type="event_whenthisspriteclicked"/>
<block type="motion_movesteps">
<value name="STEPS">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_turnright">
<value name="DEGREES">
<shadow type="math_number">
<field name="NUM">15</field>
</shadow>
</value>
</block>
<block type="motion_ifonedgebounce"/>
<block type="sound_playuntildone">
<value name="SOUND_MENU">
<shadow type="sound_sounds_menu"/>
</value>
</block>
<block type="looks_changeeffectby">
<value name="CHANGE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="control_repeat">
<value name="TIMES">
<shadow type="math_whole_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="control_wait">
<value name="DURATION">
<shadow type="math_positive_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="operator_random">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
</category>
`;
const motion = `
<category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
<block type="motion_movesteps">
<value name="STEPS">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_turnright">
<value name="DEGREES">
<shadow type="math_number">
<field name="NUM">15</field>
</shadow>
</value>
</block>
<block type="motion_turnleft">
<value name="DEGREES">
<shadow type="math_number">
<field name="NUM">15</field>
</shadow>
</value>
</block>
<block type="motion_pointindirection">
<value name="DIRECTION">
<shadow type="math_angle">
<field name="NUM">90</field>
</shadow>
</value>
</block>
<block type="motion_pointtowards">
<value name="TOWARDS">
<shadow type="motion_pointtowards_menu">
</shadow>
</value>
</block>
<block type="motion_gotoxy">
<value name="X">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
<value name="Y">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_goto">
<value name="TO">
<shadow type="motion_goto_menu">
</shadow>
</value>
</block>
<block type="motion_glidesecstoxy">
<value name="SECS">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="X">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
<value name="Y">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_glideto" id="motion_glideto">
<value name="SECS">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="motion_glideto_menu">
</shadow>
</value>
</block>
<block type="motion_changexby">
<value name="DX">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_setx">
<value name="X">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_changeyby">
<value name="DY">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_sety">
<value name="Y">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_ifonedgebounce"/>
<block type="motion_setrotationstyle"/>
<block type="motion_xposition"/>
<block type="motion_yposition"/>
<block type="motion_direction"/>
</category>
`;
const looks = `
<category name="Looks" colour="#9966FF" secondaryColour="#774DCB">
<block type="looks_show"/>
<block type="looks_hide"/>
<block type="looks_switchcostumeto">
<value name="COSTUME">
<shadow type="looks_costume"/>
</value>
</block>
<block type="looks_nextcostume"/>
<block type="looks_nextbackdrop"/>
<block type="looks_switchbackdropto">
<value name="BACKDROP">
<shadow type="looks_backdrops"/>
</value>
</block>
<block type="looks_switchbackdroptoandwait">
<value name="BACKDROP">
<shadow type="looks_backdrops"/>
</value>
</block>
<block type="looks_changeeffectby">
<value name="CHANGE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="looks_seteffectto">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="looks_cleargraphiceffects"/>
<block type="looks_changesizeby">
<value name="CHANGE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="looks_setsizeto">
<value name="SIZE">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="looks_gotofront"/>
<block type="looks_gobacklayers">
<value name="NUM">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="looks_costumeorder"/>
<block type="looks_backdroporder"/>
<block type="looks_backdropname"/>
<block type="looks_size"/>
</category>
`;
const sound = `
<category name="Sound" colour="#D65CD6" secondaryColour="#BD42BD">
<block type="sound_play">
<value name="SOUND_MENU">
<shadow type="sound_sounds_menu"/>
</value>
</block>
<block type="sound_playuntildone">
<value name="SOUND_MENU">
<shadow type="sound_sounds_menu"/>
</value>
</block>
<block type="sound_stopallsounds"/>
<block type="sound_playdrumforbeats">
<value name="DRUM">
<shadow type="sound_drums_menu"/>
</value>
<value name="BEATS">
<shadow type="math_number">
<field name="NUM">0.25</field>
</shadow>
</value>
</block>
<block type="sound_restforbeats">
<value name="BEATS">
<shadow type="math_number">
<field name="NUM">0.25</field>
</shadow>
</value>
</block>
<block type="sound_playnoteforbeats">
<value name="NOTE">
<shadow type="math_number">
<field name="NUM">60</field>
</shadow>
</value>
<value name="BEATS">
<shadow type="math_number">
<field name="NUM">0.5</field>
</shadow>
</value>
</block>
<block type="sound_setinstrumentto">
<value name="INSTRUMENT">
<shadow type="sound_instruments_menu"/>
</value>
</block>
<block type="sound_changeeffectby">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="sound_seteffectto">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="sound_cleareffects"/>
<block type="sound_changevolumeby">
<value name="VOLUME">
<shadow type="math_number">
<field name="NUM">-10</field>
</shadow>
</value>
</block>
<block type="sound_setvolumeto">
<value name="VOLUME">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="sound_volume"/>
<block type="sound_changetempoby">
<value name="TEMPO">
<shadow type="math_number">
<field name="NUM">20</field>
</shadow>
</value>
</block>
<block type="sound_settempotobpm">
<value name="TEMPO">
<shadow type="math_number">
<field name="NUM">60</field>
</shadow>
</value>
</block>
<block type="sound_tempo"/>
</category>
`;
const pen = `
<category name="Pen" colour="#00B295" secondaryColour="#0B8E69">
<block type="pen_clear"/>
<block type="pen_stamp"/>
<block type="pen_pendown"/>
<block type="pen_penup"/>
<block type="pen_setpencolortocolor">
<value name="COLOR">
<shadow type="colour_picker">
</shadow>
</value>
</block>
<block type="pen_changepencolorby">
<value name="COLOR">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="pen_setpencolortonum">
<value name="COLOR">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="pen_changepenshadeby">
<value name="SHADE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="pen_setpenshadeto">
<value name="SHADE">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
<block type="pen_changepensizeby">
<value name="SIZE">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="pen_setpensizeto">
<value name="SIZE">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="pen_changepentransparencyby" id="pen_changepentransparencyby">
<value name="TRANSPARENCY">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="pen_setpentransparencyto" id="pen_setpentransparencyto">
<value name="TRANSPARENCY">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
</category>
`;
const events = `
<category name="Events" colour="#FFD500" secondaryColour="#CC9900">
<block type="event_whenflagclicked"/>
<block type="event_whenkeypressed">
</block>
<block type="event_whenthisspriteclicked"/>
<block type="event_whenbackdropswitchesto">
</block>
<block type="event_whengreaterthan">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="event_whenbroadcastreceived">
</block>
<block type="event_broadcast">
<value name="BROADCAST_OPTION">
<shadow type="event_broadcast_menu"/>
</value>
</block>
<block type="event_broadcastandwait">
<value name="BROADCAST_OPTION">
<shadow type="event_broadcast_menu"/>
</value>
</block>
</category>
`;
const control = `
<category name="Control" colour="#FFAB19" secondaryColour="#CF8B17">
<block type="control_wait">
<value name="DURATION">
<shadow type="math_positive_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="control_repeat">
<value name="TIMES">
<shadow type="math_whole_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="control_forever"/>
<block type="control_if"/>
<block type="control_if_else"/>
<block type="control_wait_until"/>
<block type="control_repeat_until"/>
<block type="control_stop"/>
<block type="control_start_as_clone"/>
<block type="control_create_clone_of">
<value name="CLONE_OPTION">
<shadow type="control_create_clone_of_menu"/>
</value>
</block>
<block type="control_delete_this_clone"/>
</category>
`;
const sensing = `
<category name="Sensing" colour="#4CBFE6" secondaryColour="#2E8EB8">
<block type="sensing_touchingobject">
<value name="TOUCHINGOBJECTMENU">
<shadow type="sensing_touchingobjectmenu"/>
</value>
</block>
<block type="sensing_touchingcolor">
<value name="COLOR">
<shadow type="colour_picker"/>
</value>
</block>
<block type="sensing_coloristouchingcolor">
<value name="COLOR">
<shadow type="colour_picker"/>
</value>
<value name="COLOR2">
<shadow type="colour_picker"/>
</value>
</block>
<block type="sensing_distanceto">
<value name="DISTANCETOMENU">
<shadow type="sensing_distancetomenu"/>
</value>
</block>
<block type="sensing_keypressed">
<value name="KEY_OPTION">
<shadow type="sensing_keyoptions"/>
</value>
</block>
<block type="sensing_mousedown"/>
<block type="sensing_mousex"/>
<block type="sensing_mousey"/>
<block type="sensing_loudness"/>
<block type="sensing_timer"/>
<block type="sensing_resettimer"/>
<block type="sensing_of">
<value name="PROPERTY">
<shadow type="sensing_of_property_menu"/>
</value>
<value name="OBJECT">
<shadow type="sensing_of_object_menu"/>
</value>
</block>
<block type="sensing_current">
<value name="CURRENTMENU">
<shadow type="sensing_currentmenu"/>
</value>
</block>
<block type="sensing_dayssince2000"/>
</category>
`;
const operators = `
<category name="Operators" colour="#40BF4A" secondaryColour="#389438">
<block type="operator_add">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_subtract">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_multiply">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_divide">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_random">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="operator_lt">
<value name="OPERAND1">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
<value name="OPERAND2">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
</block>
<block type="operator_equals">
<value name="OPERAND1">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
<value name="OPERAND2">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
</block>
<block type="operator_gt">
<value name="OPERAND1">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
<value name="OPERAND2">
<shadow type="text">
<field name="TEXT"/>
</shadow>
</value>
</block>
<block type="operator_and"/>
<block type="operator_or"/>
<block type="operator_not"/>
<block type="operator_join">
<value name="STRING1">
<shadow type="text">
<field name="TEXT">hello</field>
</shadow>
</value>
<value name="STRING2">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
<block type="operator_letter_of">
<value name="LETTER">
<shadow type="math_whole_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="STRING">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
<block type="operator_length">
<value name="STRING">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
<block type="operator_mod">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_round">
<value name="NUM">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
<block type="operator_mathop">
<value name="NUM">
<shadow type="math_number">
<field name="NUM"/>
</shadow>
</value>
</block>
</category>
`;
const data = `
<category name="Data" colour="#FF8C1A" secondaryColour="#DB6E00" custom="VARIABLE">
</category>
`;
const xmlOpen = '<xml style="display: none">';
const xmlClose = '</xml>';
/**
* @param {string?} categoriesXML - null for default toolbox, or an XML string with <category> elements.
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
*/
const makeToolboxXML = function (categoriesXML) {
const gap = [separator];
const everything = [
xmlOpen,
top, gap,
motion, gap,
looks, gap,
sound, gap,
events, gap,
control, gap,
sensing, gap,
pen, gap,
operators, gap,
data
];
if (categoriesXML) {
everything.push(gap, categoriesXML);
}
everything.push(xmlClose);
return everything.join('\n');
};
export default makeToolboxXML;
+40
View File
@@ -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
};
+4 -1
View File
@@ -1,15 +1,18 @@
import {combineReducers} from 'redux';
import colorPickerReducer from './color-picker';
import intlReducer from './intl';
import modalReducer from './modals';
import monitorReducer from './monitors';
import targetReducer from './targets';
import toolboxReducer from './toolbox';
import vmReducer from './vm';
export default combineReducers({
colorPicker: colorPickerReducer,
intl: intlReducer,
modals: modalReducer,
monitors: monitorReducer,
targets: targetReducer,
toolbox: toolboxReducer,
vm: vmReducer
});
+10
View File
@@ -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
+31
View File
@@ -0,0 +1,31 @@
const UPDATE_TOOLBOX = 'scratch-gui/toolbox/UPDATE_TOOLBOX';
import makeToolboxXML from '../lib/make-toolbox-xml';
const initialState = {
toolboxXML: makeToolboxXML()
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case UPDATE_TOOLBOX:
return Object.assign({}, state, {
toolboxXML: action.toolboxXML
});
default:
return state;
}
};
const updateToolbox = function (toolboxXML) {
return {
type: UPDATE_TOOLBOX,
toolboxXML: toolboxXML
};
};
export {
reducer as default,
updateToolbox
};
+1 -1
View File
@@ -33,7 +33,7 @@ class SeleniumHelper {
}
clickText (text) {
return this.clickXpath(`//*[contains(text(), '${text}')]`);
return this.clickXpath(`//body//*[contains(text(), '${text}')]`);
}
clickButton (text) {
+9
View File
@@ -99,6 +99,15 @@ module.exports = {
new CopyWebpackPlugin([{
from: 'node_modules/scratch-blocks/media',
to: 'static/blocks-media'
}]),
new CopyWebpackPlugin([{
from: 'extensions/**',
to: 'static',
context: 'src/examples'
}]),
new CopyWebpackPlugin([{
from: 'extension-worker.{js,js.map}',
context: 'node_modules/scratch-vm/dist/web'
}])
].concat(process.env.NODE_ENV === 'production' ? [
new webpack.optimize.UglifyJsPlugin({