Compare commits

..
Author SHA1 Message Date
Christopher Willis-Ford d4e71024fb Merge tag '0.1.0-prerelease.20190605201216' into scratch-desktop 2019-06-09 22:14:49 -07:00
Christopher Willis-Ford a83d9369dc Revert "Merge pull request #4735 from paulkaplan/revert-storage-update"
Go back to `scratch-storage` version 1.3.1 in the `scratch-desktop`
branch so that we have the performance improvements from
LLK/scratch-storage#83

If I understand correctly we reverted from 1.3.1 to 1.2.2 because of
problems affecting HTTP saves, which are not available in Scratch
Desktop.

This reverts commit b435ac3101, reversing
changes made to 0d7a05d37a.
2019-05-16 19:59:58 -07:00
Christopher Willis-Ford 012bb612ad Revert "Merge pull request #4736 from LLK/revert-4644-load-library-images-through-storage"
This brings "Load library images through storage" back into the
`scratch-desktop` branch since it's necessary here. We need to figure
out some performance details before trying to merge it into `develop`
though.
2019-05-16 19:50:38 -07:00
Christopher Willis-Ford b9e7bde676 Merge tag '0.1.0-prerelease.20190516172446' into scratch-desktop 2019-05-16 12:38:02 -07:00
Christopher Willis-Ford 992f012ff3 TO REVERT: Scratch Desktop GUI hacks
At some point these things will be changed / disabled / removed in
master or develop; at that point this change should be replaced with the
more official approach.
2019-03-14 11:51:38 -07:00
Christopher Willis-Ford c53b88dbad use relative publicPath for file-loader 2019-03-14 11:51:38 -07:00
95 changed files with 904 additions and 16808 deletions

No files matched your search

+1
View File
@@ -4,6 +4,7 @@
# NPM
/node_modules
npm-*
/package-lock.json
# Testing
/.nyc_output
-6
View File
@@ -14,18 +14,12 @@ env:
cache:
directories:
- node_modules
before_install:
# package-lock.json was introduced in npm@5
- '[[ $(node -v) =~ ^v9.*$ ]] || npm install -g npm@latest' # skipped when using node 9
- npm install -g greenkeeper-lockfile
install:
- npm --production=false install
- npm --production=false update
before_script: greenkeeper-lockfile-update
script:
- npm test
- if [ "$TRAVIS_EVENT_TYPE" == "pull_request" ] && [ "$TRAVIS_BRANCH" == "master" ]; then npm run test:smoke; fi
after_script: greenkeeper-lockfile-upload
before_deploy:
- >
if [ -z "$BEFORE_DEPLOY_RAN" ]; then
-14540
View File
File diff suppressed because it is too large. Load diff
+5 -4
View File
@@ -100,18 +100,19 @@
"react-test-renderer": "16.2.0",
"react-tooltip": "3.8.0",
"react-virtualized": "9.20.1",
"react-visibility-sensor": "5.0.2",
"redux": "3.7.2",
"redux-mock-store": "^1.2.3",
"redux-throttle": "0.1.1",
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.20190114210212",
"scratch-l10n": "3.3.20190711005050",
"scratch-blocks": "0.1.0-prerelease.1561987297",
"scratch-paint": "0.2.0-prerelease.20190624133806",
"scratch-blocks": "0.1.0-prerelease.1559136797",
"scratch-l10n": "3.3.20190605143948",
"scratch-paint": "0.2.0-prerelease.20190524133325",
"scratch-render": "0.1.0-prerelease.20190605151415",
"scratch-storage": "1.3.2",
"scratch-svg-renderer": "0.2.0-prerelease.20190523193400",
"scratch-vm": "0.2.0-prerelease.20190628161443",
"scratch-vm": "0.2.0-prerelease.20190604212312",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
-3
View File
@@ -170,9 +170,6 @@
align-items: center;
position: relative;
text-align: center;
/* Min height prevents layout changing when images change */
min-height: 256px;
}
.step-video {
-4
View File
@@ -104,9 +104,6 @@ class VideoStep extends React.Component {
// We use the Wistia API here to update or pause the video dynamically:
// https://wistia.com/support/developers/player-api
componentDidUpdate (prevProps) {
// Ensure the wistia API is loaded and available
if (!(window.Wistia && window.Wistia.api)) return;
// Get a handle on the currently loaded video
const video = window.Wistia.api(prevProps.video);
@@ -158,7 +155,6 @@ const ImageStep = ({title, image}) => (
<img
className={styles.stepImage}
draggable={false}
key={image} /* Use src as key to prevent hanging around on slow connections */
src={image}
/>
</div>
+10 -7
View File
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import Box from '../box/box.jsx';
import ScratchImage from '../scratch-image/scratch-image.jsx';
import styles from './library-item.css';
import classNames from 'classnames';
@@ -35,10 +36,12 @@ class LibraryItemComponent extends React.PureComponent {
/>
</div>
) : null}
<img
className={styles.featuredImage}
src={this.props.iconURL}
/>
{this.props.iconSource ? (
<ScratchImage
className={styles.featuredImage}
imageSource={this.props.iconSource}
/>
) : null}
</div>
{this.props.insetIconURL ? (
<div className={styles.libraryItemInsetImageContainer}>
@@ -121,9 +124,9 @@ class LibraryItemComponent extends React.PureComponent {
{/* Layers of wrapping is to prevent layout thrashing on animation */}
<Box className={styles.libraryItemImageContainerWrapper}>
<Box className={styles.libraryItemImageContainer}>
<img
<ScratchImage
className={styles.libraryItemImage}
src={this.props.iconURL}
imageSource={this.props.iconSource}
/>
</Box>
</Box>
@@ -146,7 +149,7 @@ LibraryItemComponent.propTypes = {
extensionId: PropTypes.string,
featured: PropTypes.bool,
hidden: PropTypes.bool,
iconURL: PropTypes.string,
iconSource: ScratchImage.ImageSourcePropType,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
name: PropTypes.oneOfType([
-9
View File
@@ -12,7 +12,6 @@
overflow-y: auto;
height: auto;
padding: 0.5rem;
height: calc(100% - $library-header-height);
}
.library-scroll-grid.withFilterBar {
@@ -60,11 +59,3 @@
height: $library-filter-bar-height;
overflow: hidden;
}
.spinner-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
+52 -24
View File
@@ -9,7 +9,7 @@ import Modal from '../../containers/modal.jsx';
import Divider from '../divider/divider.jsx';
import Filter from '../filter/filter.jsx';
import TagButton from '../../containers/tag-button.jsx';
import Spinner from '../spinner/spinner.jsx';
import storage from '../../lib/storage';
import styles from './library.css';
@@ -29,6 +29,47 @@ const messages = defineMessages({
const ALL_TAG = {tag: 'all', intlLabel: messages.allTag};
const tagListPrefix = [ALL_TAG];
/**
* Find the AssetType which corresponds to a particular file extension. For example, 'png' => AssetType.ImageBitmap.
* @param {string} fileExtension - the file extension to look up.
* @returns {AssetType} - the AssetType corresponding to the extension, if any.
*/
const getAssetTypeForFileExtension = function (fileExtension) {
const compareOptions = {
sensitivity: 'accent',
usage: 'search'
};
for (const assetTypeId in storage.AssetType) {
const assetType = storage.AssetType[assetTypeId];
if (fileExtension.localeCompare(assetType.runtimeFormat, compareOptions) === 0) {
return assetType;
}
}
};
/**
* Figure out an `imageSource` (URI or asset ID & type) for a library item's icon.
* @param {object} item - either a library item or one of a library item's costumes.
* @returns {object} - an `imageSource` ready to be passed to a `ScratchImage`.
*/
const getItemImageSource = function (item) {
if (item.rawURL) {
return {
uri: item.rawURL
};
}
// TODO: adjust libraries to be more storage-friendly; don't use split() here.
const md5 = item.md5 || item.baseLayerMD5;
if (md5) {
const [assetId, fileExtension] = md5.split('.');
return {
assetId: assetId,
assetType: getAssetTypeForFileExtension(fileExtension)
};
}
};
class LibraryComponent extends React.Component {
constructor (props) {
super(props);
@@ -45,16 +86,9 @@ class LibraryComponent extends React.Component {
this.state = {
selectedItem: null,
filterQuery: '',
selectedTag: ALL_TAG.tag,
loaded: false
selectedTag: ALL_TAG.tag
};
}
componentDidMount () {
// Allow the spinner to display before loading the content
setTimeout(() => {
this.setState({loaded: true});
});
}
componentDidUpdate (prevProps, prevState) {
if (prevState.filterQuery !== this.state.filterQuery ||
prevState.selectedTag !== this.state.selectedTag) {
@@ -170,8 +204,10 @@ class LibraryComponent extends React.Component {
})}
ref={this.setFilteredDataRef}
>
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => (
<LibraryItem
{this.getFilteredData().map((dataItem, index) => {
const iconSource = getItemImageSource(dataItem);
const icons = dataItem.json && dataItem.json.costumes.map(getItemImageSource);
return (<LibraryItem
bluetoothRequired={dataItem.bluetoothRequired}
collaborator={dataItem.collaborator}
description={dataItem.description}
@@ -179,26 +215,18 @@ class LibraryComponent extends React.Component {
extensionId={dataItem.extensionId}
featured={dataItem.featured}
hidden={dataItem.hidden}
iconMd5={dataItem.md5}
iconRawURL={dataItem.rawURL}
icons={dataItem.json && dataItem.json.costumes}
iconSource={iconSource}
icons={icons}
id={index}
insetIconURL={dataItem.insetIconURL}
internetConnectionRequired={dataItem.internetConnectionRequired}
key={typeof dataItem.name === 'string' ? dataItem.name : dataItem.rawURL}
key={`item_${index}`}
name={dataItem.name}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
)) : (
<div className={styles.spinnerWrapper}>
<Spinner
large
level="primary"
/>
</div>
)}
/>);
})}
</div>
</Modal>
);
+1 -2
View File
@@ -91,8 +91,7 @@ LoupeComponent.propTypes = {
color: PropTypes.shape({
r: PropTypes.number,
g: PropTypes.number,
b: PropTypes.number,
a: PropTypes.number
b: PropTypes.number
}),
data: PropTypes.instanceOf(Uint8Array),
width: PropTypes.number,
@@ -0,0 +1,139 @@
import PropTypes from 'prop-types';
import React from 'react';
import VisibilitySensor from 'react-visibility-sensor';
import storage from '../../lib/storage';
class ScratchImage extends React.PureComponent {
static init () {
this._maxParallelism = 6;
this._currentJobs = 0;
this._pendingImages = new Set();
}
static loadPendingImages () {
if (this._currentJobs >= this._maxParallelism) {
// already busy
return;
}
// Find the first visible image. If there aren't any, find the first non-visible image.
let nextImage;
for (const image of this._pendingImages) {
if (image.isVisible) {
nextImage = image;
break;
} else {
nextImage = nextImage || image;
}
}
// If we found an image to load:
// 1) Remove it from the queue
// 2) Load the image
// 3) Pump the queue again
if (nextImage) {
this._pendingImages.delete(nextImage);
const imageSource = nextImage.props.imageSource;
++this._currentJobs;
storage
.load(imageSource.assetType, imageSource.assetId)
.then(asset => {
if (!nextImage.wasUnmounted) {
const dataURI = asset.encodeDataURI();
nextImage.setState({
imageURI: dataURI
});
}
--this._currentJobs;
this.loadPendingImages();
});
}
}
constructor (props) {
super(props);
this.state = {};
Object.assign(this.state, this._loadImageSource(props.imageSource));
}
componentWillReceiveProps (nextProps) {
const newState = this._loadImageSource(nextProps.imageSource);
this.setState(newState);
}
componentWillUnmount () {
this.wasUnmounted = true;
ScratchImage._pendingImages.delete(this);
}
/**
* Calculate the state changes necessary to load the image specified in the provided source info. If the component
* is mounted, call setState() with the return value of this function. If the component has not yet mounted, use
* the return value of this function as initial state for the component.
*
* @param {object} imageSource - the new source for the image, including either assetId or URI
* @returns {object} - the new state values, if any.
*/
_loadImageSource (imageSource) {
if (imageSource) {
if (imageSource.uri) {
ScratchImage._pendingImages.delete(this);
return {
imageURI: imageSource.uri,
lastRequestedAsset: null
};
}
if (this.state.lastRequestedAsset !== imageSource.assetId) {
ScratchImage._pendingImages.add(this);
return {
lastRequestedAsset: imageSource.assetId
};
}
}
// Nothing to do - don't change any state.
return {};
}
render () {
const {
src: _src,
imageSource: _imageSource,
...imgProps
} = this.props;
return (
<VisibilitySensor
intervalCheck
scrollCheck
>
{
({isVisible}) => {
this.isVisible = isVisible;
ScratchImage.loadPendingImages();
return (
<img
src={this.state.imageURI}
{...imgProps}
/>
);
}
}
</VisibilitySensor>
);
}
}
ScratchImage.ImageSourcePropType = PropTypes.oneOfType([
PropTypes.shape({
assetId: PropTypes.string.isRequired,
assetType: PropTypes.oneOf(Object.values(storage.AssetType)).isRequired
}),
PropTypes.shape({
uri: PropTypes.string.isRequired
})
]);
ScratchImage.propTypes = {
imageSource: ScratchImage.ImageSourcePropType.isRequired
};
ScratchImage.init();
export default ScratchImage;
-17
View File
@@ -39,16 +39,6 @@
height: .5rem;
}
.large {
width: 2.5rem;
height: 2.5rem;
}
.large::before, .large::after {
width: 2.5rem;
height: 2.5rem;
}
@keyframes spin {
0% {
transform: rotate(0deg);
@@ -81,10 +71,3 @@
.spinner.info::after {
border-top-color: $ui-white;
}
.spinner.primary {
border-color: $motion-transparent;
}
.spinner.primary::after {
border-top-color: $motion-primary;
}
+3 -10
View File
@@ -8,8 +8,7 @@ const SpinnerComponent = function (props) {
const {
className,
level,
small,
large
small
} = props;
return (
<div
@@ -17,24 +16,18 @@ const SpinnerComponent = function (props) {
className,
styles.spinner,
styles[level],
{
[styles.small]: small,
[styles.large]: large
}
{[styles.small]: small}
)}
/>
);
};
SpinnerComponent.propTypes = {
className: PropTypes.string,
large: PropTypes.bool,
level: PropTypes.string,
small: PropTypes.bool
};
SpinnerComponent.defaultProps = {
className: '',
large: false,
level: 'info',
small: false
level: 'info'
};
export default SpinnerComponent;
@@ -20,6 +20,7 @@
cursor: pointer;
user-select: none;
touch-action: none;
}
.sprite-selector-item.is-selected {
@@ -23,9 +23,8 @@ const SpriteSelectorItem = props => (
onMouseDown: props.onMouseDown,
onTouchStart: props.onMouseDown
}}
disable={props.preventContextMenu}
disable={props.dragging}
id={`${props.name}-${contextMenuId}`}
ref={props.componentRef}
>
{typeof props.number === 'undefined' ? null : (
<div className={styles.number}>{props.number}</div>
@@ -90,9 +89,9 @@ const SpriteSelectorItem = props => (
SpriteSelectorItem.propTypes = {
className: PropTypes.string,
componentRef: PropTypes.func,
costumeURL: PropTypes.string,
details: PropTypes.string,
dragging: PropTypes.bool,
name: PropTypes.string.isRequired,
number: PropTypes.number,
onClick: PropTypes.func,
@@ -102,7 +101,6 @@ SpriteSelectorItem.propTypes = {
onMouseDown: PropTypes.func,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
preventContextMenu: PropTypes.bool,
selected: PropTypes.bool.isRequired
};
@@ -110,8 +110,7 @@ SpriteList.propTypes = {
editingTarget: PropTypes.string,
hoveredTarget: PropTypes.shape({
hoveredSprite: PropTypes.string,
receivedBlocks: PropTypes.bool,
sprite: PropTypes.string
receivedBlocks: PropTypes.bool
}),
items: PropTypes.arrayOf(PropTypes.shape({
costume: PropTypes.shape({
@@ -68,8 +68,6 @@
padding-left: calc($space / 2);
padding-right: calc($space / 2);
padding-bottom: $space;
overflow-x: hidden;
}
.add-button {
+7 -38
View File
@@ -17,7 +17,6 @@ import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants';
import DropAreaHOC from '../lib/drop-area-hoc.jsx';
import DragConstants from '../lib/drag-constants';
import defineDynamicBlock from '../lib/define-dynamic-block';
import {connect} from 'react-redux';
import {updateToolbox} from '../reducers/toolbox';
@@ -394,50 +393,20 @@ class Blocks extends React.Component {
// workspace to be 'undone' here.
this.workspace.clearUndo();
}
handleExtensionAdded (categoryInfo) {
const defineBlocks = blockInfoArray => {
if (blockInfoArray && blockInfoArray.length > 0) {
const staticBlocksJson = [];
const dynamicBlocksInfo = [];
blockInfoArray.forEach(blockInfo => {
if (blockInfo.info && blockInfo.info.isDynamic) {
dynamicBlocksInfo.push(blockInfo);
} else if (blockInfo.json) {
staticBlocksJson.push(blockInfo.json);
}
// otherwise it's a non-block entry such as '---'
});
handleExtensionAdded (blocksInfo) {
// select JSON from each block info object then reject the pseudo-blocks which don't have JSON, like separators
// this actually defines blocks and MUST run regardless of the UI state
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x));
this.ScratchBlocks.defineBlocksWithJsonArray(staticBlocksJson);
dynamicBlocksInfo.forEach(blockInfo => {
// This is creating the block factory / constructor -- NOT a specific instance of the block.
// The factory should only know static info about the block: the category info and the opcode.
// Anything else will be picked up from the XML attached to the block instance.
const extendedOpcode = `${categoryInfo.id}_${blockInfo.info.opcode}`;
const blockDefinition =
defineDynamicBlock(this.ScratchBlocks, categoryInfo, blockInfo, extendedOpcode);
this.ScratchBlocks.Blocks[extendedOpcode] = blockDefinition;
});
}
};
// scratch-blocks implements a menu or custom field as a special kind of block ("shadow" block)
// these actually define blocks and MUST run regardless of the UI state
defineBlocks(
Object.getOwnPropertyNames(categoryInfo.customFieldTypes)
.map(fieldTypeName => categoryInfo.customFieldTypes[fieldTypeName].scratchBlocksDefinition));
defineBlocks(categoryInfo.menus);
defineBlocks(categoryInfo.blocks);
// Update the toolbox with new blocks if possible
// Update the toolbox with new blocks
const toolboxXML = this.getToolboxXML();
if (toolboxXML) {
this.props.updateToolboxState(toolboxXML);
}
}
handleBlocksInfoUpdate (categoryInfo) {
handleBlocksInfoUpdate (blocksInfo) {
// @todo Later we should replace this to avoid all the warnings from redefining blocks.
this.handleExtensionAdded(categoryInfo);
this.handleExtensionAdded(blocksInfo);
}
handleCategorySelected (categoryId) {
const extension = extensionData.find(ext => ext.extensionId === categoryId);
+1 -5
View File
@@ -52,7 +52,6 @@ class GUI extends React.Component {
setIsScratchDesktop(this.props.isScratchDesktop);
this.setReduxTitle(this.props.projectTitle);
this.props.onStorageInit(storage);
this.props.onVmInit(this.props.vm);
}
componentDidUpdate (prevProps) {
if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) {
@@ -93,7 +92,6 @@ class GUI extends React.Component {
onStorageInit,
onUpdateProjectId,
onUpdateReduxProjectTitle,
onVmInit,
projectHost,
projectId,
projectTitle,
@@ -133,7 +131,6 @@ GUI.propTypes = {
onUpdateProjectId: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
onUpdateReduxProjectTitle: PropTypes.func,
onVmInit: PropTypes.func,
projectHost: PropTypes.string,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
projectTitle: PropTypes.string,
@@ -145,8 +142,7 @@ GUI.defaultProps = {
isScratchDesktop: false,
onStorageInit: storageInstance => storageInstance.addOfficialScratchWebStores(),
onProjectLoaded: () => {},
onUpdateProjectId: () => {},
onVmInit: (/* vm */) => {}
onUpdateProjectId: () => {}
};
const mapStateToProps = state => {
+8 -18
View File
@@ -75,21 +75,17 @@ class LibraryItem extends React.PureComponent {
const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length;
this.setState({iconIndex: nextIconIndex});
}
curIconMd5 () {
curIconSource () {
if (this.props.icons &&
this.state.isRotatingIcon &&
this.state.iconIndex < this.props.icons.length &&
this.props.icons[this.state.iconIndex] &&
this.props.icons[this.state.iconIndex].baseLayerMD5) {
return this.props.icons[this.state.iconIndex].baseLayerMD5;
this.props.icons[this.state.iconIndex]) {
return this.props.icons[this.state.iconIndex];
}
return this.props.iconMd5;
return this.props.iconSource;
}
render () {
const iconMd5 = this.curIconMd5();
const iconURL = iconMd5 ?
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` :
this.props.iconRawURL;
const iconSource = this.curIconSource();
return (
<LibraryItemComponent
bluetoothRequired={this.props.bluetoothRequired}
@@ -99,8 +95,7 @@ class LibraryItem extends React.PureComponent {
extensionId={this.props.extensionId}
featured={this.props.featured}
hidden={this.props.hidden}
iconURL={iconURL}
icons={this.props.icons}
iconSource={iconSource}
id={this.props.id}
insetIconURL={this.props.insetIconURL}
internetConnectionRequired={this.props.internetConnectionRequired}
@@ -127,13 +122,8 @@ LibraryItem.propTypes = {
extensionId: PropTypes.string,
featured: PropTypes.bool,
hidden: PropTypes.bool,
iconMd5: PropTypes.string,
iconRawURL: PropTypes.string,
icons: PropTypes.arrayOf(
PropTypes.shape({
baseLayerMD5: PropTypes.string
})
),
iconSource: LibraryItemComponent.propTypes.iconSource, // single icon
icons: PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource), // rotating icons
id: PropTypes.number.isRequired,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
+31 -42
View File
@@ -6,19 +6,19 @@ import {connect} from 'react-redux';
import {setHoveredSprite} from '../reducers/hovered-target';
import {updateAssetDrag} from '../reducers/asset-drag';
import storage from '../lib/storage';
import {getEventXY} from '../lib/touch-utils';
import VM from 'scratch-vm';
import getCostumeUrl from '../lib/get-costume-url';
import DragRecognizer from '../lib/drag-recognizer';
import {getEventXY} from '../lib/touch-utils';
import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx';
const dragThreshold = 3; // Same as the block drag threshold
class SpriteSelectorItem extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
'getCostumeData',
'setRef',
'handleClick',
'handleDelete',
'handleDuplicate',
@@ -26,22 +26,9 @@ class SpriteSelectorItem extends React.PureComponent {
'handleMouseEnter',
'handleMouseLeave',
'handleMouseDown',
'handleDragEnd',
'handleDrag',
'handleTouchEnd'
'handleMouseMove',
'handleMouseUp'
]);
this.dragRecognizer = new DragRecognizer({
onDrag: this.handleDrag,
onDragEnd: this.handleDragEnd
});
}
componentDidMount () {
document.addEventListener('touchend', this.handleTouchEnd);
}
componentWillUnmount () {
document.removeEventListener('touchend', this.handleTouchEnd);
this.dragRecognizer.reset();
}
getCostumeData () {
if (this.props.costumeURL) return this.props.costumeURL;
@@ -49,7 +36,12 @@ class SpriteSelectorItem extends React.PureComponent {
return getCostumeUrl(this.props.asset);
}
handleDragEnd () {
handleMouseUp () {
this.initialOffset = null;
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('touchend', this.handleMouseUp);
window.removeEventListener('touchmove', this.handleMouseMove);
if (this.props.dragging) {
this.props.onDrag({
img: null,
@@ -63,26 +55,29 @@ class SpriteSelectorItem extends React.PureComponent {
this.noClick = false;
});
}
handleDrag (currentOffset) {
this.props.onDrag({
img: this.getCostumeData(),
currentOffset: currentOffset,
dragging: true,
dragType: this.props.dragType,
index: this.props.index,
payload: this.props.dragPayload
});
this.noClick = true;
}
handleTouchEnd (e) {
const {x, y} = getEventXY(e);
const {top, left, bottom, right} = this.ref.getBoundingClientRect();
if (x >= left && x <= right && y >= top && y <= bottom) {
this.handleMouseEnter();
handleMouseMove (e) {
const currentOffset = getEventXY(e);
const dx = currentOffset.x - this.initialOffset.x;
const dy = currentOffset.y - this.initialOffset.y;
if (Math.sqrt((dx * dx) + (dy * dy)) > dragThreshold) {
this.props.onDrag({
img: this.getCostumeData(),
currentOffset: currentOffset,
dragging: true,
dragType: this.props.dragType,
index: this.props.index,
payload: this.props.dragPayload
});
this.noClick = true;
}
e.preventDefault();
}
handleMouseDown (e) {
this.dragRecognizer.start(e);
this.initialOffset = getEventXY(e);
window.addEventListener('mouseup', this.handleMouseUp);
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('touchend', this.handleMouseUp);
window.addEventListener('touchmove', this.handleMouseMove);
}
handleClick (e) {
e.preventDefault();
@@ -108,10 +103,6 @@ class SpriteSelectorItem extends React.PureComponent {
handleMouseEnter () {
this.props.dispatchSetHoveredSprite(this.props.id);
}
setRef (component) {
// Access the DOM node using .elem because it is going through ContextMenuTrigger
this.ref = component && component.elem;
}
render () {
const {
/* eslint-disable no-unused-vars */
@@ -131,9 +122,7 @@ class SpriteSelectorItem extends React.PureComponent {
} = this.props;
return (
<SpriteSelectorItemComponent
componentRef={this.setRef}
costumeURL={this.getCostumeData()}
preventContextMenu={this.dragRecognizer.gestureInProgress()}
onClick={this.handleClick}
onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null}
onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null}
+1 -21
View File
@@ -15,7 +15,6 @@ import ThrottledPropertyHOC from '../lib/throttled-property-hoc.jsx';
import {emptyCostume} from '../lib/empty-assets';
import sharedMessages from '../lib/shared-messages';
import {fetchCode} from '../lib/backpack-api';
import {getEventXY} from '../lib/touch-utils';
import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx';
@@ -47,25 +46,10 @@ class StageSelector extends React.Component {
'handleBackdropUpload',
'handleMouseEnter',
'handleMouseLeave',
'handleTouchEnd',
'handleDrop',
'setFileInput',
'setRef'
'setFileInput'
]);
}
componentDidMount () {
document.addEventListener('touchend', this.handleTouchEnd);
}
componentWillUnmount () {
document.removeEventListener('touchend', this.handleTouchEnd);
}
handleTouchEnd (e) {
const {x, y} = getEventXY(e);
const {top, left, bottom, right} = this.ref.getBoundingClientRect();
if (x >= left && x <= right && y >= top && y <= bottom) {
this.handleMouseEnter();
}
}
addBackdropFromLibraryItem (item, shouldActivateTab = true) {
const vmBackdrop = {
name: item.name,
@@ -153,16 +137,12 @@ class StageSelector extends React.Component {
setFileInput (input) {
this.fileInput = input;
}
setRef (ref) {
this.ref = ref;
}
render () {
const componentProps = omit(this.props, [
'asset', 'dispatchSetHoveredSprite', 'id', 'intl',
'onActivateTab', 'onSelect', 'onShowImporting', 'onCloseImporting']);
return (
<DroppableThrottledStage
componentRef={this.setRef}
fileInputRef={this.setFileInput}
onBackdropFileUpload={this.handleBackdropUpload}
onBackdropFileUploadClick={this.handleFileUploadClick}
-108
View File
@@ -1,108 +0,0 @@
// TODO: access `BlockType` and `ArgumentType` without reaching into VM
// Should we move these into a new extension support module or something?
import ArgumentType from 'scratch-vm/src/extension-support/argument-type';
import BlockType from 'scratch-vm/src/extension-support/block-type';
/**
* Define a block using extension info which has the ability to dynamically determine (and update) its layout.
* This functionality is used for extension blocks which can change its properties based on different state
* information. For example, the `control_stop` block changes its shape based on which menu item is selected
* and a variable block changes its text to reflect the variable name without using an editable field.
* @param {object} ScratchBlocks - The ScratchBlocks name space.
* @param {object} categoryInfo - Information about this block's extension category, including any menus and icons.
* @param {object} staticBlockInfo - The base block information before any dynamic changes.
* @param {string} extendedOpcode - The opcode for the block (including the extension ID).
*/
// TODO: grow this until it can fully replace `_convertForScratchBlocks` in the VM runtime
const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extendedOpcode) => ({
init: function () {
const blockJson = {
type: extendedOpcode,
inputsInline: true,
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3
};
// There is a scratch-blocks / Blockly extension called "scratch_extension" which adjusts the styling of
// blocks to allow for an icon, a feature of Scratch extension blocks. However, Scratch "core" extension
// blocks don't have icons and so they should not use 'scratch_extension'. Adding a scratch-blocks / Blockly
// extension after `jsonInit` isn't fully supported (?), so we decide now whether there will be an icon.
if (staticBlockInfo.blockIconURI || categoryInfo.blockIconURI) {
blockJson.extensions = ['scratch_extension'];
}
// initialize the basics of the block, to be overridden & extended later by `domToMutation`
this.jsonInit(blockJson);
// initialize the cached block info used to carry block info from `domToMutation` to `mutationToDom`
this.blockInfoText = '{}';
// we need a block info update (through `domToMutation`) before we have a completely initialized block
this.needsBlockInfoUpdate = true;
},
mutationToDom: function () {
const container = document.createElement('mutation');
container.setAttribute('blockInfo', this.blockInfoText);
return container;
},
domToMutation: function (xmlElement) {
const blockInfoText = xmlElement.getAttribute('blockInfo');
if (!blockInfoText) return;
if (!this.needsBlockInfoUpdate) {
throw new Error('Attempted to update block info twice');
}
delete this.needsBlockInfoUpdate;
this.blockInfoText = blockInfoText;
const blockInfo = JSON.parse(blockInfoText);
switch (blockInfo.blockType) {
case BlockType.COMMAND:
case BlockType.CONDITIONAL:
case BlockType.LOOP:
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
this.setPreviousStatement(true);
this.setNextStatement(!blockInfo.isTerminal);
break;
case BlockType.REPORTER:
this.setOutput(true);
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND);
if (!blockInfo.disableMonitor) {
this.setCheckboxInFlyout(true);
}
break;
case BlockType.BOOLEAN:
this.setOutput(true);
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL);
break;
case BlockType.HAT:
case BlockType.EVENT:
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
this.setNextStatement(true);
break;
}
if (blockInfo.color1 || blockInfo.color2 || blockInfo.color3) {
// `setColour` handles undefined parameters by adjusting defined colors
this.setColour(blockInfo.color1, blockInfo.color2, blockInfo.color3);
}
// Layout block arguments
// TODO handle E/C Blocks
const blockText = blockInfo.text;
const args = [];
let argCount = 0;
const scratchBlocksStyleText = blockText.replace(/\[(.+?)]/g, (match, argName) => {
const arg = blockInfo.arguments[argName];
switch (arg.type) {
case ArgumentType.STRING:
args.push({type: 'input_value', name: argName});
break;
case ArgumentType.BOOLEAN:
args.push({type: 'input_value', name: argName, check: 'Boolean'});
break;
}
return `%${++argCount}`;
});
this.interpolate_(scratchBlocksStyleText, args);
}
});
export default defineDynamicBlock;
-125
View File
@@ -1,125 +0,0 @@
import bindAll from 'lodash.bindall';
import {getEventXY} from '../lib/touch-utils';
class DragRecognizer {
/* Gesture states */
static get STATE_UNIDENTIFIED () {
return 'unidentified';
}
static get STATE_SCROLL () {
return 'scroll';
}
static get STATE_DRAG () {
return 'drag';
}
constructor ({
onDrag = (() => {}),
onDragEnd = (() => {}),
touchDragAngle = 70, // Angle and distance thresholds are the same as scratch-blocks
distanceThreshold = 3
}) {
this._onDrag = onDrag;
this._onDragEnd = onDragEnd;
this._touchDragAngle = touchDragAngle;
this._distanceThreshold = distanceThreshold;
this._initialOffset = null;
this._gestureState = DragRecognizer.STATE_UNIDENTIFIED;
bindAll(this, [
'start',
'gestureInProgress',
'reset',
'_handleMove',
'_handleEnd'
]);
}
start (event) {
this._initialOffset = getEventXY(event);
this._bindListeners();
}
gestureInProgress () {
return this._gestureState !== DragRecognizer.STATE_UNIDENTIFIED;
}
reset () {
this._unbindListeners();
this._initialOffset = null;
this._gestureState = DragRecognizer.STATE_UNIDENTIFIED;
}
//
// Internal functions
//
_bindListeners () {
window.addEventListener('mouseup', this._handleEnd);
window.addEventListener('mousemove', this._handleMove);
window.addEventListener('touchend', this._handleEnd);
// touchmove must be marked as non-passive, or else it cannot prevent scrolling
window.addEventListener('touchmove', this._handleMove, {passive: false});
}
_unbindListeners () {
window.removeEventListener('mouseup', this._handleEnd);
window.removeEventListener('mousemove', this._handleMove);
window.removeEventListener('touchend', this._handleEnd);
window.removeEventListener('touchmove', this._handleMove, {passive: false});
}
_handleMove (event) {
// For gestures identified as vertical scrolls, do not process movement events
if (this._isScroll()) return;
const currentOffset = getEventXY(event);
// Try to identify this gesture if it hasn't been identified already
if (!this.gestureInProgress()) {
const dx = currentOffset.x - this._initialOffset.x;
const dy = currentOffset.y - this._initialOffset.y;
const dragDistance = Math.sqrt((dx * dx) + (dy * dy));
if (dragDistance < this._distanceThreshold) return;
// For touch moves, additionally check if the angle suggests drag vs. scroll
if (event.type === 'touchmove') {
// Direction goes from -180 to 180, with 0 toward the right.
let angle = Math.atan2(dy, dx) / Math.PI * 180;
// Fold over horizontal axis, range now 0 to 180
angle = Math.abs(angle);
// Fold over vertical axis, range now 0 to 90
if (angle > 90) angle = 180 - angle;
if (angle > this._touchDragAngle) {
this._gestureState = DragRecognizer.STATE_SCROLL;
} else {
this._gestureState = DragRecognizer.STATE_DRAG;
}
} else {
// Mouse moves are always considered drags
this._gestureState = DragRecognizer.STATE_DRAG;
}
}
if (this._isDrag()) {
this._onDrag(currentOffset, this._initialOffset);
event.preventDefault();
}
}
_handleEnd () {
this._onDragEnd();
this.reset();
}
_isDrag () {
return this._gestureState === DragRecognizer.STATE_DRAG;
}
_isScroll () {
return this._gestureState === DragRecognizer.STATE_SCROLL;
}
}
export default DragRecognizer;
+53 -72
View File
@@ -75,7 +75,7 @@
},
{
"name": "Andie-a",
"md5": "d2c433e02481b10584e34ccdcf5a046a.svg",
"md5": "419ff5a76adbb4b076c072f68267fdeb.svg",
"type": "costume",
"tags": [
"sports",
@@ -87,33 +87,14 @@
"alex eben meyer"
],
"info": [
80,
65,
86,
81,
1
]
},
{
"name": "Andie-b",
"md5": "045f92ad80d72521098ede6d7c80549a.svg",
"type": "costume",
"tags": [
"sports",
"basketball",
"people",
"wheelchair",
"handicap",
"handicapable",
"alex eben meyer"
],
"info": [
40,
91,
1
]
},
{
"name": "Andie-c",
"md5": "c9bad70ad371b8d6d97daed7ab9def98.svg",
"md5": "a6a766912b4e43064ee3f389c72925b4.svg",
"type": "costume",
"tags": [
"sports",
@@ -126,13 +107,13 @@
],
"info": [
46,
49,
106,
1
]
},
{
"name": "Andie-d",
"md5": "7c9db638cd671898ff07771112faff0f.svg",
"name": "Andie-c",
"md5": "fac1025857d2c28391cb31f39a80df54.svg",
"type": "costume",
"tags": [
"sports",
@@ -144,8 +125,27 @@
"alex eben meyer"
],
"info": [
71,
57,
52,
65,
1
]
},
{
"name": "Andie-d",
"md5": "7af50835e05d43e5e07cd2d641e7401e.svg",
"type": "costume",
"tags": [
"sports",
"basketball",
"people",
"wheelchair",
"handicap",
"handicapable",
"alex eben meyer"
],
"info": [
87,
72,
1
]
},
@@ -1069,7 +1069,7 @@
},
{
"name": "Ben-a",
"md5": "322e13bd17d02acfdc048a5ef2167be8.svg",
"md5": "4386d05d5abd6e1aa0a93b7b447d1b39.svg",
"type": "costume",
"tags": [
"sports",
@@ -1079,14 +1079,14 @@
"alex eben meyer"
],
"info": [
54,
53,
69,
1
]
},
{
"name": "Ben-b",
"md5": "59ba1e80ba75b1d06c37db4b9b4fde27.svg",
"md5": "90db137cd0743204d02fdc9be1c8eb94.svg",
"type": "costume",
"tags": [
"sports",
@@ -1096,14 +1096,14 @@
"alex eben meyer"
],
"info": [
54,
69,
53,
67,
1
]
},
{
"name": "Ben-c",
"md5": "940ec2fa85d666dde95a51bcbccd266e.svg",
"md5": "5f05d41ede773b3bb379286e86a5b869.svg",
"type": "costume",
"tags": [
"sports",
@@ -1113,14 +1113,14 @@
"alex eben meyer"
],
"info": [
73,
71,
72,
1
]
},
{
"name": "Ben-d",
"md5": "3043a50a91b9d484a8cd32eb82f2d88f.svg",
"md5": "6f6e246567c0639c758ed4f6930be76f.svg",
"type": "costume",
"tags": [
"sports",
@@ -1130,8 +1130,8 @@
"alex eben meyer"
],
"info": [
44,
71,
42,
72,
1
]
},
@@ -3628,17 +3628,16 @@
},
{
"name": "Earth",
"md5": "64220361b80d2812e9f27fc88ef39027.svg",
"md5": "d9df2cf4f6eb2460d0de3e4f1e05f40b.svg",
"type": "costume",
"tags": [
"space",
"planet",
"earth",
"globe"
"earth"
],
"info": [
55,
55,
41,
40,
1
]
},
@@ -6179,7 +6178,7 @@
},
{
"name": "Jordyn-a",
"md5": "2f59adebc2c0feb9ebfabd7ee9642f2b.svg",
"md5": "8355df83cca2baa0fdf6269d1311f1ea.svg",
"type": "costume",
"tags": [
"sports",
@@ -6190,13 +6189,13 @@
],
"info": [
51,
62,
63,
1
]
},
{
"name": "Jordyn-b",
"md5": "075f382883e60dba8094b3dc30918f60.svg",
"md5": "2e2c43bbd7b18524fb132f1426ca3134.svg",
"type": "costume",
"tags": [
"sports",
@@ -6213,7 +6212,7 @@
},
{
"name": "Jordyn-c",
"md5": "fbe81c40d4a880c8799c458359758629.svg",
"md5": "5c0d24c53805e16358a2f6f6cc4a456e.svg",
"type": "costume",
"tags": [
"sports",
@@ -6224,13 +6223,13 @@
],
"info": [
68,
62,
61,
1
]
},
{
"name": "Jordyn-d",
"md5": "68a784d4d0340475a6e6feff3b28b470.svg",
"md5": "ebe2b20b99c1e1b373fd2b02be48aab9.svg",
"type": "costume",
"tags": [
"sports",
@@ -6241,7 +6240,7 @@
],
"info": [
40,
62,
61,
1
]
},
@@ -9661,7 +9660,7 @@
},
{
"name": "Sun",
"md5": "14b0508da0dc30e83b2bede13c1be1e9.svg",
"md5": "55c931c65456822c0c56a2b30e3e550d.svg",
"type": "costume",
"tags": [
"space",
@@ -9672,8 +9671,8 @@
"nuclear"
],
"info": [
54,
54,
72,
72,
1
]
},
@@ -10583,24 +10582,6 @@
1
]
},
{
"name": "Wizard Girl",
"md5": "dd40d3a2565990bcc24d2b36cd446394.svg",
"type": "costume",
"tags": [
"people",
"female",
"girl",
"wizard",
"magic",
"fantasy"
],
"info": [
80,
91,
1
]
},
{
"name": "Wizard Hat",
"md5": "2da561a7e5cbfcda613f4750020d4aa5.svg",
@@ -12701,4 +12682,4 @@
1
]
}
]
]
+1 -162
View File
@@ -90,86 +90,6 @@ import videoPet from './steps/video-pet.en.gif';
import videoAnimate from './steps/video-animate.en.gif';
import videoPop from './steps/video-pop.en.gif';
// Make it Fly
import flyChooseBackdrop from './steps/fly-choose-backdrop.en.gif';
import flyChooseCharacter from './steps/fly-choose-character.en.gif';
import flySaySomething from './steps/fly-say-something.en.gif';
import flyMoveArrows from './steps/fly-make-interactive.en.gif';
import flyChooseObject from './steps/fly-object-to-collect.en.gif';
import flyFlyingObject from './steps/fly-flying-heart.en.gif';
import flySelectFlyingSprite from './steps/fly-select-flyer.en.jpg';
import flyAddScore from './steps/fly-add-score.en.gif';
import flyKeepScore from './steps/fly-keep-score.en.gif';
import flyAddScenery from './steps/fly-choose-scenery.en.gif';
import flyMoveScenery from './steps/fly-move-scenery.en.gif';
import flySwitchLooks from './steps/fly-switch-costume.en.gif';
// Pong
import pongAddBackdrop from './steps/pong-add-backdrop.en.png';
import pongAddBallSprite from './steps/pong-add-ball-sprite.en.png';
import pongBounceAround from './steps/pong-bounce-around.en.gif';
import pongAddPaddle from './steps/pong-add-a-paddle.en.gif';
import pongMoveThePaddle from './steps/pong-move-the-paddle.en.gif';
import pongSelectBallSprite from './steps/pong-select-ball.en.png';
import pongAddMoreCodeToBall from './steps/pong-add-code-to-ball.en.png';
import pongAddAScore from './steps/pong-add-score.en.gif';
import pongChooseScoreFromMenu from './steps/pong-choose-score.en.png';
import pongInsertChangeScoreBlock from './steps/pong-insert-change-score.en.png';
import pongResetScore from './steps/pong-reset-score.en.png';
import pongAddLineSprite from './steps/pong-add-line.en.gif';
import pongGameOver from './steps/pong-game-over.en.png';
// Imagine If
import imagineTypeWhatYouWant from './steps/imagine-type-what-you-want.en.png';
import imagineClickGreenFlag from './steps/imagine-click-green-flag.en.png';
import imagineChooseBackdrop from './steps/imagine-choose-backdrop.en.png';
import imagineChooseSprite from './steps/imagine-choose-any-sprite.en.png';
import imagineFlyAround from './steps/imagine-fly-around.en.png';
import imagineChooseAnotherSprite from './steps/imagine-choose-another-sprite.en.png';
import imagineLeftRight from './steps/imagine-left-right.en.png';
import imagineUpDown from './steps/imagine-up-down.en.png';
import imagineChangeCostumes from './steps/imagine-change-costumes.en.png';
import imagineGlideToPoint from './steps/imagine-glide-to-point.en.png';
import imagineGrowShrink from './steps/imagine-grow-shrink.en.png';
import imagineChooseAnotherBackdrop from './steps/imagine-choose-another-backdrop.en.png';
import imagineSwitchBackdrops from './steps/imagine-switch-backdrops.en.gif';
import imagineRecordASound from './steps/imagine-record-a-sound.en.gif';
import imagineChooseSound from './steps/imagine-choose-sound.en.png';
// Add a Backdrop
import addBackdrop from './steps/add-backdrop.en.png';
// Add Effectsf
import addEffects from './steps/add-effects.en.png';
// Hide and Show
import hideAndShow from './steps/hide-show.en.png';
// Switch Costumes
import switchCostumes from './steps/switch-costumes.en.png';
// Change Size
import changeSize from './steps/change-size.en.png';
// Spin
import spinTurn from './steps/spin-turn.en.png';
import spinPointInDirection from './steps/spin-point-in-direction.en.png';
// Record a Sound
import recordASoundSoundsTab from './steps/record-a-sound-sounds-tab.en.png';
import recordASoundClickRecord from './steps/record-a-sound-click-record.en.png';
import recordASoundPressRecordButton from './steps/record-a-sound-press-record-button.en.png';
import recordASoundChooseSound from './steps/record-a-sound-choose-sound.en.png';
import recordASoundPlayYourSound from './steps/record-a-sound-play-your-sound.en.png';
// Use Arrow Keys
import moveArrowKeysLeftRight from './steps/move-arrow-keys-left-right.en.png';
import moveArrowKeysUpDown from './steps/move-arrow-keys-up-down.en.png';
// Glide Around
import glideAroundBackAndForth from './steps/glide-around-back-and-forth.en.png';
import glideAroundPoint from './steps/glide-around-point.en.png';
const enImages = {
// Intro
introMove: introMove,
@@ -261,89 +181,8 @@ const enImages = {
videoAddExtension: videoAddExtension,
videoPet: videoPet,
videoAnimate: videoAnimate,
videoPop: videoPop,
videoPop: videoPop
// Make it Fly
flyChooseBackdrop: flyChooseBackdrop,
flyChooseCharacter: flyChooseCharacter,
flySaySomething: flySaySomething,
flyMoveArrows: flyMoveArrows,
flyChooseObject: flyChooseObject,
flyFlyingObject: flyFlyingObject,
flySelectFlyingSprite: flySelectFlyingSprite,
flyAddScore: flyAddScore,
flyKeepScore: flyKeepScore,
flyAddScenery: flyAddScenery,
flyMoveScenery: flyMoveScenery,
flySwitchLooks: flySwitchLooks,
// Pong
pongAddBackdrop: pongAddBackdrop,
pongAddBallSprite: pongAddBallSprite,
pongBounceAround: pongBounceAround,
pongAddPaddle: pongAddPaddle,
pongMoveThePaddle: pongMoveThePaddle,
pongSelectBallSprite: pongSelectBallSprite,
pongAddMoreCodeToBall: pongAddMoreCodeToBall,
pongAddAScore: pongAddAScore,
pongChooseScoreFromMenu: pongChooseScoreFromMenu,
pongInsertChangeScoreBlock: pongInsertChangeScoreBlock,
pongResetScore: pongResetScore,
pongAddLineSprite: pongAddLineSprite,
pongGameOver: pongGameOver,
// Imagine If
imagineTypeWhatYouWant: imagineTypeWhatYouWant,
imagineClickGreenFlag: imagineClickGreenFlag,
imagineChooseBackdrop: imagineChooseBackdrop,
imagineChooseSprite: imagineChooseSprite,
imagineFlyAround: imagineFlyAround,
imagineChooseAnotherSprite: imagineChooseAnotherSprite,
imagineLeftRight: imagineLeftRight,
imagineUpDown: imagineUpDown,
imagineChangeCostumes: imagineChangeCostumes,
imagineGlideToPoint: imagineGlideToPoint,
imagineGrowShrink: imagineGrowShrink,
imagineChooseAnotherBackdrop: imagineChooseAnotherBackdrop,
imagineSwitchBackdrops: imagineSwitchBackdrops,
imagineRecordASound: imagineRecordASound,
imagineChooseSound: imagineChooseSound,
// Add a Backdrop
addBackdrop: addBackdrop,
// Add Effects
addEffects: addEffects,
// Hide and Show
hideAndShow: hideAndShow,
// Switch Costumes
switchCostumes: switchCostumes,
// Change Size
changeSize: changeSize,
// Spin
spinTurn: spinTurn,
spinPointInDirection: spinPointInDirection,
// Record a Sound
recordASoundSoundsTab: recordASoundSoundsTab,
recordASoundClickRecord: recordASoundClickRecord,
recordASoundPressRecordButton: recordASoundPressRecordButton,
recordASoundChooseSound: recordASoundChooseSound,
recordASoundPlayYourSound: recordASoundPlayYourSound,
// Use Arrow Keys
moveArrowKeysLeftRight: moveArrowKeysLeftRight,
moveArrowKeysUpDown: moveArrowKeysUpDown,
// Glide Around
glideAroundBackAndForth: glideAroundBackAndForth,
glideAroundPoint: glideAroundPoint
};
export {enImages};
File diff suppressed because it is too large. Load diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

@@ -74,12 +74,6 @@ const videos = {
'en': '3jvl8zgjo2',
'ja': 'q7zq9vu0wb',
'ja-Hira': 'q7zq9vu0wb'
},
'make-it-fly': {
en: 'zbtdx2dem9'
},
'pong-game': {
en: '8m48dv0ens'
}
};
+68 -127
View File
@@ -118,7 +118,7 @@
},
{
"name": "Andie",
"md5": "d2c433e02481b10584e34ccdcf5a046a.svg",
"md5": "419ff5a76adbb4b076c072f68267fdeb.svg",
"type": "sprite",
"tags": [
"sports",
@@ -138,7 +138,7 @@
"objName": "Andie",
"sounds": [
{
"soundName": "Basketball Bounce",
"soundName": "basketball bounce",
"soundID": -1,
"md5": "1727f65b5f22d151685b8e5917456a60.wav",
"sampleCount": 8099,
@@ -149,40 +149,40 @@
"costumes": [
{
"costumeName": "andie-a",
"baseLayerID": -1,
"baseLayerMD5": "d2c433e02481b10584e34ccdcf5a046a.svg",
"baseLayerID": 0,
"baseLayerMD5": "419ff5a76adbb4b076c072f68267fdeb.svg",
"bitmapResolution": 1,
"rotationCenterX": 80,
"rotationCenterY": 65
"rotationCenterX": 86,
"rotationCenterY": 81
},
{
"costumeName": "andie-b",
"baseLayerID": -1,
"baseLayerMD5": "045f92ad80d72521098ede6d7c80549a.svg",
"baseLayerMD5": "a6a766912b4e43064ee3f389c72925b4.svg",
"bitmapResolution": 1,
"rotationCenterX": 40,
"rotationCenterY": 91
"rotationCenterX": 46,
"rotationCenterY": 106
},
{
"costumeName": "andie-c",
"baseLayerID": -1,
"baseLayerMD5": "c9bad70ad371b8d6d97daed7ab9def98.svg",
"baseLayerMD5": "fac1025857d2c28391cb31f39a80df54.svg",
"bitmapResolution": 1,
"rotationCenterX": 46,
"rotationCenterY": 49
"rotationCenterX": 52,
"rotationCenterY": 65
},
{
"costumeName": "andie-d",
"baseLayerID": -1,
"baseLayerMD5": "7c9db638cd671898ff07771112faff0f.svg",
"baseLayerMD5": "7af50835e05d43e5e07cd2d641e7401e.svg",
"bitmapResolution": 1,
"rotationCenterX": 71,
"rotationCenterY": 57
"rotationCenterX": 87,
"rotationCenterY": 72
}
],
"currentCostumeIndex": 0,
"scratchX": -150,
"scratchY": 90,
"scratchX": -129,
"scratchY": -73,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -1442,7 +1442,7 @@
},
{
"name": "Ben",
"md5": "322e13bd17d02acfdc048a5ef2167be8.svg",
"md5": "4386d05d5abd6e1aa0a93b7b447d1b39.svg",
"type": "sprite",
"tags": [
"sports",
@@ -1460,18 +1460,18 @@
"objName": "Ben",
"sounds": [
{
"soundName": "Goal Cheer",
"soundName": "pop",
"soundID": -1,
"md5": "a434069c58e79d42f5d21abb1c318919.wav",
"sampleCount": 84096,
"rate": 22050,
"format": "adpcm"
"md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
"sampleCount": 258,
"rate": 11025,
"format": ""
},
{
"soundName": "Referee Whistle",
"soundName": "basketball bounce",
"soundID": -1,
"md5": "8468b9b3f11a665ee4d215afd8463b97.wav",
"sampleCount": 14034,
"md5": "1727f65b5f22d151685b8e5917456a60.wav",
"sampleCount": 8099,
"rate": 22050,
"format": "adpcm"
}
@@ -1480,39 +1480,39 @@
{
"costumeName": "ben-a",
"baseLayerID": -1,
"baseLayerMD5": "322e13bd17d02acfdc048a5ef2167be8.svg",
"baseLayerMD5": "4386d05d5abd6e1aa0a93b7b447d1b39.svg",
"bitmapResolution": 1,
"rotationCenterX": 54,
"rotationCenterX": 53,
"rotationCenterY": 69
},
{
"costumeName": "ben-b",
"baseLayerID": -1,
"baseLayerMD5": "59ba1e80ba75b1d06c37db4b9b4fde27.svg",
"baseLayerMD5": "90db137cd0743204d02fdc9be1c8eb94.svg",
"bitmapResolution": 1,
"rotationCenterX": 54,
"rotationCenterY": 69
"rotationCenterX": 53,
"rotationCenterY": 67
},
{
"costumeName": "ben-c",
"baseLayerID": -1,
"baseLayerMD5": "940ec2fa85d666dde95a51bcbccd266e.svg",
"baseLayerMD5": "5f05d41ede773b3bb379286e86a5b869.svg",
"bitmapResolution": 1,
"rotationCenterX": 73,
"rotationCenterY": 71
"rotationCenterX": 71,
"rotationCenterY": 72
},
{
"costumeName": "ben-d",
"baseLayerID": -1,
"baseLayerMD5": "3043a50a91b9d484a8cd32eb82f2d88f.svg",
"baseLayerMD5": "6f6e246567c0639c758ed4f6930be76f.svg",
"bitmapResolution": 1,
"rotationCenterX": 44,
"rotationCenterY": 71
"rotationCenterX": 42,
"rotationCenterY": 72
}
],
"currentCostumeIndex": 0,
"scratchX": -15,
"scratchY": 93,
"scratchX": -63,
"scratchY": -94,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -5434,13 +5434,12 @@
},
{
"name": "Earth",
"md5": "64220361b80d2812e9f27fc88ef39027.svg",
"md5": "d9df2cf4f6eb2460d0de3e4f1e05f40b.svg",
"type": "sprite",
"tags": [
"space",
"planet",
"earth",
"globe"
"earth"
],
"info": [
0,
@@ -5461,17 +5460,17 @@
],
"costumes": [
{
"costumeName": "earth",
"costumeName": "Earth",
"baseLayerID": -1,
"baseLayerMD5": "64220361b80d2812e9f27fc88ef39027.svg",
"baseLayerMD5": "d9df2cf4f6eb2460d0de3e4f1e05f40b.svg",
"bitmapResolution": 1,
"rotationCenterX": 55,
"rotationCenterY": 55
"rotationCenterX": 41,
"rotationCenterY": 40
}
],
"currentCostumeIndex": 0,
"scratchX": 142,
"scratchY": -53,
"scratchX": 57,
"scratchY": -39,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -8843,7 +8842,7 @@
},
{
"name": "Jordyn",
"md5": "2f59adebc2c0feb9ebfabd7ee9642f2b.svg",
"md5": "8355df83cca2baa0fdf6269d1311f1ea.svg",
"type": "sprite",
"tags": [
"sports",
@@ -8855,24 +8854,16 @@
"info": [
0,
4,
2
1
],
"json": {
"objName": "Jordyn",
"sounds": [
{
"soundName": "Goal Cheer",
"soundName": "basketball bounce",
"soundID": -1,
"md5": "199b30c8b4fe0642e849924bd1e1b463.wav",
"sampleCount": 84411,
"rate": 22050,
"format": "adpcm"
},
{
"soundName": "Referee Whistle",
"soundID": -1,
"md5": "7d91d95d841dc6cf1282914306a4674a.wav",
"sampleCount": 14238,
"md5": "1727f65b5f22d151685b8e5917456a60.wav",
"sampleCount": 8099,
"rate": 22050,
"format": "adpcm"
}
@@ -8881,15 +8872,15 @@
{
"costumeName": "jordyn-a",
"baseLayerID": -1,
"baseLayerMD5": "2f59adebc2c0feb9ebfabd7ee9642f2b.svg",
"baseLayerMD5": "8355df83cca2baa0fdf6269d1311f1ea.svg",
"bitmapResolution": 1,
"rotationCenterX": 51,
"rotationCenterY": 62
"rotationCenterY": 63
},
{
"costumeName": "jordyn-b",
"baseLayerID": -1,
"baseLayerMD5": "075f382883e60dba8094b3dc30918f60.svg",
"baseLayerMD5": "2e2c43bbd7b18524fb132f1426ca3134.svg",
"bitmapResolution": 1,
"rotationCenterX": 51,
"rotationCenterY": 63
@@ -8897,23 +8888,23 @@
{
"costumeName": "jordyn-c",
"baseLayerID": -1,
"baseLayerMD5": "fbe81c40d4a880c8799c458359758629.svg",
"baseLayerMD5": "5c0d24c53805e16358a2f6f6cc4a456e.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
"rotationCenterY": 62
"rotationCenterY": 61
},
{
"costumeName": "jordyn-d",
"baseLayerID": -1,
"baseLayerMD5": "68a784d4d0340475a6e6feff3b28b470.svg",
"baseLayerMD5": "ebe2b20b99c1e1b373fd2b02be48aab9.svg",
"bitmapResolution": 1,
"rotationCenterX": 40,
"rotationCenterY": 62
"rotationCenterY": 61
}
],
"currentCostumeIndex": 0,
"scratchX": 61,
"scratchY": 93,
"scratchX": -164,
"scratchY": -41,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -14381,7 +14372,7 @@
},
{
"name": "Sun",
"md5": "14b0508da0dc30e83b2bede13c1be1e9.svg",
"md5": "55c931c65456822c0c56a2b30e3e550d.svg",
"type": "sprite",
"tags": [
"space",
@@ -14412,15 +14403,15 @@
{
"costumeName": "sun",
"baseLayerID": -1,
"baseLayerMD5": "14b0508da0dc30e83b2bede13c1be1e9.svg",
"baseLayerMD5": "55c931c65456822c0c56a2b30e3e550d.svg",
"bitmapResolution": 1,
"rotationCenterX": 54,
"rotationCenterY": 54
"rotationCenterX": 72,
"rotationCenterY": 72
}
],
"currentCostumeIndex": 0,
"scratchX": -3,
"scratchY": -42,
"scratchX": 16,
"scratchY": -28,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -15787,56 +15778,6 @@
"spriteInfo": {}
}
},
{
"name": "Wizard Girl",
"md5": "dd40d3a2565990bcc24d2b36cd446394.svg",
"type": "sprite",
"tags": [
"people",
"female",
"girl",
"wizard",
"magic",
"fantasy"
],
"info": [
0,
1,
1
],
"json": {
"objName": "Wizard Girl",
"sounds": [
{
"soundName": "Magic Spell",
"soundID": -1,
"md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav",
"sampleCount": 43077,
"rate": 22050,
"format": "adpcm"
}
],
"costumes": [
{
"costumeName": "wizard girl",
"baseLayerID": -1,
"baseLayerMD5": "dd40d3a2565990bcc24d2b36cd446394.svg",
"bitmapResolution": 1,
"rotationCenterX": 80,
"rotationCenterY": 91
}
],
"currentCostumeIndex": 0,
"scratchX": -147,
"scratchY": -71,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
"isDraggable": false,
"visible": true,
"spriteInfo": {}
}
},
{
"name": "Wizard Hat",
"md5": "2da561a7e5cbfcda613f4750020d4aa5.svg",
@@ -20471,4 +20412,4 @@
"spriteInfo": {}
}
}
]
]
+13 -36
View File
@@ -716,16 +716,13 @@ const xmlClose = '</xml>';
/**
* @param {!boolean} isStage - Whether the toolbox is for a stage-type target.
* @param {?string} targetId - The current editing target
* @param {?Array.<object>} categoriesXML - optional array of `{id,xml}` for categories. This can include both core
* and other extensions: core extensions will be placed in the normal Scratch order; others will go at the bottom.
* @property {string} id - the extension / category ID.
* @property {string} xml - the `<category>...</category>` XML for this extension / category.
* @param {?string} categoriesXML - null for default toolbox, or an XML string with <category> elements.
* @param {?string} costumeName - The name of the default selected costume dropdown.
* @param {?string} backdropName - The name of the default selected backdrop dropdown.
* @param {?string} soundName - The name of the default selected sound dropdown.
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
*/
const makeToolboxXML = function (isStage, targetId, categoriesXML = [],
const makeToolboxXML = function (isStage, targetId, categoriesXML,
costumeName = '', backdropName = '', soundName = '') {
const gap = [categorySeparator];
@@ -733,41 +730,21 @@ const makeToolboxXML = function (isStage, targetId, categoriesXML = [],
backdropName = xmlEscape(backdropName);
soundName = xmlEscape(soundName);
categoriesXML = categoriesXML.slice();
const moveCategory = categoryId => {
const index = categoriesXML.findIndex(categoryInfo => categoryInfo.id === categoryId);
if (index >= 0) {
// remove the category from categoriesXML and return its XML
const [categoryInfo] = categoriesXML.splice(index, 1);
return categoryInfo.xml;
}
// return `undefined`
};
const motionXML = moveCategory('motion') || motion(isStage, targetId);
const looksXML = moveCategory('looks') || looks(isStage, targetId, costumeName, backdropName);
const soundXML = moveCategory('sound') || sound(isStage, targetId, soundName);
const eventsXML = moveCategory('event') || events(isStage, targetId);
const controlXML = moveCategory('control') || control(isStage, targetId);
const sensingXML = moveCategory('sensing') || sensing(isStage, targetId);
const operatorsXML = moveCategory('operators') || operators(isStage, targetId);
const variablesXML = moveCategory('data') || variables(isStage, targetId);
const myBlocksXML = moveCategory('procedures') || myBlocks(isStage, targetId);
const everything = [
xmlOpen,
motionXML, gap,
looksXML, gap,
soundXML, gap,
eventsXML, gap,
controlXML, gap,
sensingXML, gap,
operatorsXML, gap,
variablesXML, gap,
myBlocksXML
motion(isStage, targetId), gap,
looks(isStage, targetId, costumeName, backdropName), gap,
sound(isStage, targetId, soundName), gap,
events(isStage, targetId), gap,
control(isStage, targetId), gap,
sensing(isStage, targetId), gap,
operators(isStage, targetId), gap,
variables(isStage, targetId), gap,
myBlocks(isStage, targetId)
];
for (const extensionCategory of categoriesXML) {
everything.push(gap, extensionCategory.xml);
if (categoriesXML) {
everything.push(gap, categoriesXML);
}
everything.push(xmlClose);
+3 -8
View File
@@ -234,14 +234,9 @@ const ProjectSaverHOC = function (WrappedComponent) {
asset.dataFormat,
asset.data,
asset.assetId
).then(response => {
// Asset servers respond with {status: ok} for successful POSTs
if (response.status !== 'ok') {
// Errors include a `code` property, e.g. "Forbidden"
return Promise.reject(response.code);
}
asset.clean = true;
})
).then(
() => (asset.clean = true)
)
)
)
.then(() => this.props.onUpdateProjectData(projectId, savedVMState, requestParams))
-1
View File
@@ -45,7 +45,6 @@ export default function (projectId, vmState, params) {
return new Promise((resolve, reject) => {
xhr(opts, (err, response) => {
if (err) return reject(err);
if (response.statusCode !== 200) return reject(response.statusCode);
let body;
try {
// Since we didn't set json: true, we have to parse manually
+5 -1
View File
@@ -193,7 +193,11 @@ describe('Working with costumes', () => {
.mouseMove(abbyElement)
.perform();
// wait for one of Abby's alternate costumes to appear
await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/b6e23922f23b49ddc6f62f675e77417c.svg/get/"]');
const src1 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
await driver.sleep(300);
const src2 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
const sourcesMatch = (src1 === src2);
await expect(sourcesMatch).toBeFalsy(); // 'src' attribute should have changed by now
const logs = await getLogs();
await expect(logs).toEqual([]);
});
+1 -2
View File
@@ -75,8 +75,7 @@ describe('blocks example', () => {
await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/96708228']);
});
// skipping per https://github.com/LLK/scratch-gui/issues/4902 until we have better approach
test.skip('Change categories', async () => {
test('Change categories', async () => {
await loadUri(`${uri}`);
await clickText('Looks');
await clickText('Sound');
-199
View File
@@ -1,199 +0,0 @@
import defineDynamicBlock from '../../../src/lib/define-dynamic-block';
import BlockType from 'scratch-vm/src/extension-support/block-type';
const MockScratchBlocks = {
OUTPUT_SHAPE_HEXAGONAL: 1,
OUTPUT_SHAPE_ROUND: 2,
OUTPUT_SHAPE_SQUARE: 3
};
const categoryInfo = {
name: 'test category',
color1: '#111',
color2: '#222',
color3: '#333'
};
const penIconURI = 'data:image/svg+xml;base64,fake_pen_icon_svg_base64_data';
const testBlockInfo = {
commandWithIcon: {
blockType: BlockType.COMMAND,
blockIconURI: penIconURI,
text: 'command with icon'
},
commandWithoutIcon: {
blockType: BlockType.COMMAND,
text: 'command without icon'
},
terminalCommand: {
blockType: BlockType.COMMAND,
isTerminal: true,
text: 'terminal command'
},
reporter: {
blockType: BlockType.REPORTER,
text: 'reporter'
},
boolean: {
blockType: BlockType.BOOLEAN,
text: 'Boolean'
},
hat: {
blockType: BlockType.HAT,
text: 'hat'
}
};
// similar to goog.mixin from the Closure library
const mixin = function (target, source) {
for (const x in source) {
target[x] = source[x];
}
};
class MockBlock {
constructor (blockInfo, extendedOpcode) {
// mimic Closure-style inheritance by mixing in `defineDynamicBlock` output as this instance's prototype
// see also the `Blockly.Block` constructor
const prototype = defineDynamicBlock(MockScratchBlocks, categoryInfo, blockInfo, extendedOpcode);
mixin(this, prototype);
this.init();
// bootstrap the mutation<->DOM cycle
this.blockInfoText = JSON.stringify(blockInfo);
const xmlElement = this.mutationToDom();
// parse blockInfo from XML to fill dynamic properties
this.domToMutation(xmlElement);
}
jsonInit (json) {
this.result = Object.assign({}, json);
}
interpolate_ () {
// TODO: add tests for this?
}
setCheckboxInFlyout (isEnabled) {
this.result.checkboxInFlyout_ = isEnabled;
}
setOutput (isEnabled) {
this.result.outputConnection = isEnabled; // Blockly calls `makeConnection_` here
}
setOutputShape (outputShape) {
this.result.outputShape_ = outputShape;
}
setNextStatement (isEnabled) {
this.result.nextConnection = isEnabled; // Blockly calls `makeConnection_` here
}
setPreviousStatement (isEnabled) {
this.result.previousConnection = isEnabled; // Blockly calls `makeConnection_` here
}
}
describe('defineDynamicBlock', () => {
test('is a function', () => {
expect(typeof defineDynamicBlock).toBe('function');
});
test('can define a command block with an icon', () => {
const extendedOpcode = 'test.commandWithIcon';
const block = new MockBlock(testBlockInfo.commandWithIcon, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
extensions: ['scratch_extension'],
inputsInline: true,
nextConnection: true,
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
previousConnection: true,
type: extendedOpcode
});
});
test('can define a command block without an icon', () => {
const extendedOpcode = 'test.commandWithoutIcon';
const block = new MockBlock(testBlockInfo.commandWithoutIcon, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
nextConnection: true,
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
previousConnection: true,
type: extendedOpcode
});
});
test('can define a terminal command', () => {
const extendedOpcode = 'test.terminal';
const block = new MockBlock(testBlockInfo.terminalCommand, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
nextConnection: false, // terminal
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
previousConnection: true,
type: extendedOpcode
});
});
test('can define a reporter', () => {
const extendedOpcode = 'test.reporter';
const block = new MockBlock(testBlockInfo.reporter, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
checkboxInFlyout_: true,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
// nextConnection: undefined, // reporter
outputConnection: true, // reporter
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_ROUND, // reporter
// previousConnection: undefined, // reporter
type: extendedOpcode
});
});
test('can define a Boolean', () => {
const extendedOpcode = 'test.boolean';
const block = new MockBlock(testBlockInfo.boolean, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
// checkboxInFlyout_: undefined,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
// nextConnection: undefined, // reporter
outputConnection: true, // reporter
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_HEXAGONAL, // Boolean
// previousConnection: undefined, // reporter
type: extendedOpcode
});
});
test('can define a hat', () => {
const extendedOpcode = 'test.hat';
const block = new MockBlock(testBlockInfo.hat, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
nextConnection: true,
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
// previousConnection: undefined, // hat
type: extendedOpcode
});
});
});
-109
View File
@@ -1,109 +0,0 @@
import DragRecognizer from '../../../src/lib/drag-recognizer';
describe('DragRecognizer', () => {
let onDrag;
let onDragEnd;
let dragRecognizer;
beforeEach(() => {
onDrag = jest.fn();
onDragEnd = jest.fn();
dragRecognizer = new DragRecognizer({onDrag, onDragEnd});
});
afterEach(() => {
dragRecognizer.reset();
});
test('start -> small drag', () => {
dragRecognizer.start({clientX: 100, clientY: 100});
window.dispatchEvent(new MouseEvent('mousemove', {clientX: 101, clientY: 101}));
expect(onDrag).not.toBeCalled();
});
test('start -> large vertical touch move -> scroll, not drag', () => {
dragRecognizer.start({clientX: 100, clientY: 100});
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 106, clientY: 150}));
expect(onDrag).not.toBeCalled();
});
test('start -> large vertical mouse move -> mouse moves always drag)', () => {
dragRecognizer.start({clientX: 100, clientY: 100});
window.dispatchEvent(new MouseEvent('mousemove', {clientX: 100, clientY: 150}));
expect(onDrag).toBeCalled();
});
test('start -> large horizontal touch move -> drag', () => {
dragRecognizer.start({clientX: 100, clientY: 100});
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
expect(onDrag).toBeCalled();
});
test('after starting a scroll, it cannot become a drag', () => {
dragRecognizer.start({clientX: 100, clientY: 100});
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 100, clientY: 110}));
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 100, clientY: 100}));
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 110, clientY: 100}));
expect(onDrag).not.toBeCalled();
});
test('start -> end unbinds', () => {
dragRecognizer.start({clientX: 100, clientY: 100});
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
expect(onDrag).toHaveBeenCalledTimes(1);
window.dispatchEvent(new MouseEvent('touchend', {clientX: 150, clientY: 106}));
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
expect(onDrag).toHaveBeenCalledTimes(1); // Still 1
});
test('start -> reset unbinds', () => {
dragRecognizer.start({clientX: 100, clientY: 100});
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
expect(onDrag).toHaveBeenCalledTimes(1);
dragRecognizer.reset();
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
expect(onDrag).toHaveBeenCalledTimes(1); // Still 1
});
test('scrolls do not call prevent default', () => {
dragRecognizer.start({clientX: 100, clientY: 100});
const event = new MouseEvent('touchmove', {clientX: 100, clientY: 110});
event.preventDefault = jest.fn();
window.dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalledTimes(0);
});
test('confirmed drags have preventDefault called on them', () => {
dragRecognizer.start({clientX: 100, clientY: 100});
const event = new MouseEvent('touchmove', {clientX: 150, clientY: 106});
event.preventDefault = jest.fn();
window.dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalled();
});
test('multiple horizontal drag angles', () => {
// +45 from horizontal => drag
dragRecognizer.start({clientX: 0, clientY: 0});
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 10, clientY: 10}));
expect(onDrag).toHaveBeenCalledTimes(1);
dragRecognizer.reset();
// -45 from horizontal => drag
dragRecognizer.start({clientX: 0, clientY: 0});
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 10, clientY: -10}));
expect(onDrag).toHaveBeenCalledTimes(2);
dragRecognizer.reset();
// +135 from horizontal => drag
dragRecognizer.start({clientX: 0, clientY: 0});
window.dispatchEvent(new MouseEvent('touchmove', {clientX: -10, clientY: 10}));
expect(onDrag).toHaveBeenCalledTimes(3);
dragRecognizer.reset();
// -135 from horizontal => drag
dragRecognizer.start({clientX: 0, clientY: 0});
window.dispatchEvent(new MouseEvent('touchmove', {clientX: -10, clientY: -10}));
expect(onDrag).toHaveBeenCalledTimes(4);
dragRecognizer.reset();
});
});