- {this.props.data.map((dataItem, itemId) => {
+ {this.getFilteredData().map((dataItem, index) => {
const scratchURL = dataItem.md5 ?
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${dataItem.md5}/get/` :
dataItem.rawURL;
return (
);
@@ -59,6 +79,7 @@ class LibraryComponent extends React.Component {
LibraryComponent.propTypes = {
data: PropTypes.arrayOf(
/* eslint-disable react/no-unused-prop-types, lines-around-comment */
+ // An item in the library
PropTypes.shape({
// @todo remove md5/rawURL prop from library, refactor to use storage
md5: PropTypes.string,
@@ -67,7 +88,8 @@ LibraryComponent.propTypes = {
})
/* eslint-enable react/no-unused-prop-types, lines-around-comment */
),
- onItemChosen: PropTypes.func,
+ onItemMouseEnter: PropTypes.func,
+ onItemMouseLeave: PropTypes.func,
onItemSelected: PropTypes.func,
onRequestClose: PropTypes.func,
title: PropTypes.string.isRequired,
diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css
index 25a134f5e..a72a4f06d 100644
--- a/src/components/modal/modal.css
+++ b/src/components/modal/modal.css
@@ -11,32 +11,69 @@
background-color: rgba(0, 0, 0, .75);
}
-.modal-content {
+/* @todo: extract to type: full ? */
+.full-modal-content {
position: absolute;
outline: none;
- overflow-y: scroll;
+ overflow-y: auto;
-webkit-overflow-scrolling: 'touch';
- border: 1px solid #ccc;
- padding: 0;
- top: 5%;
- right: 5%;
- bottom: 5%;
- left: 5%;
- border-radius: $space;
+ user-select: none;
+ height: 100%;
+ width: 100%;
+ display: flex;
+}
+
+/*
+ Modal header has 3 items:
+ |filter title x|
+
+ Use the same width for both side item containers,
+ so that title remains centered
+*/
+$sides: 20rem;
+
+.header {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ height: $library-header-height;
+
+ box-sizing: border-box;
+ width: 100%;
+ background-color: $blue;
+
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 1rem;
+ font-weight: normal;
+}
+
+.header-item {
+ display: flex;
+ align-items: center;
+ padding: 1rem;
+ text-decoration: none;
+ color: white;
user-select: none;
}
-.modal-children {
- overflow: hidden;
- height: 100%;
- z-index: 0;
- padding: 2rem;
- background: $ui-pane-gray;
+.header-item-filter {
+ display: flex;
+ flex-basis: $sides;
+ justify-content: flex-start;
}
-.close-button {
- position: absolute;
- top: 1rem;
- right: 1rem;
- z-index: 2;
+.header-item-title {
+ flex-grow: 1;
+ flex-shrink: 0;
+ justify-content: center;
+ user-select: none;
+ letter-spacing: 0.4px;
+ cursor: default;
}
+
+.header-item-close {
+ flex-basis: $sides;
+ justify-content: flex-end;
+}
+
diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx
index 3a3fc25db..2a743bd74 100644
--- a/src/components/modal/modal.jsx
+++ b/src/components/modal/modal.jsx
@@ -1,9 +1,11 @@
+const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
const ReactModal = require('react-modal');
const Box = require('../box/box.jsx');
const CloseButton = require('../close-button/close-button.jsx');
+const Filter = require('../filter/filter.jsx');
const styles = require('./modal.css');
@@ -11,21 +13,44 @@ class ModalComponent extends React.Component {
render () {
return (
(this.modal = m)}
- onRequestClose={this.props.onRequestClose}
>
-
+
+
+
+
+
+ {this.props.contentLabel}
+
+
+
+
+
{this.props.children}
@@ -36,7 +61,10 @@ class ModalComponent extends React.Component {
ModalComponent.propTypes = {
children: PropTypes.node,
contentLabel: PropTypes.string.isRequired,
- onRequestClose: PropTypes.func.isRequired,
+ filterQuery: PropTypes.string,
+ onFilterChange: PropTypes.func,
+ onFilterClear: PropTypes.func,
+ onRequestClose: PropTypes.func,
visible: PropTypes.bool.isRequired
};
diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx
index edd0fb043..54b1ebe8a 100644
--- a/src/components/monitor/monitor.jsx
+++ b/src/components/monitor/monitor.jsx
@@ -7,7 +7,7 @@ const styles = require('./monitor.css');
const categories = {
data: '#FF8C1A',
sensing: '#5CB1D6',
- sounds: '#CF63CF',
+ sound: '#CF63CF',
looks: '#9966FF',
motion: '#4C97FF'
};
diff --git a/src/containers/backdrop-library.jsx b/src/containers/backdrop-library.jsx
index b19921b4c..62e9cd473 100644
--- a/src/containers/backdrop-library.jsx
+++ b/src/containers/backdrop-library.jsx
@@ -4,7 +4,7 @@ const React = require('react');
const VM = require('scratch-vm');
const backdropLibraryContent = require('../lib/libraries/backdrops.json');
-const LibaryComponent = require('../components/library/library.jsx');
+const LibraryComponent = require('../components/library/library.jsx');
class BackdropLibrary extends React.Component {
@@ -26,7 +26,7 @@ class BackdropLibrary extends React.Component {
}
render () {
return (
-
{
+ this.updateToolboxBlockValue(`${prefix}x`, this.props.vm.editingTarget.x.toFixed(0));
+ this.updateToolboxBlockValue(`${prefix}y`, this.props.vm.editingTarget.y.toFixed(0));
+ });
+ }
}
-
onWorkspaceMetricsChange () {
const target = this.props.vm.editingTarget;
if (target && target.id) {
@@ -127,7 +148,6 @@ class Blocks extends React.Component {
if (this.props.vm.editingTarget && !this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
this.onWorkspaceMetricsChange();
}
-
this.ScratchBlocks.Events.disable();
this.workspace.clear();
@@ -152,6 +172,7 @@ class Blocks extends React.Component {
}
handlePromptCallback (data) {
this.state.prompt.callback(data);
+ this.props.vm.createVariable(data);
this.handlePromptClose();
}
handlePromptClose () {
diff --git a/src/containers/costume-library.jsx b/src/containers/costume-library.jsx
index 1fea39a2d..f245c098f 100644
--- a/src/containers/costume-library.jsx
+++ b/src/containers/costume-library.jsx
@@ -4,10 +4,10 @@ const React = require('react');
const VM = require('scratch-vm');
const costumeLibraryContent = require('../lib/libraries/costumes.json');
-const LibaryComponent = require('../components/library/library.jsx');
+const LibraryComponent = require('../components/library/library.jsx');
-class CostumeLibrary extends React.Component {
+class CostumeLibrary extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
@@ -26,7 +26,7 @@ class CostumeLibrary extends React.Component {
}
render () {
return (
-
diff --git a/src/containers/sprite-library.jsx b/src/containers/sprite-library.jsx
index c08748eb2..6395b421a 100644
--- a/src/containers/sprite-library.jsx
+++ b/src/containers/sprite-library.jsx
@@ -5,24 +5,70 @@ const VM = require('scratch-vm');
const spriteLibraryContent = require('../lib/libraries/sprites.json');
-const LibaryComponent = require('../components/library/library.jsx');
+const LibraryComponent = require('../components/library/library.jsx');
-class SpriteLibrary extends React.Component {
+class SpriteLibrary extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
- 'handleItemSelect'
+ 'handleItemSelect',
+ 'handleMouseEnter',
+ 'handleMouseLeave',
+ 'rotateCostume',
+ 'startRotatingCostumes',
+ 'stopRotatingCostumes'
]);
+ this.state = {
+ activeSprite: null,
+ costumeIndex: 0,
+ sprites: spriteLibraryContent
+ };
+ }
+ componentWillReceiveProps (newProps) {
+ if (!newProps.visible) clearInterval(this.intervalId);
}
handleItemSelect (item) {
this.props.vm.addSprite2(JSON.stringify(item.json));
}
+ handleMouseEnter (item) {
+ this.stopRotatingCostumes();
+ this.setState({activeSprite: item}, this.startRotatingCostumes);
+ }
+ handleMouseLeave () {
+ this.stopRotatingCostumes();
+ }
+ startRotatingCostumes () {
+ if (!this.state.activeSprite) return;
+ this.rotateCostume();
+ this.intervalId = setInterval(this.rotateCostume, 300);
+ }
+ stopRotatingCostumes () {
+ this.intervalId = clearInterval(this.intervalId);
+ }
+ rotateCostume () {
+ const costumes = this.state.activeSprite.json.costumes;
+ const nextCostumeIndex = (this.state.costumeIndex + 1) % costumes.length;
+ this.setState({
+ costumeIndex: nextCostumeIndex,
+ sprites: this.state.sprites.map(sprite => {
+ if (sprite.name === this.state.activeSprite.name) {
+ return {
+ ...sprite,
+ md5: sprite.json.costumes[nextCostumeIndex].baseLayerMD5
+ };
+ }
+ return sprite;
+ })
+ });
+ }
render () {
return (
-
diff --git a/src/css/units.css b/src/css/units.css
index ab4393ac1..3f9aaba17 100644
--- a/src/css/units.css
+++ b/src/css/units.css
@@ -3,5 +3,7 @@ $space: 0.5rem;
$sprites-per-row: 5;
$menu-bar-height: 3rem;
-$sprite-info-height: 5.25rem; /* TODO: SpriteInfo isn't explicitly set to this height yet */
+$sprite-info-height: 5.25rem; /* @todo: SpriteInfo isn't explicitly set to this height yet */
$stage-menu-height: 3rem;
+
+$library-header-height: 4.375rem;
\ No newline at end of file
diff --git a/src/lib/monitor-adapter.js b/src/lib/monitor-adapter.js
new file mode 100644
index 000000000..8dcc802a4
--- /dev/null
+++ b/src/lib/monitor-adapter.js
@@ -0,0 +1,26 @@
+/**
+ * Convert monitors from VM format to what the GUI needs to render.
+ * - Convert opcode to a label and a category
+ * - Add missing XY position data if needed
+ */
+const OpcodeLabels = require('../lib/opcode-labels.js');
+
+const PADDING = 5;
+const MONITOR_HEIGHT = 23;
+
+const isUndefined = a => typeof a === 'undefined';
+
+module.exports = function ({id, opcode, params, value, x, y}, monitorIndex) {
+ let {label, category, labelFn} = OpcodeLabels(opcode);
+
+ // Use labelFn if provided for dynamic labelling (e.g. variables)
+ if (!isUndefined(labelFn)) label = labelFn(params);
+
+ // Simple layout if x or y are undefined
+ // @todo scratch2 has a more complex layout behavior we may want to adopt
+ // @todo e.g. this does not work well when monitors have already been moved
+ if (isUndefined(x)) x = PADDING;
+ if (isUndefined(y)) y = PADDING + (monitorIndex * (PADDING + MONITOR_HEIGHT));
+
+ return {id, label, category, value, x, y};
+};
diff --git a/src/lib/opcode-labels.js b/src/lib/opcode-labels.js
new file mode 100644
index 000000000..bebe14743
--- /dev/null
+++ b/src/lib/opcode-labels.js
@@ -0,0 +1,75 @@
+const opcodeMap = {
+ // Motion
+ motion_direction: {
+ category: 'motion',
+ label: 'direction'
+ },
+ motion_xposition: {
+ category: 'motion',
+ label: 'x position'
+ },
+ motion_yposition: {
+ category: 'motion',
+ label: 'y position'
+ },
+
+ // Looks
+ looks_size: {
+ category: 'looks',
+ label: 'size'
+ },
+ looks_costumeorder: {
+ category: 'looks',
+ label: 'costume #'
+ },
+ looks_backdroporder: {
+ category: 'looks',
+ label: 'backdrop #'
+ },
+ looks_backdropname: {
+ category: 'looks',
+ label: 'backdrop name'
+ },
+
+ // Data
+ data_variable: {
+ category: 'data',
+ labelFn: params => params.VARIABLE
+ },
+
+ // Sound
+ sound_volume: {
+ category: 'sound',
+ label: 'volume'
+ },
+ sound_tempo: {
+ category: 'sound',
+ label: 'tempo'
+ },
+
+ // Sensing
+ sensing_loudness: {
+ category: 'sensing',
+ label: 'loudness'
+ },
+ sensing_of: {
+ category: 'sensing',
+ labelFn: params => `${params.PROPERTY} of ${params.OBJECT}`
+ },
+ sensing_current: {
+ category: 'sensing',
+ labelFn: params => params.CURRENTMENU.toLowerCase()
+ },
+ sensing_timer: {
+ category: 'sensing',
+ label: 'timer'
+ }
+};
+
+module.exports = function (opcode) {
+ if (opcode in opcodeMap) return opcodeMap[opcode];
+ return {
+ category: 'data',
+ label: opcode
+ };
+};
diff --git a/src/reducers/monitors.js b/src/reducers/monitors.js
index e8b4fa947..18e9529f4 100644
--- a/src/reducers/monitors.js
+++ b/src/reducers/monitors.js
@@ -1,3 +1,5 @@
+const monitorAdapter = require('../lib/monitor-adapter.js');
+
const UPDATE_MONITORS = 'scratch-gui/monitors/UPDATE_MONITORS';
const initialState = [];
@@ -6,7 +8,7 @@ const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case UPDATE_MONITORS:
- return [...action.monitors];
+ return action.monitors.map(monitorAdapter);
default:
return state;
}