Merge branch 'develop' into feature/checked-callback
This commit is contained in:
commit
7414ead04a
23 files changed
+577
-130
No files matched your search
@@ -39,6 +39,7 @@
|
||||
"gh-pages": "^1.0.0",
|
||||
"html-webpack-plugin": "2.28.0",
|
||||
"lodash.bindall": "4.4.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.defaultsdeep": "4.6.0",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"lodash.omit": "4.5.0",
|
||||
|
||||
@@ -6,40 +6,46 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
color: gray;
|
||||
background-color: $blue;
|
||||
|
||||
overflow: hidden; /* Mask the icon animation */
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
border-color: #dbdbdb;
|
||||
border-style: solid;
|
||||
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.15s ease-out; /* @todo: standardize with var */
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-out;
|
||||
}
|
||||
|
||||
.close-button.large:hover {
|
||||
transform: scale(1.1, 1.1);
|
||||
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.small {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-width: 1px;
|
||||
color: #FFF;
|
||||
background-color: $blue;
|
||||
}
|
||||
|
||||
.large {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-width: 2px;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
transform: scale(1.1, 1.1);
|
||||
}
|
||||
|
||||
/* Same icon as Sprite Selector Add button, but rotated.
|
||||
@todo: reuse?
|
||||
*/
|
||||
.close-icon {
|
||||
position: relative;
|
||||
margin: 0.25rem;
|
||||
user-select: none;
|
||||
transform-origin: 50%;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.small .close-icon {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.large .close-icon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
@@ -11,8 +11,8 @@ const CloseButton = props => (
|
||||
styles.closeButton,
|
||||
props.className,
|
||||
{
|
||||
[styles.large]: props.size === CloseButton.SIZE_LARGE,
|
||||
[styles.small]: props.size === CloseButton.SIZE_SMALL
|
||||
[styles.small]: props.size === CloseButton.SIZE_SMALL,
|
||||
[styles.large]: props.size === CloseButton.SIZE_LARGE
|
||||
}
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
@@ -24,13 +24,13 @@ const CloseButton = props => (
|
||||
</div>
|
||||
);
|
||||
|
||||
CloseButton.SIZE_LARGE = 'large';
|
||||
CloseButton.SIZE_SMALL = 'small';
|
||||
CloseButton.SIZE_LARGE = 'large';
|
||||
|
||||
CloseButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
size: PropTypes.oneOf([CloseButton.SIZE_LARGE, CloseButton.SIZE_SMALL])
|
||||
size: PropTypes.oneOf([CloseButton.SIZE_SMALL, CloseButton.SIZE_LARGE])
|
||||
};
|
||||
|
||||
CloseButton.defaultProps = {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
@import "../../css/units.css";
|
||||
@import "../../css/colors.css";
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
/*
|
||||
Should we use width 100% instead, for cases when component's
|
||||
parent is not a flexbox container?
|
||||
*/
|
||||
flex-grow: 1;
|
||||
|
||||
padding: 0.3rem 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.10);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 10rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.filter:hover {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
height: 0.9rem;
|
||||
width: 0.9rem;
|
||||
margin: 0 0.5rem 0 0.75rem;
|
||||
}
|
||||
|
||||
/*
|
||||
Hidden state
|
||||
*/
|
||||
.x-icon-wrapper {
|
||||
opacity: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
overflow: hidden; /* Mask the icon animation */
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
margin: 0 0.25rem 0 0.5rem; /* @todo: move to parent to make component*/
|
||||
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
transition: opacity 0.05s linear;
|
||||
}
|
||||
|
||||
/*
|
||||
Shown state
|
||||
*/
|
||||
.filter.is-active .x-icon-wrapper {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
transition: opacity 0.05s linear;
|
||||
}
|
||||
|
||||
.filter.is-active .x-icon-wrapper:hover {
|
||||
transform: scale(1.2, 1.2);
|
||||
}
|
||||
|
||||
/*
|
||||
Hidden state
|
||||
*/
|
||||
.x-icon {
|
||||
position: relative;
|
||||
margin: 0.25rem;
|
||||
user-select: none;
|
||||
transform: translateX(0.5rem);
|
||||
transition: transform 0.085s cubic-bezier(0.78, 1, 1, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
Shown state
|
||||
*/
|
||||
.filter.is-active .x-icon-wrapper .x-icon {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
flex-grow: 1;
|
||||
line-height: 1.25rem;
|
||||
background-color: transparent;
|
||||
-webkit-appearance: none;
|
||||
outline: none;
|
||||
border: 0;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.15px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.filter-input::placeholder {
|
||||
opacity: 0.75;
|
||||
padding: 0 0 0 0.25rem;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
const classNames = require('classnames');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
|
||||
const filterIcon = require('./icon--filter.svg');
|
||||
const xIcon = require('./icon--x.svg');
|
||||
const styles = require('./filter.css');
|
||||
|
||||
const FilterComponent = props => {
|
||||
const {
|
||||
onChange,
|
||||
onClear,
|
||||
placeholderText,
|
||||
filterQuery
|
||||
} = props;
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
[styles.filter]: true,
|
||||
[styles.isActive]: filterQuery.length > 0
|
||||
})}
|
||||
>
|
||||
<img
|
||||
className={styles.filterIcon}
|
||||
src={filterIcon}
|
||||
/>
|
||||
<input
|
||||
autoFocus
|
||||
className={styles.filterInput}
|
||||
placeholder={placeholderText}
|
||||
type="text"
|
||||
value={filterQuery}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div
|
||||
className={styles.xIconWrapper}
|
||||
onClick={onClear}
|
||||
>
|
||||
<img
|
||||
className={styles.xIcon}
|
||||
src={xIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FilterComponent.propTypes = {
|
||||
filterQuery: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onClear: PropTypes.func,
|
||||
placeholderText: PropTypes.string
|
||||
};
|
||||
FilterComponent.defaultProps = {
|
||||
placeholderText: 'what are you looking for?'
|
||||
};
|
||||
module.exports = FilterComponent;
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14.5" height="14.6" viewBox="0 0 14.5 14.6">
|
||||
<path fill="#FFFFFF" d="M10.4 9.2l-.8-.1-.2-.2c.5-.5.8-1 1-1.6.2-.6.3-1.2.3-1.9 0-1-.2-1.9-.7-2.7-.4-.9-1.1-1.5-1.9-2C7.3.2 6.3 0 5.4 0c-.9 0-1.9.2-2.7.7-.9.5-1.5 1.2-2 2S0 4.4 0 5.4s.2 1.9.7 2.8c.5.8 1.2 1.4 2 1.9s1.7.7 2.7.7c.7 0 1.3-.1 1.9-.3.6-.3 1.1-.6 1.6-1l.2.3v.6l4.1 4.2 1.3-1.3-4.1-4.1zM8.7 7.3c-.4.6-.8 1.1-1.4 1.4-.6.3-1.2.5-1.9.5-.6 0-1.3-.2-1.8-.5-.6-.4-1.1-.8-1.4-1.4-.3-.6-.5-1.2-.5-1.9s.2-1.4.5-1.9.8-1 1.4-1.3c.5-.4 1.1-.5 1.8-.5s1.3.1 1.9.5c.6.3 1 .8 1.4 1.3.3.6.5 1.2.5 1.9S9 6.7 8.7 7.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 613 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="8" viewBox="0 0 9 8">
|
||||
<path fill="none" stroke="#FFF" stroke-width="2" d="M1 7.333L7.333 1M1 1L7.333 7.333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 224 B |
@@ -6,8 +6,9 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-basis: 160px;
|
||||
max-width: 160px;
|
||||
height: 160px;
|
||||
margin: calc($space / 2);
|
||||
margin: $space;
|
||||
padding: 1rem 1rem 0 1rem;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #575e75;
|
||||
@@ -23,18 +24,18 @@
|
||||
.library-item:hover {
|
||||
border-width: 2px;
|
||||
border-color: #1dacf4;
|
||||
transition: 0.1s ease-out;
|
||||
transform: scale(1.02, 1.02);
|
||||
}
|
||||
|
||||
.library-item.is-selected {
|
||||
border-width: 2px;
|
||||
border-color: #1dacf4;
|
||||
transition: 0.25s ease-out;
|
||||
.library-item-image-container-wrapper {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.library-item-image-container {
|
||||
position: absolute;
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.library-item-image {
|
||||
@@ -47,12 +48,12 @@
|
||||
margin: 0.25rem 0;
|
||||
text-align: center;
|
||||
|
||||
/*
|
||||
/*
|
||||
For truncating overflowing text gracefully
|
||||
Min-width is for a bug: https://css-tricks.com/flexbox-truncated-text
|
||||
*/
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
const classNames = require('classnames');
|
||||
const bindAll = require('lodash.bindall');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
@@ -9,26 +8,41 @@ const styles = require('./library-item.css');
|
||||
class LibraryItem extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, ['handleClick']);
|
||||
bindAll(this, [
|
||||
'handleClick',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave'
|
||||
]);
|
||||
}
|
||||
shouldComponentUpdate (nextProps) {
|
||||
return this.props.iconURL !== nextProps.iconURL;
|
||||
}
|
||||
handleClick (e) {
|
||||
this.props.onSelect(this.props.id);
|
||||
e.preventDefault();
|
||||
}
|
||||
handleMouseEnter () {
|
||||
this.props.onMouseEnter(this.props.id);
|
||||
}
|
||||
handleMouseLeave () {
|
||||
this.props.onMouseLeave(this.props.id);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<Box
|
||||
className={classNames({
|
||||
[styles.libraryItem]: true,
|
||||
[styles.isSelected]: this.props.selected
|
||||
})}
|
||||
className={styles.libraryItem}
|
||||
onClick={this.handleClick}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<Box className={styles.libraryItemImageContainer}>
|
||||
<img
|
||||
className={styles.libraryItemImage}
|
||||
src={this.props.iconURL}
|
||||
/>
|
||||
{/* Layers of wrapping is to prevent layout thrashing on animation */}
|
||||
<Box className={styles.libraryItemImageContainerWrapper}>
|
||||
<Box className={styles.libraryItemImageContainer}>
|
||||
<img
|
||||
className={styles.libraryItemImage}
|
||||
src={this.props.iconURL}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<span className={styles.libraryItemName}>{this.props.name}</span>
|
||||
</Box>
|
||||
@@ -40,8 +54,9 @@ LibraryItem.propTypes = {
|
||||
iconURL: PropTypes.string.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
selected: PropTypes.bool.isRequired
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
onMouseLeave: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
module.exports = LibraryItem;
|
||||
@@ -1,24 +1,14 @@
|
||||
@import "../../css/units.css";
|
||||
@import "../../css/colors.css";
|
||||
|
||||
.library-scroll-grid {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: flex-start;
|
||||
background: $ui-pane-gray;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
height: calc(100% - 6rem); /* @todo: currently estimate, fix precision */
|
||||
|
||||
/*
|
||||
Gives sprites a bit of room so they don't get cut off when they grow on hover
|
||||
@todo: sync as a var, with the transform defined on .library-item:hover
|
||||
*/
|
||||
padding: 0.15rem;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
overflow-y: auto;
|
||||
height: calc(100% - $library-header-height);
|
||||
padding: 0.5rem;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
color: #8e8f95;
|
||||
}
|
||||
@@ -10,42 +10,62 @@ const styles = require('./library.css');
|
||||
class LibraryComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, ['handleSelect']);
|
||||
this.state = {selectedItem: null};
|
||||
bindAll(this, [
|
||||
'handleFilterChange',
|
||||
'handleFilterClear',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handleSelect'
|
||||
]);
|
||||
this.state = {
|
||||
selectedItem: null,
|
||||
filterQuery: ''
|
||||
};
|
||||
}
|
||||
handleSelect (id) {
|
||||
if (this.state.selectedItem === id) {
|
||||
// Double select: select as the library's value.
|
||||
this.props.onRequestClose();
|
||||
this.props.onItemSelected(this.props.data[id]);
|
||||
} else {
|
||||
if (this.props.onItemChosen) {
|
||||
this.props.onItemChosen(this.props.data[id]);
|
||||
}
|
||||
}
|
||||
this.setState({selectedItem: id});
|
||||
this.props.onRequestClose();
|
||||
this.props.onItemSelected(this.getFilteredData()[id]);
|
||||
}
|
||||
handleMouseEnter (id) {
|
||||
if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.getFilteredData()[id]);
|
||||
}
|
||||
handleMouseLeave (id) {
|
||||
if (this.props.onItemMouseLeave) this.props.onItemMouseLeave(this.getFilteredData()[id]);
|
||||
}
|
||||
handleFilterChange (event) {
|
||||
this.setState({filterQuery: event.target.value});
|
||||
}
|
||||
handleFilterClear () {
|
||||
this.setState({filterQuery: ''});
|
||||
}
|
||||
getFilteredData () {
|
||||
return this.props.data.filter(dataItem =>
|
||||
dataItem.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) !== -1);
|
||||
}
|
||||
render () {
|
||||
if (!this.props.visible) return null;
|
||||
return (
|
||||
<ModalComponent
|
||||
contentLabel={this.props.title}
|
||||
filterQuery={this.state.filterQuery}
|
||||
visible={this.props.visible}
|
||||
onFilterChange={this.handleFilterChange}
|
||||
onFilterClear={this.handleFilterClear}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
>
|
||||
<h1 className={styles.modalHeader}>{this.props.title}</h1>
|
||||
<div className={styles.libraryScrollGrid}>
|
||||
{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 (
|
||||
<LibraryItem
|
||||
iconURL={scratchURL}
|
||||
id={itemId}
|
||||
key={`item_${itemId}`}
|
||||
id={index}
|
||||
key={`item_${index}`}
|
||||
name={dataItem.name}
|
||||
selected={this.state.selectedItem === itemId}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onSelect={this.handleSelect}
|
||||
/>
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<ReactModal
|
||||
className={styles.modalContent}
|
||||
className={styles.fullModalContent}
|
||||
contentLabel={this.props.contentLabel}
|
||||
isOpen={this.props.visible}
|
||||
overlayClassName={styles.modalOverlay}
|
||||
ref={m => (this.modal = m)}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
>
|
||||
<CloseButton
|
||||
className={styles.closeButton}
|
||||
onClick={this.props.onRequestClose}
|
||||
/>
|
||||
<Box
|
||||
className={styles.modalChildren}
|
||||
direction="column"
|
||||
grow={1}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div className={classNames(styles.headerItem, styles.headerItemFilter)}>
|
||||
<Filter
|
||||
filterQuery={this.props.filterQuery}
|
||||
onChange={this.props.onFilterChange}
|
||||
onClear={this.props.onFilterClear}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.headerItem,
|
||||
styles.headerItemTitle
|
||||
)}
|
||||
>
|
||||
{this.props.contentLabel}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.headerItem,
|
||||
styles.headerItemClose
|
||||
)}
|
||||
>
|
||||
<CloseButton
|
||||
size={CloseButton.SIZE_LARGE}
|
||||
onClick={this.props.onRequestClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{this.props.children}
|
||||
</Box>
|
||||
</ReactModal>
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const styles = require('./monitor.css');
|
||||
const categories = {
|
||||
data: '#FF8C1A',
|
||||
sensing: '#5CB1D6',
|
||||
sounds: '#CF63CF',
|
||||
sound: '#CF63CF',
|
||||
looks: '#9966FF',
|
||||
motion: '#4C97FF'
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<LibaryComponent
|
||||
<LibraryComponent
|
||||
data={backdropLibraryContent}
|
||||
title="Backdrop Library"
|
||||
visible={this.props.visible}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const bindAll = require('lodash.bindall');
|
||||
const debounce = require('lodash.debounce');
|
||||
const defaultsDeep = require('lodash.defaultsdeep');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
@@ -30,6 +31,7 @@ class Blocks extends React.Component {
|
||||
'onScriptGlowOff',
|
||||
'onBlockGlowOn',
|
||||
'onBlockGlowOff',
|
||||
'onTargetsUpdate',
|
||||
'onVisualReport',
|
||||
'onWorkspaceUpdate',
|
||||
'onWorkspaceMetricsChange',
|
||||
@@ -40,6 +42,7 @@ class Blocks extends React.Component {
|
||||
workspaceMetrics: {},
|
||||
prompt: null
|
||||
};
|
||||
this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
|
||||
}
|
||||
componentDidMount () {
|
||||
const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
|
||||
@@ -85,6 +88,7 @@ class Blocks extends React.Component {
|
||||
this.props.vm.addListener('BLOCK_GLOW_OFF', this.onBlockGlowOff);
|
||||
this.props.vm.addListener('VISUAL_REPORT', this.onVisualReport);
|
||||
this.props.vm.addListener('workspaceUpdate', this.onWorkspaceUpdate);
|
||||
this.props.vm.addListener('targetsUpdate', this.onTargetsUpdate);
|
||||
}
|
||||
detachVM () {
|
||||
this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
|
||||
@@ -93,8 +97,25 @@ class Blocks extends React.Component {
|
||||
this.props.vm.removeListener('BLOCK_GLOW_OFF', this.onBlockGlowOff);
|
||||
this.props.vm.removeListener('VISUAL_REPORT', this.onVisualReport);
|
||||
this.props.vm.removeListener('workspaceUpdate', this.onWorkspaceUpdate);
|
||||
this.props.vm.removeListener('targetsUpdate', this.onTargetsUpdate);
|
||||
}
|
||||
updateToolboxBlockValue (id, value) {
|
||||
this.workspace
|
||||
.getFlyout()
|
||||
.getWorkspace()
|
||||
.getBlockById(id)
|
||||
.inputList[0]
|
||||
.fieldRow[0]
|
||||
.setValue(value);
|
||||
}
|
||||
onTargetsUpdate () {
|
||||
if (this.props.vm.editingTarget) {
|
||||
['glide', 'move', 'set'].forEach(prefix => {
|
||||
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 () {
|
||||
|
||||
@@ -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 (
|
||||
<LibaryComponent
|
||||
<LibraryComponent
|
||||
data={costumeLibraryContent}
|
||||
title="Costume Library"
|
||||
visible={this.props.visible}
|
||||
|
||||
@@ -3,25 +3,30 @@ const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const VM = require('scratch-vm');
|
||||
const AudioEngine = require('scratch-audio');
|
||||
const LibaryComponent = require('../components/library/library.jsx');
|
||||
const LibraryComponent = require('../components/library/library.jsx');
|
||||
|
||||
const soundIcon = require('../components/asset-panel/icon--sound.svg');
|
||||
|
||||
const soundLibraryContent = require('../lib/libraries/sounds.json');
|
||||
|
||||
class SoundLibrary extends React.Component {
|
||||
class SoundLibrary extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleItemSelected',
|
||||
'handleItemChosen'
|
||||
'handleItemMouseEnter',
|
||||
'handleItemMouseLeave'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
this.audioEngine = new AudioEngine();
|
||||
this.player = this.audioEngine.createPlayer();
|
||||
}
|
||||
handleItemChosen (soundItem) {
|
||||
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();
|
||||
}
|
||||
handleItemMouseEnter (soundItem) {
|
||||
const md5ext = soundItem._md5;
|
||||
const idParts = md5ext.split('.');
|
||||
const md5 = idParts[0];
|
||||
@@ -39,6 +44,9 @@ class SoundLibrary extends React.Component {
|
||||
this.player.playSound(soundItem._md5);
|
||||
});
|
||||
}
|
||||
handleItemMouseLeave () {
|
||||
this.player.stopAllSounds();
|
||||
}
|
||||
handleItemSelected (soundItem) {
|
||||
const vmSound = {
|
||||
format: soundItem.format,
|
||||
@@ -64,11 +72,12 @@ class SoundLibrary extends React.Component {
|
||||
});
|
||||
|
||||
return (
|
||||
<LibaryComponent
|
||||
<LibraryComponent
|
||||
data={soundLibraryThumbnailData}
|
||||
title="Sound Library"
|
||||
visible={this.props.visible}
|
||||
onItemChosen={this.handleItemChosen}
|
||||
onItemMouseEnter={this.handleItemMouseEnter}
|
||||
onItemMouseLeave={this.handleItemMouseLeave}
|
||||
onItemSelected={this.handleItemSelected}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<LibaryComponent
|
||||
data={spriteLibraryContent}
|
||||
<LibraryComponent
|
||||
data={this.state.sprites}
|
||||
title="Sprite Library"
|
||||
visible={this.props.visible}
|
||||
onItemMouseEnter={this.handleMouseEnter}
|
||||
onItemMouseLeave={this.handleMouseLeave}
|
||||
onItemSelected={this.handleItemSelect}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
/>
|
||||
|
||||
+3
-1
@@ -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;
|
||||
@@ -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};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user