diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx index d9ceb926957..e398445708f 100644 --- a/src/components/monitor/monitor.jsx +++ b/src/components/monitor/monitor.jsx @@ -1,49 +1,56 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import Draggable from 'react-draggable'; -import {FormattedMessage} from 'react-intl'; -import {ContextMenuTrigger} from 'react-contextmenu'; -import {BorderedMenuItem, ContextMenu, MenuItem} from '../context-menu/context-menu.jsx'; -import Box from '../box/box.jsx'; -import DefaultMonitor from './default-monitor.jsx'; -import LargeMonitor from './large-monitor.jsx'; -import SliderMonitor from '../../containers/slider-monitor.jsx'; -import ListMonitor from '../../containers/list-monitor.jsx'; -import {getColorsForTheme} from '../../lib/themes/index.js'; +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import Draggable from "react-draggable"; +import { FormattedMessage } from "react-intl"; +import { ContextMenuTrigger } from "react-contextmenu"; +import { + BorderedMenuItem, + ContextMenu, + MenuItem, +} from "../context-menu/context-menu.jsx"; +import Box from "../box/box.jsx"; +import DefaultMonitor from "./default-monitor.jsx"; +import LargeMonitor from "./large-monitor.jsx"; +import SliderMonitor from "../../containers/slider-monitor.jsx"; +import ListMonitor from "../../containers/list-monitor.jsx"; +import { getColorsForTheme } from "../../lib/themes/index.js"; -import styles from './monitor.css'; +import styles from "./monitor.css"; -// Map category name to color name used in scratch-blocks Blockly.Colours +// Map category name to color name used in scratch-blocks Blockly.Colours. Note +// that Blockly uses the UK spelling of "colour", so fields that interact +// directly with Blockly follow that convention, while Scratch code uses the US +// spelling of "color". const categoryColorMap = { - data: 'data', - sensing: 'sensing', - sound: 'sounds', - looks: 'looks', - motion: 'motion', - list: 'data_lists', - extension: 'pen' + data: "data", + sensing: "sensing", + sound: "sounds", + looks: "looks", + motion: "motion", + list: "data_lists", + extension: "pen", }; const modes = { default: DefaultMonitor, large: LargeMonitor, slider: SliderMonitor, - list: ListMonitor + list: ListMonitor, }; const getCategoryColor = (theme, category) => { const colors = getColorsForTheme(theme); return { - background: colors[categoryColorMap[category]].primary, - text: colors.text + background: colors[categoryColorMap[category]].colourPrimary, + text: colors.text, }; }; -const MonitorComponent = props => ( +const MonitorComponent = (props) => ( ( {React.createElement(modes[props.mode], { - categoryColor: getCategoryColor(props.theme, props.category), - ...props + categoryColor: getCategoryColor( + props.theme, + props.category + ), + ...props, })} - {ReactDOM.createPortal(( + {ReactDOM.createPortal( // Use a portal to render the context menu outside the flow to avoid // positioning conflicts between the monitors `transform: scale` and // the context menus `position: fixed`. For more details, see // http://meyerweb.com/eric/thoughts/2011/09/12/un-fixing-fixed-elements-with-css-transforms/ - {props.onSetModeToDefault && + {props.onSetModeToDefault && ( - } - {props.onSetModeToLarge && + + )} + {props.onSetModeToLarge && ( - } - {props.onSetModeToSlider && + + )} + {props.onSetModeToSlider && ( - } - {props.onSliderPromptOpen && props.mode === 'slider' && + + )} + {props.onSliderPromptOpen && props.mode === "slider" && ( - } - {props.onImport && + + )} + {props.onImport && ( - } - {props.onExport && + + )} + {props.onExport && ( - } - {props.onHide && + + )} + {props.onHide && ( - } - - ), document.body)} + + )} + , + document.body + )} - ); const monitorModes = Object.keys(modes); @@ -149,15 +170,12 @@ MonitorComponent.propTypes = { onSetModeToLarge: PropTypes.func, onSetModeToSlider: PropTypes.func, onSliderPromptOpen: PropTypes.func, - theme: PropTypes.string.isRequired + theme: PropTypes.string.isRequired, }; MonitorComponent.defaultProps = { - category: 'extension', - mode: 'default' + category: "extension", + mode: "default", }; -export { - MonitorComponent as default, - monitorModes -}; +export { MonitorComponent as default, monitorModes }; diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 750be83fc25..8c5c4da0f63 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -1,39 +1,50 @@ -import bindAll from 'lodash.bindall'; -import debounce from 'lodash.debounce'; -import defaultsDeep from 'lodash.defaultsdeep'; -import makeToolboxXML from '../lib/make-toolbox-xml'; -import PropTypes from 'prop-types'; -import React from 'react'; -import VMScratchBlocks from '../lib/blocks'; -import VM from 'scratch-vm'; - -import log from '../lib/log.js'; -import Prompt from './prompt.jsx'; -import BlocksComponent from '../components/blocks/blocks.jsx'; -import ExtensionLibrary from './extension-library.jsx'; -import extensionData from '../lib/libraries/extensions/index.jsx'; -import CustomProcedures from './custom-procedures.jsx'; -import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; -import {BLOCKS_DEFAULT_SCALE, 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 {DEFAULT_THEME, getColorsForTheme, themeMap} from '../lib/themes'; -import {injectExtensionBlockTheme, injectExtensionCategoryTheme} from '../lib/themes/blockHelpers'; - -import {connect} from 'react-redux'; -import {updateToolbox} from '../reducers/toolbox'; -import {activateColorPicker} from '../reducers/color-picker'; -import {closeExtensionLibrary, openSoundRecorder, openConnectionModal} from '../reducers/modals'; -import {activateCustomProcedures, deactivateCustomProcedures} from '../reducers/custom-procedures'; -import {setConnectionModalExtensionId} from '../reducers/connection-modal'; -import {updateMetrics} from '../reducers/workspace-metrics'; -import {isTimeTravel2020} from '../reducers/time-travel'; - +import bindAll from "lodash.bindall"; +import debounce from "lodash.debounce"; +import defaultsDeep from "lodash.defaultsdeep"; +import makeToolboxXML from "../lib/make-toolbox-xml"; +import PropTypes from "prop-types"; +import React from "react"; +import VMScratchBlocks from "../lib/blocks"; +import VM from "scratch-vm"; + +import log from "../lib/log.js"; +import Prompt from "./prompt.jsx"; +import BlocksComponent from "../components/blocks/blocks.jsx"; +import ExtensionLibrary from "./extension-library.jsx"; +import extensionData from "../lib/libraries/extensions/index.jsx"; +import CustomProcedures from "./custom-procedures.jsx"; +import errorBoundaryHOC from "../lib/error-boundary-hoc.jsx"; +import { + BLOCKS_DEFAULT_SCALE, + 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 { DEFAULT_THEME, getColorsForTheme, themeMap } from "../lib/themes"; +import { + injectExtensionBlockIcons, + injectExtensionCategoryTheme, + getExtensionColors, +} from "../lib/themes/blockHelpers"; + +import { connect } from "react-redux"; +import { updateToolbox } from "../reducers/toolbox"; +import { activateColorPicker } from "../reducers/color-picker"; import { - activateTab, - SOUNDS_TAB_INDEX -} from '../reducers/editor-tab'; + closeExtensionLibrary, + openSoundRecorder, + openConnectionModal, +} from "../reducers/modals"; +import { + activateCustomProcedures, + deactivateCustomProcedures, +} from "../reducers/custom-procedures"; +import { setConnectionModalExtensionId } from "../reducers/connection-modal"; +import { updateMetrics } from "../reducers/workspace-metrics"; +import { isTimeTravel2020 } from "../reducers/time-travel"; + +import { activateTab, SOUNDS_TAB_INDEX } from "../reducers/editor-tab"; const addFunctionListener = (object, property, callback) => { const oldFn = object[property]; @@ -44,99 +55,163 @@ const addFunctionListener = (object, property, callback) => { }; }; -const DroppableBlocks = DropAreaHOC([ - DragConstants.BACKPACK_CODE -])(BlocksComponent); +const DroppableBlocks = DropAreaHOC([DragConstants.BACKPACK_CODE])( + BlocksComponent +); class Blocks extends React.Component { - constructor (props) { + constructor(props) { super(props); this.ScratchBlocks = VMScratchBlocks(props.vm, false); bindAll(this, [ - 'attachVM', - 'detachVM', - 'getToolboxXML', - 'handleCategorySelected', - 'handleConnectionModalStart', - 'handleDrop', - 'handleStatusButtonUpdate', - 'handleOpenSoundRecorder', - 'handlePromptStart', - 'handlePromptCallback', - 'handlePromptClose', - 'handleCustomProceduresClose', - 'onScriptGlowOn', - 'onScriptGlowOff', - 'onBlockGlowOn', - 'onBlockGlowOff', - 'handleMonitorsUpdate', - 'handleExtensionAdded', - 'handleBlocksInfoUpdate', - 'onTargetsUpdate', - 'onVisualReport', - 'onWorkspaceUpdate', - 'onWorkspaceMetricsChange', - 'setBlocks', - 'setLocale' + "attachVM", + "detachVM", + "getToolboxXML", + "handleCategorySelected", + "handleConnectionModalStart", + "handleDrop", + "handleStatusButtonUpdate", + "handleOpenSoundRecorder", + "handlePromptStart", + "handlePromptCallback", + "handlePromptClose", + "handleCustomProceduresClose", + "onScriptGlowOn", + "onScriptGlowOff", + "onBlockGlowOn", + "onBlockGlowOff", + "handleMonitorsUpdate", + "handleExtensionAdded", + "handleBlocksInfoUpdate", + "onTargetsUpdate", + "onVisualReport", + "onWorkspaceUpdate", + "onWorkspaceMetricsChange", + "setBlocks", + "setLocale", ]); - this.ScratchBlocks.prompt = this.handlePromptStart; - this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; + this.ScratchBlocks.dialog.setPrompt(this.handlePromptStart); + this.ScratchBlocks.ScratchVariables.setPromptHandler( + this.handlePromptStart + ); + this.ScratchBlocks.StatusIndicatorLabel.statusButtonCallback = + this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; this.state = { - prompt: null + prompt: null, }; this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100); this.toolboxUpdateQueue = []; } - componentDidMount () { - this.ScratchBlocks = VMScratchBlocks(this.props.vm, this.props.useCatBlocks); - this.ScratchBlocks.prompt = this.handlePromptStart; - this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; + componentDidMount() { + this.ScratchBlocks = VMScratchBlocks( + this.props.vm, + this.props.useCatBlocks + ); + this.ScratchBlocks.dialog.setPrompt(this.handlePromptStart); + this.ScratchBlocks.StatusIndicatorLabel.statusButtonCallback = + this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; - this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; - this.ScratchBlocks.Procedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures; + this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = + this.props.onActivateColorPicker; + this.ScratchBlocks.ScratchProcedures.externalProcedureDefCallback = + this.props.onActivateCustomProcedures; this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); - const workspaceConfig = defaultsDeep({}, + const workspaceConfig = defaultsDeep( + {}, Blocks.defaultOptions, this.props.options, - {rtl: this.props.isRtl, toolbox: this.props.toolboxXML, colours: getColorsForTheme(this.props.theme)} + { + rtl: this.props.isRtl, + toolbox: this.props.toolboxXML, + theme: new this.ScratchBlocks.Theme( + this.props.theme, + getColorsForTheme(this.props.theme) + ), + } ); - this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); + this.workspace = this.ScratchBlocks.inject( + this.blocks, + workspaceConfig + ); + this.workspace.registerToolboxCategoryCallback( + "VARIABLE", + this.ScratchBlocks.ScratchVariables.getVariablesCategory + ); + this.workspace.registerToolboxCategoryCallback( + "PROCEDURE", + this.ScratchBlocks.ScratchProcedures.getProceduresCategory + ); + + this.toolboxUpdateChangeListener = (event) => { + if ( + event.type === this.ScratchBlocks.Events.VAR_CREATE || + event.type === this.ScratchBlocks.Events.VAR_RENAME || + event.type === this.ScratchBlocks.Events.VAR_DELETE || + (event.type === this.ScratchBlocks.Events.BLOCK_DELETE && + event.oldJson.type === "procedures_definition") || + // Only refresh the toolbox when procedure block creations are + // triggered by undoing a deletion (implied by recordUndo being + // false on the event). + (event.type === this.ScratchBlocks.Events.BLOCK_CREATE && + event.json.type === "procedures_definition" && + !event.recordUndo) + ) { + this.requestToolboxUpdate(); + } + }; + this.workspace.addChangeListener(this.toolboxUpdateChangeListener); // Register buttons under new callback keys for creating variables, // lists, and procedures from extensions. const toolboxWorkspace = this.workspace.getFlyout().getWorkspace(); - const varListButtonCallback = type => - (() => this.ScratchBlocks.Variables.createVariable(this.workspace, null, type)); + const varListButtonCallback = (type) => () => + this.ScratchBlocks.ScratchVariables.createVariable( + this.workspace, + null, + type + ); const procButtonCallback = () => { - this.ScratchBlocks.Procedures.createProcedureDefCallback_(this.workspace); + this.ScratchBlocks.ScratchProcedures.createProcedureDefCallback( + this.workspace + ); }; - toolboxWorkspace.registerButtonCallback('MAKE_A_VARIABLE', varListButtonCallback('')); - toolboxWorkspace.registerButtonCallback('MAKE_A_LIST', varListButtonCallback('list')); - toolboxWorkspace.registerButtonCallback('MAKE_A_PROCEDURE', procButtonCallback); + toolboxWorkspace.registerButtonCallback( + "MAKE_A_VARIABLE", + varListButtonCallback("") + ); + toolboxWorkspace.registerButtonCallback( + "MAKE_A_LIST", + varListButtonCallback("list") + ); + toolboxWorkspace.registerButtonCallback( + "MAKE_A_PROCEDURE", + procButtonCallback + ); // Store the xml of the toolbox that is actually rendered. // This is used in componentDidUpdate instead of prevProps, because // the xml can change while e.g. on the costumes tab. this._renderedToolboxXML = this.props.toolboxXML; - // we actually never want the workspace to enable "refresh toolbox" - this basically re-renders the - // entire toolbox every time we reset the workspace. We call updateToolbox as a part of - // componentDidUpdate so the toolbox will still correctly be updated - this.setToolboxRefreshEnabled = this.workspace.setToolboxRefreshEnabled.bind(this.workspace); - this.workspace.setToolboxRefreshEnabled = () => { - this.setToolboxRefreshEnabled(false); - }; - // @todo change this when blockly supports UI events - addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange); - addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange); + addFunctionListener( + this.workspace, + "translate", + this.onWorkspaceMetricsChange + ); + addFunctionListener( + this.workspace, + "zoom", + this.onWorkspaceMetricsChange + ); + this.workspace.getToolbox().selectItemByPosition(0); this.attachVM(); // Only update blocks/vm locale when visible to avoid sizing issues @@ -145,19 +220,21 @@ class Blocks extends React.Component { this.setLocale(); } } - shouldComponentUpdate (nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState) { return ( this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible || this._renderedToolboxXML !== nextProps.toolboxXML || - this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible || - this.props.customProceduresVisible !== nextProps.customProceduresVisible || + this.props.extensionLibraryVisible !== + nextProps.extensionLibraryVisible || + this.props.customProceduresVisible !== + nextProps.customProceduresVisible || this.props.locale !== nextProps.locale || this.props.anyModalVisible !== nextProps.anyModalVisible || this.props.stageSize !== nextProps.stageSize ); } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { // If any modals are open, call hideChaff to close z-indexed field editors if (this.props.anyModalVisible && !prevProps.anyModalVisible) { this.ScratchBlocks.hideChaff(); @@ -166,36 +243,42 @@ class Blocks extends React.Component { // Only rerender the toolbox when the blocks are visible and the xml is // different from the previously rendered toolbox xml. // Do not check against prevProps.toolboxXML because that may not have been rendered. - if (this.props.isVisible && this.props.toolboxXML !== this._renderedToolboxXML) { + if ( + this.props.isVisible && + this.props.toolboxXML !== this._renderedToolboxXML + ) { this.requestToolboxUpdate(); } if (this.props.isVisible === prevProps.isVisible) { if (this.props.stageSize !== prevProps.stageSize) { // force workspace to redraw for the new stage size - window.dispatchEvent(new Event('resize')); + window.dispatchEvent(new Event("resize")); } return; } // @todo hack to resize blockly manually in case resize happened while hidden // @todo hack to reload the workspace due to gui bug #413 - if (this.props.isVisible) { // Scripts tab + if (this.props.isVisible) { + // Scripts tab this.workspace.setVisible(true); - if (prevProps.locale !== this.props.locale || this.props.locale !== this.props.vm.getLocale()) { + if ( + prevProps.locale !== this.props.locale || + this.props.locale !== this.props.vm.getLocale() + ) { // call setLocale if the locale has changed, or changed while the blocks were hidden. // vm.getLocale() will be out of sync if locale was changed while not visible this.setLocale(); } else { this.props.vm.refreshWorkspace(); - this.requestToolboxUpdate(); } - window.dispatchEvent(new Event('resize')); + window.dispatchEvent(new Event("resize")); } else { this.workspace.setVisible(false); } } - componentWillUnmount () { + componentWillUnmount() { this.detachVM(); this.workspace.dispose(); clearTimeout(this.toolboxUpdateTimeout); @@ -203,15 +286,16 @@ class Blocks extends React.Component { // Clear the flyout blocks so that they can be recreated on mount. this.props.vm.clearFlyoutBlocks(); } - requestToolboxUpdate () { + requestToolboxUpdate() { clearTimeout(this.toolboxUpdateTimeout); this.toolboxUpdateTimeout = setTimeout(() => { this.updateToolbox(); }, 0); } - setLocale () { + setLocale() { this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); - this.props.vm.setLocale(this.props.locale, this.props.messages) + this.props.vm + .setLocale(this.props.locale, this.props.messages) .then(() => { this.workspace.getFlyout().setRecyclingEnabled(false); this.props.vm.refreshWorkspace(); @@ -222,33 +306,45 @@ class Blocks extends React.Component { }); } - updateToolbox () { + updateToolbox() { this.toolboxUpdateTimeout = false; - const categoryId = this.workspace.toolbox_.getSelectedCategoryId(); - const offset = this.workspace.toolbox_.getCategoryScrollOffset(); + const scale = this.workspace.getFlyout().getWorkspace().scale; + const selectedCategoryName = this.workspace + .getToolbox() + .getSelectedItem() + .getName(); + const selectedCategoryScrollPosition = + this.workspace + .getFlyout() + .getCategoryScrollPosition(selectedCategoryName) * scale; + const offsetWithinCategory = + this.workspace.getFlyout().getWorkspace().getMetrics().viewTop - + selectedCategoryScrollPosition; + this.workspace.updateToolbox(this.props.toolboxXML); + this.workspace.getToolbox().runAfterRerender(() => { + const newCategoryScrollPosition = this.workspace + .getFlyout() + .getCategoryScrollPosition(selectedCategoryName); + if (newCategoryScrollPosition) { + this.workspace + .getFlyout() + .getWorkspace() + .scrollbar.setY( + newCategoryScrollPosition * scale + offsetWithinCategory + ); + } + }); + this.workspace.getToolbox().forceRerender(); this._renderedToolboxXML = this.props.toolboxXML; - // In order to catch any changes that mutate the toolbox during "normal runtime" - // (variable changes/etc), re-enable toolbox refresh. - // Using the setter function will rerender the entire toolbox which we just rendered. - this.workspace.toolboxRefreshEnabled_ = true; - - const currentCategoryPos = this.workspace.toolbox_.getCategoryPositionById(categoryId); - const currentCategoryLen = this.workspace.toolbox_.getCategoryLengthById(categoryId); - if (offset < currentCategoryLen) { - this.workspace.toolbox_.setFlyoutScrollPos(currentCategoryPos + offset); - } else { - this.workspace.toolbox_.setFlyoutScrollPos(currentCategoryPos); - } - const queue = this.toolboxUpdateQueue; this.toolboxUpdateQueue = []; - queue.forEach(fn => fn()); + queue.forEach((fn) => fn()); } - withToolboxUpdates (fn) { + withToolboxUpdates(fn) { // if there is a queued toolbox update, we need to wait if (this.toolboxUpdateTimeout) { this.toolboxUpdateQueue.push(fn); @@ -257,42 +353,68 @@ class Blocks extends React.Component { } } - attachVM () { + attachVM() { this.workspace.addChangeListener(this.props.vm.blockListener); - this.flyoutWorkspace = this.workspace - .getFlyout() - .getWorkspace(); - this.flyoutWorkspace.addChangeListener(this.props.vm.flyoutBlockListener); - this.flyoutWorkspace.addChangeListener(this.props.vm.monitorBlockListener); - this.props.vm.addListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); - this.props.vm.addListener('SCRIPT_GLOW_OFF', this.onScriptGlowOff); - this.props.vm.addListener('BLOCK_GLOW_ON', this.onBlockGlowOn); - 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); - this.props.vm.addListener('MONITORS_UPDATE', this.handleMonitorsUpdate); - this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded); - this.props.vm.addListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); - this.props.vm.addListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); - this.props.vm.addListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); - } - detachVM () { - this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); - this.props.vm.removeListener('SCRIPT_GLOW_OFF', this.onScriptGlowOff); - this.props.vm.removeListener('BLOCK_GLOW_ON', this.onBlockGlowOn); - 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); - this.props.vm.removeListener('MONITORS_UPDATE', this.handleMonitorsUpdate); - this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded); - this.props.vm.removeListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); - this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); - this.props.vm.removeListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); - } - - updateToolboxBlockValue (id, value) { + this.flyoutWorkspace = this.workspace.getFlyout().getWorkspace(); + this.flyoutWorkspace.addChangeListener( + this.props.vm.flyoutBlockListener + ); + this.flyoutWorkspace.addChangeListener( + this.props.vm.monitorBlockListener + ); + this.props.vm.addListener("SCRIPT_GLOW_ON", this.onScriptGlowOn); + this.props.vm.addListener("SCRIPT_GLOW_OFF", this.onScriptGlowOff); + this.props.vm.addListener("BLOCK_GLOW_ON", this.onBlockGlowOn); + 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); + this.props.vm.addListener("MONITORS_UPDATE", this.handleMonitorsUpdate); + this.props.vm.addListener("EXTENSION_ADDED", this.handleExtensionAdded); + this.props.vm.addListener( + "BLOCKSINFO_UPDATE", + this.handleBlocksInfoUpdate + ); + this.props.vm.addListener( + "PERIPHERAL_CONNECTED", + this.handleStatusButtonUpdate + ); + this.props.vm.addListener( + "PERIPHERAL_DISCONNECTED", + this.handleStatusButtonUpdate + ); + } + detachVM() { + this.props.vm.removeListener("SCRIPT_GLOW_ON", this.onScriptGlowOn); + this.props.vm.removeListener("SCRIPT_GLOW_OFF", this.onScriptGlowOff); + this.props.vm.removeListener("BLOCK_GLOW_ON", this.onBlockGlowOn); + 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); + this.props.vm.removeListener( + "MONITORS_UPDATE", + this.handleMonitorsUpdate + ); + this.props.vm.removeListener( + "EXTENSION_ADDED", + this.handleExtensionAdded + ); + this.props.vm.removeListener( + "BLOCKSINFO_UPDATE", + this.handleBlocksInfoUpdate + ); + this.props.vm.removeListener( + "PERIPHERAL_CONNECTED", + this.handleStatusButtonUpdate + ); + this.props.vm.removeListener( + "PERIPHERAL_DISCONNECTED", + this.handleStatusButtonUpdate + ); + } + + updateToolboxBlockValue(id, value) { this.withToolboxUpdates(() => { const block = this.workspace .getFlyout() @@ -304,15 +426,21 @@ class Blocks extends React.Component { }); } - onTargetsUpdate () { + onTargetsUpdate() { if (this.props.vm.editingTarget && this.workspace.getFlyout()) { - ['glide', 'move', 'set'].forEach(prefix => { - this.updateToolboxBlockValue(`${prefix}x`, Math.round(this.props.vm.editingTarget.x).toString()); - this.updateToolboxBlockValue(`${prefix}y`, Math.round(this.props.vm.editingTarget.y).toString()); + ["glide", "move", "set"].forEach((prefix) => { + this.updateToolboxBlockValue( + `${prefix}x`, + Math.round(this.props.vm.editingTarget.x).toString() + ); + this.updateToolboxBlockValue( + `${prefix}y`, + Math.round(this.props.vm.editingTarget.y).toString() + ); }); } } - onWorkspaceMetricsChange () { + onWorkspaceMetricsChange() { const target = this.props.vm.editingTarget; if (target && target.id) { // Dispatch updateMetrics later, since onWorkspaceMetricsChange may be (very indirectly) @@ -323,32 +451,32 @@ class Blocks extends React.Component { targetID: target.id, scrollX: this.workspace.scrollX, scrollY: this.workspace.scrollY, - scale: this.workspace.scale + scale: this.workspace.scale, }); }, 0); } } - onScriptGlowOn (data) { - this.workspace.glowStack(data.id, true); + onScriptGlowOn(data) { + this.ScratchBlocks.glowStack(data.id, true); } - onScriptGlowOff (data) { - this.workspace.glowStack(data.id, false); + onScriptGlowOff(data) { + this.ScratchBlocks.glowStack(data.id, false); } - onBlockGlowOn (data) { - this.workspace.glowBlock(data.id, true); + onBlockGlowOn(data) { + // No-op, support may be added in the future } - onBlockGlowOff (data) { - this.workspace.glowBlock(data.id, false); + onBlockGlowOff(data) { + // No-op, support may be added in the future } - onVisualReport (data) { - this.workspace.reportValue(data.id, data.value); + onVisualReport(data) { + this.ScratchBlocks.reportValue(data.id, data.value); } - getToolboxXML () { + getToolboxXML() { // Use try/catch because this requires digging pretty deep into the VM // Code inside intentionally ignores several error situations (no stage, etc.) // Because they would get caught by this try/catch try { - let {editingTarget: target, runtime} = this.props.vm; + let { editingTarget: target, runtime } = this.props.vm; const stage = runtime.getTargetForStage(); if (!target) target = stage; // If no editingTarget, use the stage @@ -359,32 +487,45 @@ class Blocks extends React.Component { this.props.vm.runtime.getBlocksXML(target), this.props.theme ); - return makeToolboxXML(false, target.isStage, target.id, dynamicBlocksXML, + return makeToolboxXML( + false, + target.isStage, + target.id, + dynamicBlocksXML, targetCostumes[targetCostumes.length - 1].name, stageCostumes[stageCostumes.length - 1].name, - targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : '', + targetSounds.length > 0 + ? targetSounds[targetSounds.length - 1].name + : "", getColorsForTheme(this.props.theme) ); } catch { return null; } } - onWorkspaceUpdate (data) { + onWorkspaceUpdate(data) { // When we change sprites, update the toolbox to have the new sprite's blocks const toolboxXML = this.getToolboxXML(); if (toolboxXML) { this.props.updateToolboxState(toolboxXML); } - if (this.props.vm.editingTarget && !this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id]) { + if ( + this.props.vm.editingTarget && + !this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id] + ) { this.onWorkspaceMetricsChange(); } // Remove and reattach the workspace listener (but allow flyout events) this.workspace.removeChangeListener(this.props.vm.blockListener); - const dom = this.ScratchBlocks.Xml.textToDom(data.xml); + this.workspace.removeChangeListener(this.toolboxUpdateChangeListener); + const dom = this.ScratchBlocks.utils.xml.textToDom(data.xml); try { - this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace); + this.ScratchBlocks.clearWorkspaceAndLoadFromXml( + dom, + this.workspace + ); } catch (error) { // The workspace is likely incomplete. What did update should be // functional. @@ -402,8 +543,14 @@ class Blocks extends React.Component { } this.workspace.addChangeListener(this.props.vm.blockListener); - if (this.props.vm.editingTarget && this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id]) { - const {scrollX, scrollY, scale} = this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id]; + if ( + this.props.vm.editingTarget && + this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id] + ) { + const { scrollX, scrollY, scale } = + this.props.workspaceMetrics.targets[ + this.props.vm.editingTarget.id + ]; this.workspace.scrollX = scrollX; this.workspace.scrollY = scrollY; this.workspace.scale = scale; @@ -414,15 +561,24 @@ class Blocks extends React.Component { // fresh workspace and we don't want any changes made to another sprites // workspace to be 'undone' here. this.workspace.clearUndo(); + // Let events get flushed before readding the toolbox-updater listener + // to avoid unneeded refreshes. + requestAnimationFrame(() => { + setTimeout(() => { + this.workspace.addChangeListener( + this.toolboxUpdateChangeListener + ); + }); + }); } - handleMonitorsUpdate (monitors) { + handleMonitorsUpdate(monitors) { // Update the checkboxes of the relevant monitors. // TODO: What about monitors that have fields? See todo in scratch-vm blocks.js changeBlock: // https://github.com/LLK/scratch-vm/blob/2373f9483edaf705f11d62662f7bb2a57fbb5e28/src/engine/blocks.js#L569-L576 const flyout = this.workspace.getFlyout(); for (const monitor of monitors.values()) { - const blockId = monitor.get('id'); - const isVisible = monitor.get('visible'); + const blockId = monitor.get("id"); + const isVisible = monitor.get("visible"); flyout.setCheckboxState(blockId, isVisible); // We also need to update the isMonitored flag for this block on the VM, since it's used to determine // whether the checkbox is activated or not when the checkbox is re-displayed (e.g. local variables/blocks @@ -433,28 +589,37 @@ class Blocks extends React.Component { } } } - handleExtensionAdded (categoryInfo) { - const defineBlocks = blockInfoArray => { + handleExtensionAdded(categoryInfo) { + const defineBlocks = (blockInfoArray) => { if (blockInfoArray && blockInfoArray.length > 0) { const staticBlocksJson = []; const dynamicBlocksInfo = []; - blockInfoArray.forEach(blockInfo => { + blockInfoArray.forEach((blockInfo) => { if (blockInfo.info && blockInfo.info.isDynamic) { dynamicBlocksInfo.push(blockInfo); } else if (blockInfo.json) { - staticBlocksJson.push(injectExtensionBlockTheme(blockInfo.json, this.props.theme)); + staticBlocksJson.push( + injectExtensionBlockIcons( + blockInfo.json, + this.props.theme + ) + ); } // otherwise it's a non-block entry such as '---' }); this.ScratchBlocks.defineBlocksWithJsonArray(staticBlocksJson); - dynamicBlocksInfo.forEach(blockInfo => { + 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); + const blockDefinition = defineDynamicBlock( + this.ScratchBlocks, + categoryInfo, + blockInfo, + extendedOpcode + ); this.ScratchBlocks.Blocks[extendedOpcode] = blockDefinition; }); } @@ -463,54 +628,99 @@ class Blocks extends React.Component { // 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)); + Object.getOwnPropertyNames(categoryInfo.customFieldTypes).map( + (fieldTypeName) => + categoryInfo.customFieldTypes[fieldTypeName] + .scratchBlocksDefinition + ) + ); defineBlocks(categoryInfo.menus); defineBlocks(categoryInfo.blocks); - + // Note that Blockly uses the UK spelling of "colour", so fields that + // interact directly with Blockly follow that convention, while Scratch + // code uses the US spelling of "color". + let colourPrimary = categoryInfo.color1; + let colourSecondary = categoryInfo.color2; + let colourTertiary = categoryInfo.color3; + let colourQuaternary = categoryInfo.color3; + if (this.props.theme !== DEFAULT_THEME) { + const colors = getExtensionColors(this.props.theme); + colourPrimary = colors.colourPrimary; + colourSecondary = colors.colourSecondary; + colourTertiary = colors.colourTertiary; + colourQuaternary = colors.colourQuaternary; + } + this.ScratchBlocks.getMainWorkspace() + .getTheme() + .setBlockStyle(categoryInfo.id, { + colourPrimary, + colourSecondary, + colourTertiary, + colourQuaternary, + }); + this.ScratchBlocks.getMainWorkspace() + .getTheme() + .setBlockStyle(`${categoryInfo.id}_selected`, { + colourPrimary: colourQuaternary, + colourSecondary: colourQuaternary, + colourTertiary: colourQuaternary, + colourQuaternary: colourQuaternary, + }); + this.ScratchBlocks.getMainWorkspace().setTheme( + this.ScratchBlocks.getMainWorkspace().getTheme() + ); // Update the toolbox with new blocks if possible const toolboxXML = this.getToolboxXML(); if (toolboxXML) { this.props.updateToolboxState(toolboxXML); } } - handleBlocksInfoUpdate (categoryInfo) { + handleBlocksInfoUpdate(categoryInfo) { // @todo Later we should replace this to avoid all the warnings from redefining blocks. this.handleExtensionAdded(categoryInfo); } - handleCategorySelected (categoryId) { - const extension = extensionData.find(ext => ext.extensionId === categoryId); + handleCategorySelected(categoryId) { + const extension = extensionData.find( + (ext) => ext.extensionId === categoryId + ); if (extension && extension.launchPeripheralConnectionFlow) { this.handleConnectionModalStart(categoryId); } this.withToolboxUpdates(() => { - this.workspace.toolbox_.setSelectedCategoryById(categoryId); + const toolbox = this.workspace.getToolbox(); + toolbox.setSelectedItem(toolbox.getToolboxItemById(categoryId)); }); } - setBlocks (blocks) { + setBlocks(blocks) { this.blocks = blocks; } - handlePromptStart (message, defaultValue, callback, optTitle, optVarType) { - const p = {prompt: {callback, message, defaultValue}}; - p.prompt.title = optTitle ? optTitle : - this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE; - p.prompt.varType = typeof optVarType === 'string' ? - optVarType : this.ScratchBlocks.SCALAR_VARIABLE_TYPE; + handlePromptStart(message, defaultValue, callback, optTitle, optVarType) { + const p = { prompt: { callback, message, defaultValue } }; + p.prompt.title = optTitle + ? optTitle + : this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE; + p.prompt.varType = + typeof optVarType === "string" + ? optVarType + : this.ScratchBlocks.SCALAR_VARIABLE_TYPE; p.prompt.showVariableOptions = // This flag means that we should show variable/list options about scope optVarType !== this.ScratchBlocks.BROADCAST_MESSAGE_VARIABLE_TYPE && - p.prompt.title !== this.ScratchBlocks.Msg.RENAME_VARIABLE_MODAL_TITLE && + p.prompt.title !== + this.ScratchBlocks.Msg.RENAME_VARIABLE_MODAL_TITLE && p.prompt.title !== this.ScratchBlocks.Msg.RENAME_LIST_MODAL_TITLE; - p.prompt.showCloudOption = (optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud; + p.prompt.showCloudOption = + optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE && + this.props.canUseCloud; this.setState(p); } - handleConnectionModalStart (extensionId) { + handleConnectionModalStart(extensionId) { this.props.onOpenConnectionModal(extensionId); } - handleStatusButtonUpdate () { - this.ScratchBlocks.refreshStatusButtons(this.workspace); + handleStatusButtonUpdate() { + this.workspace.getFlyout().refreshStatusButtons(); } - handleOpenSoundRecorder () { + handleOpenSoundRecorder() { this.props.onOpenSoundRecorder(); } @@ -519,32 +729,39 @@ class Blocks extends React.Component { * and additional potentially conflicting variable names from the VM * to the variable validation prompt callback used in scratch-blocks. */ - handlePromptCallback (input, variableOptions) { + handlePromptCallback(input, variableOptions) { this.state.prompt.callback( input, - this.props.vm.runtime.getAllVarNamesOfType(this.state.prompt.varType), - variableOptions); + this.props.vm.runtime.getAllVarNamesOfType( + this.state.prompt.varType + ), + variableOptions + ); this.handlePromptClose(); } - handlePromptClose () { - this.setState({prompt: null}); + handlePromptClose() { + this.setState({ prompt: null }); } - handleCustomProceduresClose (data) { + handleCustomProceduresClose(data) { this.props.onRequestCloseCustomProcedures(data); const ws = this.workspace; - ws.refreshToolboxSelection_(); - ws.toolbox_.scrollToCategoryById('myBlocks'); + this.updateToolbox(); + ws.getToolbox().selectCategoryByName("myBlocks"); } - handleDrop (dragInfo) { + handleDrop(dragInfo) { fetch(dragInfo.payload.bodyUrl) - .then(response => response.json()) - .then(blocks => this.props.vm.shareBlocksToTarget(blocks, this.props.vm.editingTarget.id)) + .then((response) => response.json()) + .then((blocks) => + this.props.vm.shareBlocksToTarget( + blocks, + this.props.vm.editingTarget.id + ) + ) .then(() => { this.props.vm.refreshWorkspace(); - this.updateToolbox(); // To show new variables/custom blocks }); } - render () { + render() { /* eslint-disable no-unused-vars */ const { anyModalVisible, @@ -567,6 +784,7 @@ class Blocks extends React.Component { updateMetrics: updateMetricsProp, useCatBlocks, workspaceMetrics, + theme, ...props } = this.props; /* eslint-enable no-unused-vars */ @@ -581,10 +799,15 @@ class Blocks extends React.Component { ) : null} @@ -631,10 +855,10 @@ Blocks.propTypes = { zoom: PropTypes.shape({ controls: PropTypes.bool, wheel: PropTypes.bool, - startScale: PropTypes.number + startScale: PropTypes.number, }), comments: PropTypes.bool, - collapse: PropTypes.bool + collapse: PropTypes.bool, }), stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired, theme: PropTypes.oneOf(Object.keys(themeMap)), @@ -644,37 +868,43 @@ Blocks.propTypes = { useCatBlocks: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired, workspaceMetrics: PropTypes.shape({ - targets: PropTypes.objectOf(PropTypes.object) - }) + targets: PropTypes.objectOf(PropTypes.object), + }), }; Blocks.defaultOptions = { zoom: { controls: true, wheel: true, - startScale: BLOCKS_DEFAULT_SCALE + pinch: true, + startScale: BLOCKS_DEFAULT_SCALE, + }, + move: { + wheel: true, }, grid: { spacing: 40, length: 2, - colour: '#ddd' + colour: "#ddd", }, comments: true, collapse: false, - sounds: false + sounds: false, + trashcan: false, + modalInputs: false, }; Blocks.defaultProps = { isVisible: true, options: Blocks.defaultOptions, - theme: DEFAULT_THEME + theme: DEFAULT_THEME, }; -const mapStateToProps = state => ({ - anyModalVisible: ( - Object.keys(state.scratchGui.modals).some(key => state.scratchGui.modals[key]) || - state.scratchGui.mode.isFullScreen - ), +const mapStateToProps = (state) => ({ + anyModalVisible: + Object.keys(state.scratchGui.modals).some( + (key) => state.scratchGui.modals[key] + ) || state.scratchGui.mode.isFullScreen, extensionLibraryVisible: state.scratchGui.modals.extensionLibrary, isRtl: state.locales.isRtl, locale: state.locales.locale, @@ -682,13 +912,15 @@ const mapStateToProps = state => ({ toolboxXML: state.scratchGui.toolbox.toolboxXML, customProceduresVisible: state.scratchGui.customProcedures.active, workspaceMetrics: state.scratchGui.workspaceMetrics, - useCatBlocks: isTimeTravel2020(state) + useCatBlocks: isTimeTravel2020(state), }); -const mapDispatchToProps = dispatch => ({ - onActivateColorPicker: callback => dispatch(activateColorPicker(callback)), - onActivateCustomProcedures: (data, callback) => dispatch(activateCustomProcedures(data, callback)), - onOpenConnectionModal: id => { +const mapDispatchToProps = (dispatch) => ({ + onActivateColorPicker: (callback) => + dispatch(activateColorPicker(callback)), + onActivateCustomProcedures: (data, callback) => + dispatch(activateCustomProcedures(data, callback)), + onOpenConnectionModal: (id) => { dispatch(setConnectionModalExtensionId(id)); dispatch(openConnectionModal()); }, @@ -699,20 +931,17 @@ const mapDispatchToProps = dispatch => ({ onRequestCloseExtensionLibrary: () => { dispatch(closeExtensionLibrary()); }, - onRequestCloseCustomProcedures: data => { + onRequestCloseCustomProcedures: (data) => { dispatch(deactivateCustomProcedures(data)); }, - updateToolboxState: toolboxXML => { + updateToolboxState: (toolboxXML) => { dispatch(updateToolbox(toolboxXML)); }, - updateMetrics: metrics => { + updateMetrics: (metrics) => { dispatch(updateMetrics(metrics)); - } + }, }); -export default errorBoundaryHOC('Blocks')( - connect( - mapStateToProps, - mapDispatchToProps - )(Blocks) +export default errorBoundaryHOC("Blocks")( + connect(mapStateToProps, mapDispatchToProps)(Blocks) ); diff --git a/src/containers/custom-procedures.jsx b/src/containers/custom-procedures.jsx index e691b0466f7..068b9097b16 100644 --- a/src/containers/custom-procedures.jsx +++ b/src/containers/custom-procedures.jsx @@ -1,50 +1,53 @@ -import bindAll from 'lodash.bindall'; -import defaultsDeep from 'lodash.defaultsdeep'; -import PropTypes from 'prop-types'; -import React from 'react'; -import CustomProceduresComponent from '../components/custom-procedures/custom-procedures.jsx'; -import ScratchBlocks from 'scratch-blocks'; -import {connect} from 'react-redux'; +import bindAll from "lodash.bindall"; +import defaultsDeep from "lodash.defaultsdeep"; +import PropTypes from "prop-types"; +import React from "react"; +import CustomProceduresComponent from "../components/custom-procedures/custom-procedures.jsx"; +import { getColorsForTheme, themeMap } from "../lib/themes"; +import { ScratchBlocks } from "scratch-blocks"; +import { connect } from "react-redux"; class CustomProcedures extends React.Component { - constructor (props) { + constructor(props) { super(props); bindAll(this, [ - 'handleAddLabel', - 'handleAddBoolean', - 'handleAddTextNumber', - 'handleToggleWarp', - 'handleCancel', - 'handleOk', - 'setBlocks' + "handleAddLabel", + "handleAddBoolean", + "handleAddTextNumber", + "handleToggleWarp", + "handleCancel", + "handleOk", + "setBlocks", ]); this.state = { rtlOffset: 0, - warp: false + warp: false, }; } - componentWillUnmount () { + componentWillUnmount() { if (this.workspace) { this.workspace.dispose(); } } - setBlocks (blocksRef) { + setBlocks(blocksRef) { if (!blocksRef) return; this.blocks = blocksRef; - const workspaceConfig = defaultsDeep({}, + const workspaceConfig = defaultsDeep( + {}, CustomProcedures.defaultOptions, this.props.options, - {rtl: this.props.isRtl} + { rtl: this.props.isRtl } ); - // @todo This is a hack to make there be no toolbox. - const oldDefaultToolbox = ScratchBlocks.Blocks.defaultToolbox; - ScratchBlocks.Blocks.defaultToolbox = null; + const theme = new ScratchBlocks.Theme( + this.props.theme, + getColorsForTheme(this.props.theme) + ); + workspaceConfig.theme = theme; this.workspace = ScratchBlocks.inject(this.blocks, workspaceConfig); - ScratchBlocks.Blocks.defaultToolbox = oldDefaultToolbox; // Create the procedure declaration block for editing the mutation. - this.mutationRoot = this.workspace.newBlock('procedures_declaration'); + this.mutationRoot = this.workspace.newBlock("procedures_declaration"); // Make the declaration immovable, undeletable and have no context menu this.mutationRoot.setMovable(false); this.mutationRoot.setDeletable(false); @@ -54,8 +57,9 @@ class CustomProcedures extends React.Component { this.mutationRoot.onChangeFn(); // Keep the block centered on the workspace const metrics = this.workspace.getMetrics(); - const {x, y} = this.mutationRoot.getRelativeToSurfaceXY(); - const dy = (metrics.viewHeight / 2) - (this.mutationRoot.height / 2) - y; + const { x, y } = this.mutationRoot.getRelativeToSurfaceXY(); + const dy = + metrics.viewHeight / 2 - this.mutationRoot.height / 2 - y; let dx; if (this.props.isRtl) { // // TODO: https://github.com/LLK/scratch-gui/issues/2838 @@ -67,8 +71,9 @@ class CustomProcedures extends React.Component { // Calculate a new left postion based on new width // Convert current x position into LTR (mirror) x position (uses original offset) // Use the difference between ltrX and mirrorX as the amount to move - const ltrX = ((metrics.viewWidth / 2) - (this.mutationRoot.width / 2) + 25); - const mirrorX = x - ((x - this.state.rtlOffset) * 2); + const ltrX = + metrics.viewWidth / 2 - this.mutationRoot.width / 2 + 25; + const mirrorX = x - (x - this.state.rtlOffset) * 2; if (mirrorX === ltrX) { return; } @@ -79,19 +84,25 @@ class CustomProcedures extends React.Component { if (this.mutationRoot.width < midPoint) { dx = ltrX; } else if (this.mutationRoot.width < metrics.viewWidth) { - dx = midPoint - ((metrics.viewWidth - this.mutationRoot.width) / 2); + dx = + midPoint - + (metrics.viewWidth - this.mutationRoot.width) / 2; } else { - dx = midPoint + (this.mutationRoot.width - metrics.viewWidth); + dx = + midPoint + + (this.mutationRoot.width - metrics.viewWidth); } this.mutationRoot.moveBy(dx, dy); - this.setState({rtlOffset: this.mutationRoot.getRelativeToSurfaceXY().x}); + this.setState({ + rtlOffset: this.mutationRoot.getRelativeToSurfaceXY().x, + }); return; } if (this.mutationRoot.width > metrics.viewWidth) { dx = dx + this.mutationRoot.width - metrics.viewWidth; } } else { - dx = (metrics.viewWidth / 2) - (this.mutationRoot.width / 2) - x; + dx = metrics.viewWidth / 2 - this.mutationRoot.width / 2 - x; // If the procedure declaration is wider than the view width, // keep the right-hand side of the procedure in view. if (this.mutationRoot.width > metrics.viewWidth) { @@ -103,42 +114,44 @@ class CustomProcedures extends React.Component { this.mutationRoot.domToMutation(this.props.mutator); this.mutationRoot.initSvg(); this.mutationRoot.render(); - this.setState({warp: this.mutationRoot.getWarp()}); + this.setState({ warp: this.mutationRoot.getWarp() }); // Allow the initial events to run to position this block, then focus. setTimeout(() => { this.mutationRoot.focusLastEditor_(); }); } - handleCancel () { + handleCancel() { this.props.onRequestClose(); } - handleOk () { - const newMutation = this.mutationRoot ? this.mutationRoot.mutationToDom(true) : null; + handleOk() { + const newMutation = this.mutationRoot + ? this.mutationRoot.mutationToDom(true) + : null; this.props.onRequestClose(newMutation); } - handleAddLabel () { + handleAddLabel() { if (this.mutationRoot) { this.mutationRoot.addLabelExternal(); } } - handleAddBoolean () { + handleAddBoolean() { if (this.mutationRoot) { this.mutationRoot.addBooleanExternal(); } } - handleAddTextNumber () { + handleAddTextNumber() { if (this.mutationRoot) { this.mutationRoot.addStringNumberExternal(); } } - handleToggleWarp () { + handleToggleWarp() { if (this.mutationRoot) { const newWarp = !this.mutationRoot.getWarp(); this.mutationRoot.setWarp(newWarp); - this.setState({warp: newWarp}); + this.setState({ warp: newWarp }); } } - render () { + render() { return ( ({ +const mapStateToProps = (state) => ({ isRtl: state.locales.isRtl, - mutator: state.scratchGui.customProcedures.mutator + mutator: state.scratchGui.customProcedures.mutator, }); -export default connect( - mapStateToProps -)(CustomProcedures); +export default connect(mapStateToProps)(CustomProcedures); diff --git a/src/lib/blocks.js b/src/lib/blocks.js index f89f5fe0b02..474943751d4 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -5,126 +5,154 @@ * @return {ScratchBlocks} ScratchBlocks connected with the vm */ export default function (vm, useCatBlocks) { - const ScratchBlocks = useCatBlocks ? require('cat-blocks') : require('scratch-blocks'); - const jsonForMenuBlock = function (name, menuOptionsFn, colors, start) { + const { ScratchBlocks } = useCatBlocks + ? require("cat-blocks") + : require("scratch-blocks"); + const jsonForMenuBlock = function (name, menuOptionsFn, category, start) { return { - message0: '%1', + message0: "%1", args0: [ { - type: 'field_dropdown', + type: "field_dropdown", name: name, options: function () { return start.concat(menuOptionsFn()); - } - } + }, + }, ], inputsInline: true, - output: 'String', - colour: colors.secondary, - colourSecondary: colors.secondary, - colourTertiary: colors.tertiary, - colourQuaternary: colors.quaternary, - outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND + output: "String", + outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND, + extensions: [`colours_${category}`], }; }; - const jsonForHatBlockMenu = function (hatName, name, menuOptionsFn, colors, start) { + const jsonForHatBlockMenu = function ( + hatName, + name, + menuOptionsFn, + category, + start + ) { return { message0: hatName, args0: [ { - type: 'field_dropdown', + type: "field_dropdown", name: name, options: function () { return start.concat(menuOptionsFn()); - } - } + }, + }, ], - colour: colors.primary, - colourSecondary: colors.secondary, - colourTertiary: colors.tertiary, - colourQuaternary: colors.quaternary, - extensions: ['shape_hat'] + extensions: [`colours_${category}`, "shape_hat"], }; }; - const jsonForSensingMenus = function (menuOptionsFn) { return { message0: ScratchBlocks.Msg.SENSING_OF, args0: [ { - type: 'field_dropdown', - name: 'PROPERTY', + type: "field_dropdown", + name: "PROPERTY", options: function () { return menuOptionsFn(); - } - + }, }, { - type: 'input_value', - name: 'OBJECT' - } + type: "input_value", + name: "OBJECT", + }, ], output: true, - colour: ScratchBlocks.Colours.sensing.primary, - colourSecondary: ScratchBlocks.Colours.sensing.secondary, - colourTertiary: ScratchBlocks.Colours.sensing.tertiary, - colourQuaternary: ScratchBlocks.Colours.sensing.quaternary, - outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND + outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND, + extensions: ["colours_sensing"], }; }; const soundsMenu = function () { - let menu = [['', '']]; + let menu = [["", ""]]; if (vm.editingTarget && vm.editingTarget.sprite.sounds.length > 0) { - menu = vm.editingTarget.sprite.sounds.map(sound => [sound.name, sound.name]); + menu = vm.editingTarget.sprite.sounds.map((sound) => [ + sound.name, + sound.name, + ]); } menu.push([ - ScratchBlocks.ScratchMsgs.translate('SOUND_RECORD', 'record...'), - ScratchBlocks.recordSoundCallback + ScratchBlocks.ScratchMsgs.translate("SOUND_RECORD", "record..."), + "SOUND_RECORD", ]); return menu; }; const costumesMenu = function () { if (vm.editingTarget && vm.editingTarget.getCostumes().length > 0) { - return vm.editingTarget.getCostumes().map(costume => [costume.name, costume.name]); + return vm.editingTarget + .getCostumes() + .map((costume) => [costume.name, costume.name]); } - return [['', '']]; + return [["", ""]]; }; const backdropsMenu = function () { - const next = ScratchBlocks.ScratchMsgs.translate('LOOKS_NEXTBACKDROP', 'next backdrop'); - const previous = ScratchBlocks.ScratchMsgs.translate('LOOKS_PREVIOUSBACKDROP', 'previous backdrop'); - const random = ScratchBlocks.ScratchMsgs.translate('LOOKS_RANDOMBACKDROP', 'random backdrop'); - if (vm.runtime.targets[0] && vm.runtime.targets[0].getCostumes().length > 0) { - return vm.runtime.targets[0].getCostumes().map(costume => [costume.name, costume.name]) - .concat([[next, 'next backdrop'], - [previous, 'previous backdrop'], - [random, 'random backdrop']]); + const next = ScratchBlocks.ScratchMsgs.translate( + "LOOKS_NEXTBACKDROP", + "next backdrop" + ); + const previous = ScratchBlocks.ScratchMsgs.translate( + "LOOKS_PREVIOUSBACKDROP", + "previous backdrop" + ); + const random = ScratchBlocks.ScratchMsgs.translate( + "LOOKS_RANDOMBACKDROP", + "random backdrop" + ); + if ( + vm.runtime.targets[0] && + vm.runtime.targets[0].getCostumes().length > 0 + ) { + return vm.runtime.targets[0] + .getCostumes() + .map((costume) => [costume.name, costume.name]) + .concat([ + [next, "next backdrop"], + [previous, "previous backdrop"], + [random, "random backdrop"], + ]); } - return [['', '']]; + return [["", ""]]; }; const backdropNamesMenu = function () { const stage = vm.runtime.getTargetForStage(); if (stage && stage.getCostumes().length > 0) { - return stage.getCostumes().map(costume => [costume.name, costume.name]); + return stage + .getCostumes() + .map((costume) => [costume.name, costume.name]); } - return [['', '']]; + return [["", ""]]; }; const spriteMenu = function () { const sprites = []; for (const targetId in vm.runtime.targets) { - if (!Object.prototype.hasOwnProperty.call(vm.runtime.targets, targetId)) continue; + if ( + !Object.prototype.hasOwnProperty.call( + vm.runtime.targets, + targetId + ) + ) + continue; if (vm.runtime.targets[targetId].isOriginal) { if (!vm.runtime.targets[targetId].isStage) { if (vm.runtime.targets[targetId] === vm.editingTarget) { continue; } - sprites.push([vm.runtime.targets[targetId].sprite.name, vm.runtime.targets[targetId].sprite.name]); + sprites.push([ + vm.runtime.targets[targetId].sprite.name, + vm.runtime.targets[targetId].sprite.name, + ]); } } } @@ -135,80 +163,100 @@ export default function (vm, useCatBlocks) { if (vm.editingTarget && vm.editingTarget.isStage) { const menu = spriteMenu(); if (menu.length === 0) { - return [['', '']]; // Empty menu matches Scratch 2 behavior + return [["", ""]]; // Empty menu matches Scratch 2 behavior } return menu; } - const myself = ScratchBlocks.ScratchMsgs.translate('CONTROL_CREATECLONEOF_MYSELF', 'myself'); - return [[myself, '_myself_']].concat(spriteMenu()); + const myself = ScratchBlocks.ScratchMsgs.translate( + "CONTROL_CREATECLONEOF_MYSELF", + "myself" + ); + return [[myself, "_myself_"]].concat(spriteMenu()); }; - const soundColors = ScratchBlocks.Colours.sounds; - - const looksColors = ScratchBlocks.Colours.looks; - - const motionColors = ScratchBlocks.Colours.motion; - - const sensingColors = ScratchBlocks.Colours.sensing; - - const controlColors = ScratchBlocks.Colours.control; - - const eventColors = ScratchBlocks.Colours.event; - ScratchBlocks.Blocks.sound_sounds_menu.init = function () { - const json = jsonForMenuBlock('SOUND_MENU', soundsMenu, soundColors, []); + const json = jsonForMenuBlock("SOUND_MENU", soundsMenu, "sounds", []); this.jsonInit(json); + this.getField("SOUND_MENU").setValidator((newValue) => { + if (newValue === "SOUND_RECORD") { + ScratchBlocks.recordSoundCallback(); + return null; + } + return newValue; + }); }; ScratchBlocks.Blocks.looks_costume.init = function () { - const json = jsonForMenuBlock('COSTUME', costumesMenu, looksColors, []); + const json = jsonForMenuBlock("COSTUME", costumesMenu, "looks", []); this.jsonInit(json); }; ScratchBlocks.Blocks.looks_backdrops.init = function () { - const json = jsonForMenuBlock('BACKDROP', backdropsMenu, looksColors, []); + const json = jsonForMenuBlock("BACKDROP", backdropsMenu, "looks", []); this.jsonInit(json); }; ScratchBlocks.Blocks.event_whenbackdropswitchesto.init = function () { const json = jsonForHatBlockMenu( ScratchBlocks.Msg.EVENT_WHENBACKDROPSWITCHESTO, - 'BACKDROP', backdropNamesMenu, eventColors, []); + "BACKDROP", + backdropNamesMenu, + "event", + [] + ); this.jsonInit(json); }; ScratchBlocks.Blocks.motion_pointtowards_menu.init = function () { - const mouse = ScratchBlocks.ScratchMsgs.translate('MOTION_POINTTOWARDS_POINTER', 'mouse-pointer'); - const json = jsonForMenuBlock('TOWARDS', spriteMenu, motionColors, [ - [mouse, '_mouse_'] + const mouse = ScratchBlocks.ScratchMsgs.translate( + "MOTION_POINTTOWARDS_POINTER", + "mouse-pointer" + ); + const json = jsonForMenuBlock("TOWARDS", spriteMenu, "motion", [ + [mouse, "_mouse_"], ]); this.jsonInit(json); }; ScratchBlocks.Blocks.motion_goto_menu.init = function () { - const random = ScratchBlocks.ScratchMsgs.translate('MOTION_GOTO_RANDOM', 'random position'); - const mouse = ScratchBlocks.ScratchMsgs.translate('MOTION_GOTO_POINTER', 'mouse-pointer'); - const json = jsonForMenuBlock('TO', spriteMenu, motionColors, [ - [random, '_random_'], - [mouse, '_mouse_'] + const random = ScratchBlocks.ScratchMsgs.translate( + "MOTION_GOTO_RANDOM", + "random position" + ); + const mouse = ScratchBlocks.ScratchMsgs.translate( + "MOTION_GOTO_POINTER", + "mouse-pointer" + ); + const json = jsonForMenuBlock("TO", spriteMenu, "motion", [ + [random, "_random_"], + [mouse, "_mouse_"], ]); this.jsonInit(json); }; ScratchBlocks.Blocks.motion_glideto_menu.init = function () { - const random = ScratchBlocks.ScratchMsgs.translate('MOTION_GLIDETO_RANDOM', 'random position'); - const mouse = ScratchBlocks.ScratchMsgs.translate('MOTION_GLIDETO_POINTER', 'mouse-pointer'); - const json = jsonForMenuBlock('TO', spriteMenu, motionColors, [ - [random, '_random_'], - [mouse, '_mouse_'] + const random = ScratchBlocks.ScratchMsgs.translate( + "MOTION_GLIDETO_RANDOM", + "random position" + ); + const mouse = ScratchBlocks.ScratchMsgs.translate( + "MOTION_GLIDETO_POINTER", + "mouse-pointer" + ); + const json = jsonForMenuBlock("TO", spriteMenu, "motion", [ + [random, "_random_"], + [mouse, "_mouse_"], ]); this.jsonInit(json); }; ScratchBlocks.Blocks.sensing_of_object_menu.init = function () { - const stage = ScratchBlocks.ScratchMsgs.translate('SENSING_OF_STAGE', 'Stage'); - const json = jsonForMenuBlock('OBJECT', spriteMenu, sensingColors, [ - [stage, '_stage_'] + const stage = ScratchBlocks.ScratchMsgs.translate( + "SENSING_OF_STAGE", + "Stage" + ); + const json = jsonForMenuBlock("OBJECT", spriteMenu, "sensing", [ + [stage, "_stage_"], ]); this.jsonInit(json); }; @@ -220,7 +268,7 @@ export default function (vm, useCatBlocks) { // Get the sensing_of block from vm. let defaultSensingOfBlock; const blocks = vm.runtime.flyoutBlocks._blocks; - Object.keys(blocks).forEach(id => { + Object.keys(blocks).forEach((id) => { const block = blocks[id]; if (id === blockType || (block && block.opcode === blockType)) { defaultSensingOfBlock = block; @@ -231,18 +279,18 @@ export default function (vm, useCatBlocks) { // Called every time it opens since it depends on the values in the other block input. const menuFn = function () { const stageOptions = [ - [ScratchBlocks.Msg.SENSING_OF_BACKDROPNUMBER, 'backdrop #'], - [ScratchBlocks.Msg.SENSING_OF_BACKDROPNAME, 'backdrop name'], - [ScratchBlocks.Msg.SENSING_OF_VOLUME, 'volume'] + [ScratchBlocks.Msg.SENSING_OF_BACKDROPNUMBER, "backdrop #"], + [ScratchBlocks.Msg.SENSING_OF_BACKDROPNAME, "backdrop name"], + [ScratchBlocks.Msg.SENSING_OF_VOLUME, "volume"], ]; const spriteOptions = [ - [ScratchBlocks.Msg.SENSING_OF_XPOSITION, 'x position'], - [ScratchBlocks.Msg.SENSING_OF_YPOSITION, 'y position'], - [ScratchBlocks.Msg.SENSING_OF_DIRECTION, 'direction'], - [ScratchBlocks.Msg.SENSING_OF_COSTUMENUMBER, 'costume #'], - [ScratchBlocks.Msg.SENSING_OF_COSTUMENAME, 'costume name'], - [ScratchBlocks.Msg.SENSING_OF_SIZE, 'size'], - [ScratchBlocks.Msg.SENSING_OF_VOLUME, 'volume'] + [ScratchBlocks.Msg.SENSING_OF_XPOSITION, "x position"], + [ScratchBlocks.Msg.SENSING_OF_YPOSITION, "y position"], + [ScratchBlocks.Msg.SENSING_OF_DIRECTION, "direction"], + [ScratchBlocks.Msg.SENSING_OF_COSTUMENUMBER, "costume #"], + [ScratchBlocks.Msg.SENSING_OF_COSTUMENAME, "costume name"], + [ScratchBlocks.Msg.SENSING_OF_SIZE, "size"], + [ScratchBlocks.Msg.SENSING_OF_VOLUME, "volume"], ]; if (vm.editingTarget) { let lookupBlocks = vm.editingTarget.blocks; @@ -250,31 +298,44 @@ export default function (vm, useCatBlocks) { // The block doesn't exist, but should be in the flyout. Look there. if (!sensingOfBlock) { - sensingOfBlock = vm.runtime.flyoutBlocks.getBlock(blockId) || defaultSensingOfBlock; + sensingOfBlock = + vm.runtime.flyoutBlocks.getBlock(blockId) || + defaultSensingOfBlock; // If we still don't have a block, just return an empty list . This happens during // scratch blocks construction. if (!sensingOfBlock) { - return [['', '']]; + return [["", ""]]; } // The block was in the flyout so look up future block info there. lookupBlocks = vm.runtime.flyoutBlocks; } const sort = function (options) { - options.sort(ScratchBlocks.scratchBlocksUtils.compareStrings); + options.sort( + ScratchBlocks.scratchBlocksUtils.compareStrings + ); }; // Get all the stage variables (no lists) so we can add them to menu when the stage is selected. - const stageVariableOptions = vm.runtime.getTargetForStage().getAllVariableNamesInScopeByType(''); + const stageVariableOptions = vm.runtime + .getTargetForStage() + .getAllVariableNamesInScopeByType(""); sort(stageVariableOptions); - const stageVariableMenuItems = stageVariableOptions.map(variable => [variable, variable]); - if (sensingOfBlock.inputs.OBJECT.shadow !== sensingOfBlock.inputs.OBJECT.block) { + const stageVariableMenuItems = stageVariableOptions.map( + (variable) => [variable, variable] + ); + if ( + sensingOfBlock.inputs.OBJECT.shadow !== + sensingOfBlock.inputs.OBJECT.block + ) { // There's a block dropped on top of the menu. It'd be nice to evaluate it and // return the correct list, but that is tricky. Scratch2 just returns stage options // so just do that here too. return stageOptions.concat(stageVariableMenuItems); } - const menuBlock = lookupBlocks.getBlock(sensingOfBlock.inputs.OBJECT.shadow); + const menuBlock = lookupBlocks.getBlock( + sensingOfBlock.inputs.OBJECT.shadow + ); const selectedItem = menuBlock.fields.OBJECT.value; - if (selectedItem === '_stage_') { + if (selectedItem === "_stage_") { return stageOptions.concat(stageVariableMenuItems); } // Get all the local variables (no lists) and add them to the menu. @@ -282,13 +343,16 @@ export default function (vm, useCatBlocks) { let spriteVariableOptions = []; // The target should exist, but there are ways for it not to (e.g. #4203). if (target) { - spriteVariableOptions = target.getAllVariableNamesInScopeByType('', true); + spriteVariableOptions = + target.getAllVariableNamesInScopeByType("", true); sort(spriteVariableOptions); } - const spriteVariableMenuItems = spriteVariableOptions.map(variable => [variable, variable]); + const spriteVariableMenuItems = spriteVariableOptions.map( + (variable) => [variable, variable] + ); return spriteOptions.concat(spriteVariableMenuItems); } - return [['', '']]; + return [["", ""]]; }; const json = jsonForSensingMenus(menuFn); @@ -296,34 +360,50 @@ export default function (vm, useCatBlocks) { }; ScratchBlocks.Blocks.sensing_distancetomenu.init = function () { - const mouse = ScratchBlocks.ScratchMsgs.translate('SENSING_DISTANCETO_POINTER', 'mouse-pointer'); - const json = jsonForMenuBlock('DISTANCETOMENU', spriteMenu, sensingColors, [ - [mouse, '_mouse_'] + const mouse = ScratchBlocks.ScratchMsgs.translate( + "SENSING_DISTANCETO_POINTER", + "mouse-pointer" + ); + const json = jsonForMenuBlock("DISTANCETOMENU", spriteMenu, "sensing", [ + [mouse, "_mouse_"], ]); this.jsonInit(json); }; ScratchBlocks.Blocks.sensing_touchingobjectmenu.init = function () { - const mouse = ScratchBlocks.ScratchMsgs.translate('SENSING_TOUCHINGOBJECT_POINTER', 'mouse-pointer'); - const edge = ScratchBlocks.ScratchMsgs.translate('SENSING_TOUCHINGOBJECT_EDGE', 'edge'); - const json = jsonForMenuBlock('TOUCHINGOBJECTMENU', spriteMenu, sensingColors, [ - [mouse, '_mouse_'], - [edge, '_edge_'] - ]); + const mouse = ScratchBlocks.ScratchMsgs.translate( + "SENSING_TOUCHINGOBJECT_POINTER", + "mouse-pointer" + ); + const edge = ScratchBlocks.ScratchMsgs.translate( + "SENSING_TOUCHINGOBJECT_EDGE", + "edge" + ); + const json = jsonForMenuBlock( + "TOUCHINGOBJECTMENU", + spriteMenu, + "sensing", + [ + [mouse, "_mouse_"], + [edge, "_edge_"], + ] + ); this.jsonInit(json); }; ScratchBlocks.Blocks.control_create_clone_of_menu.init = function () { - const json = jsonForMenuBlock('CLONE_OPTION', cloneMenu, controlColors, []); + const json = jsonForMenuBlock("CLONE_OPTION", cloneMenu, "control", []); this.jsonInit(json); }; - ScratchBlocks.VerticalFlyout.getCheckboxState = function (blockId) { + ScratchBlocks.CheckboxBubble.prototype.isChecked = function (blockId) { const monitoredBlock = vm.runtime.monitorBlocks._blocks[blockId]; return monitoredBlock ? monitoredBlock.isMonitored : false; }; - ScratchBlocks.FlyoutExtensionCategoryHeader.getExtensionState = function (extensionId) { + ScratchBlocks.StatusIndicatorLabel.prototype.getExtensionState = function ( + extensionId + ) { if (vm.getPeripheralIsConnected(extensionId)) { return ScratchBlocks.StatusButtonState.READY; } @@ -331,19 +411,19 @@ export default function (vm, useCatBlocks) { }; ScratchBlocks.FieldNote.playNote_ = function (noteNum, extensionId) { - vm.runtime.emit('PLAY_NOTE', noteNum, extensionId); + vm.runtime.emit("PLAY_NOTE", noteNum, extensionId); }; // Use a collator's compare instead of localeCompare which internally // creates a collator. Using this is a lot faster in browsers that create a // collator for every localeCompare call. const collator = new Intl.Collator([], { - sensitivity: 'base', - numeric: true + sensitivity: "base", + numeric: true, }); - ScratchBlocks.scratchBlocksUtils.compareStrings = function (str1, str2) { - return collator.compare(str1, str2); - }; + // ScratchBlocks.scratchBlocksUtils.compareStrings = function (str1, str2) { + // return collator.compare(str1, str2); + // }; // Blocks wants to know if 3D CSS transforms are supported. The cross // section of browsers Scratch supports and browsers that support 3D CSS diff --git a/src/lib/define-dynamic-block.js b/src/lib/define-dynamic-block.js index 5f6222ab0d6..248d69ae870 100644 --- a/src/lib/define-dynamic-block.js +++ b/src/lib/define-dynamic-block.js @@ -1,6 +1,6 @@ // TODO: access `BlockType` and `ArgumentType` without reaching into VM // Should we move these into a new extension support module or something? -import {ArgumentType, BlockType} from 'scratch-vm'; +import { ArgumentType, BlockType } from "scratch-vm"; /** * Define a block using extension info which has the ability to dynamically determine (and update) its layout. @@ -13,74 +13,72 @@ import {ArgumentType, BlockType} from 'scratch-vm'; * @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) => ({ +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 + style: categoryInfo.id, }; // 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']; + 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 = '{}'; + 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); + const container = document.createElement("mutation"); + container.setAttribute("blockInfo", this.blockInfoText); return container; }, domToMutation: function (xmlElement) { - const blockInfoText = xmlElement.getAttribute('blockInfo'); + const blockInfoText = xmlElement.getAttribute("blockInfo"); if (!blockInfoText) return; if (!this.needsBlockInfoUpdate) { - throw new Error('Attempted to update block info twice'); + 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); + 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; } // Layout block arguments @@ -88,20 +86,27 @@ const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extend 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; + 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}`; } - return `%${++argCount}`; - }); + ); this.interpolate_(scratchBlocksStyleText, args); - } + }, }); export default defineDynamicBlock; diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 8d356e442d9..7d6acc24aba 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -1,5 +1,5 @@ -import ScratchBlocks from 'scratch-blocks'; -import {defaultColors} from './themes'; +import { ScratchBlocks } from "scratch-blocks"; +import { defaultColors } from "./themes"; const categorySeparator = ''; @@ -8,15 +8,25 @@ const blockSeparator = ''; // At default scale, about 28px /* eslint-disable no-unused-vars */ const motion = function (isInitialSetup, isStage, targetId, colors) { const stageSelected = ScratchBlocks.ScratchMsgs.translate( - 'MOTION_STAGE_SELECTED', - 'Stage selected: no motion blocks' + "MOTION_STAGE_SELECTED", + "Stage selected: no motion blocks" ); - // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. + // Note: the category's secondaryColour matches up with the blocks' tertiary + // color, both used for border color. Since Blockly uses the UK spelling of + // "colour", certain attributes are named accordingly. return ` - - ${isStage ? ` + + ${ + isStage + ? ` - ` : ` + ` + : ` @@ -135,31 +145,52 @@ const motion = function (isInitialSetup, isStage, targetId, colors) { ${blockSeparator} - `} + ` + } ${categorySeparator} `; }; const xmlEscape = function (unsafe) { - return unsafe.replace(/[<>&'"]/g, c => { + return unsafe.replace(/[<>&'"]/g, (c) => { switch (c) { - case '<': return '<'; - case '>': return '>'; - case '&': return '&'; - case '\'': return '''; - case '"': return '"'; + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + case "'": + return "'"; + case '"': + return """; } }); }; -const looks = function (isInitialSetup, isStage, targetId, costumeName, backdropName, colors) { - const hello = ScratchBlocks.ScratchMsgs.translate('LOOKS_HELLO', 'Hello!'); - const hmm = ScratchBlocks.ScratchMsgs.translate('LOOKS_HMM', 'Hmm...'); +const looks = function ( + isInitialSetup, + isStage, + targetId, + costumeName, + backdropName, + colors +) { + const hello = ScratchBlocks.ScratchMsgs.translate("LOOKS_HELLO", "Hello!"); + const hmm = ScratchBlocks.ScratchMsgs.translate("LOOKS_HMM", "Hmm..."); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - - ${isStage ? '' : ` + + ${ + isStage + ? "" + : ` @@ -199,8 +230,11 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop ${blockSeparator} - `} - ${isStage ? ` + ` + } + ${ + isStage + ? ` @@ -216,7 +250,8 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop - ` : ` + ` + : ` @@ -248,7 +283,8 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop - `} + ` + } ${blockSeparator} @@ -266,7 +302,10 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop ${blockSeparator} - ${isStage ? '' : ` + ${ + isStage + ? "" + : ` ${blockSeparator} @@ -278,14 +317,19 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop - `} - ${isStage ? ` + ` + } + ${ + isStage + ? ` - ` : ` + ` + : ` - `} + ` + } ${categorySeparator} `; @@ -294,7 +338,12 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -350,15 +399,24 @@ const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { const events = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + - ${isStage ? ` + ${ + isStage + ? ` - ` : ` + ` + : ` - `} + ` + } ${blockSeparator} @@ -391,10 +449,13 @@ const control = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` + name="${ScratchBlocks.ScratchMsgs.translate( + "CATEGORY_CONTROL", + "Control" + )}" + toolboxitemid="control" + colour="${colors.colourPrimary}" + secondaryColour="${colors.colourTertiary}"> @@ -419,13 +480,16 @@ const control = function (isInitialSetup, isStage, targetId, colors) { ${blockSeparator} ${blockSeparator} - ${isStage ? ` + ${ + isStage + ? ` - ` : ` + ` + : ` @@ -433,22 +497,32 @@ const control = function (isInitialSetup, isStage, targetId, colors) { - `} + ` + } ${categorySeparator} `; }; const sensing = function (isInitialSetup, isStage, targetId, colors) { - const name = ScratchBlocks.ScratchMsgs.translate('SENSING_ASK_TEXT', 'What\'s your name?'); + const name = ScratchBlocks.ScratchMsgs.translate( + "SENSING_ASK_TEXT", + "What's your name?" + ); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - ${isStage ? '' : ` + name="${ScratchBlocks.ScratchMsgs.translate( + "CATEGORY_SENSING", + "Sensing" + )}" + toolboxitemid="sensing" + colour="${colors.colourPrimary}" + secondaryColour="${colors.colourTertiary}"> + ${ + isStage + ? "" + : ` @@ -473,8 +547,12 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { ${blockSeparator} - `} - ${isInitialSetup ? '' : ` + ` + } + ${ + isInitialSetup + ? "" + : ` @@ -482,7 +560,8 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { - `} + ` + } ${blockSeparator} @@ -493,11 +572,15 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { - ${isStage ? '' : ` + ${ + isStage + ? "" + : ` ${blockSeparator} ''+ ${blockSeparator} - `} + ` + } ${blockSeparator} ${blockSeparator} @@ -520,16 +603,28 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { }; const operators = function (isInitialSetup, isStage, targetId, colors) { - const apple = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_APPLE', 'apple'); - const banana = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_BANANA', 'banana'); - const letter = ScratchBlocks.ScratchMsgs.translate('OPERATORS_LETTEROF_APPLE', 'a'); + const apple = ScratchBlocks.ScratchMsgs.translate( + "OPERATORS_JOIN_APPLE", + "apple" + ); + const banana = ScratchBlocks.ScratchMsgs.translate( + "OPERATORS_JOIN_BANANA", + "banana" + ); + const letter = ScratchBlocks.ScratchMsgs.translate( + "OPERATORS_LETTEROF_APPLE", + "a" + ); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` + name="${ScratchBlocks.ScratchMsgs.translate( + "CATEGORY_OPERATORS", + "Operators" + )}" + toolboxitemid="operators" + colour="${colors.colourPrimary}" + secondaryColour="${colors.colourTertiary}"> @@ -633,7 +728,10 @@ const operators = function (isInitialSetup, isStage, targetId, colors) { ${blockSeparator} - ${isInitialSetup ? '' : ` + ${ + isInitialSetup + ? "" + : ` @@ -677,7 +775,8 @@ const operators = function (isInitialSetup, isStage, targetId, colors) { - `} + ` + } ${blockSeparator} @@ -715,10 +814,13 @@ const variables = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` `; @@ -728,10 +830,13 @@ const myBlocks = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` `; @@ -739,7 +844,7 @@ const myBlocks = function (isInitialSetup, isStage, targetId, colors) { /* eslint-enable no-unused-vars */ const xmlOpen = ''; -const xmlClose = ''; +const xmlClose = ""; /** * @param {!boolean} isInitialSetup - Whether the toolbox is for initial setup. If the mode is "initial setup", @@ -757,8 +862,16 @@ const xmlClose = ''; * @param {?object} colors - The colors for the theme. * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox. */ -const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categoriesXML = [], - costumeName = '', backdropName = '', soundName = '', colors = defaultColors) { +const makeToolboxXML = function ( + isInitialSetup, + isStage = true, + targetId, + categoriesXML = [], + costumeName = "", + backdropName = "", + soundName = "", + colors = defaultColors +) { isStage = isInitialSetup || isStage; const gap = [categorySeparator]; @@ -767,8 +880,10 @@ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categ soundName = xmlEscape(soundName); categoriesXML = categoriesXML.slice(); - const moveCategory = categoryId => { - const index = categoriesXML.findIndex(categoryInfo => categoryInfo.id === categoryId); + 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); @@ -776,28 +891,60 @@ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categ } // return `undefined` }; - const motionXML = moveCategory('motion') || motion(isInitialSetup, isStage, targetId, colors.motion); - const looksXML = moveCategory('looks') || - looks(isInitialSetup, isStage, targetId, costumeName, backdropName, colors.looks); - const soundXML = moveCategory('sound') || sound(isInitialSetup, isStage, targetId, soundName, colors.sounds); - const eventsXML = moveCategory('event') || events(isInitialSetup, isStage, targetId, colors.event); - const controlXML = moveCategory('control') || control(isInitialSetup, isStage, targetId, colors.control); - const sensingXML = moveCategory('sensing') || sensing(isInitialSetup, isStage, targetId, colors.sensing); - const operatorsXML = moveCategory('operators') || operators(isInitialSetup, isStage, targetId, colors.operators); - const variablesXML = moveCategory('data') || variables(isInitialSetup, isStage, targetId, colors.data); - const myBlocksXML = moveCategory('procedures') || myBlocks(isInitialSetup, isStage, targetId, colors.more); + const motionXML = + moveCategory("motion") || + motion(isInitialSetup, isStage, targetId, colors.motion); + const looksXML = + moveCategory("looks") || + looks( + isInitialSetup, + isStage, + targetId, + costumeName, + backdropName, + colors.looks + ); + const soundXML = + moveCategory("sound") || + sound(isInitialSetup, isStage, targetId, soundName, colors.sounds); + const eventsXML = + moveCategory("event") || + events(isInitialSetup, isStage, targetId, colors.event); + const controlXML = + moveCategory("control") || + control(isInitialSetup, isStage, targetId, colors.control); + const sensingXML = + moveCategory("sensing") || + sensing(isInitialSetup, isStage, targetId, colors.sensing); + const operatorsXML = + moveCategory("operators") || + operators(isInitialSetup, isStage, targetId, colors.operators); + const variablesXML = + moveCategory("data") || + variables(isInitialSetup, isStage, targetId, colors.data); + const myBlocksXML = + moveCategory("procedures") || + myBlocks(isInitialSetup, isStage, targetId, colors.more); const everything = [ xmlOpen, - motionXML, gap, - looksXML, gap, - soundXML, gap, - eventsXML, gap, - controlXML, gap, - sensingXML, gap, - operatorsXML, gap, - variablesXML, gap, - myBlocksXML + motionXML, + gap, + looksXML, + gap, + soundXML, + gap, + eventsXML, + gap, + controlXML, + gap, + sensingXML, + gap, + operatorsXML, + gap, + variablesXML, + gap, + myBlocksXML, ]; for (const extensionCategory of categoriesXML) { @@ -805,7 +952,7 @@ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categ } everything.push(xmlClose); - return everything.join('\n'); + return everything.join("\n"); }; export default makeToolboxXML; diff --git a/src/lib/themes/blockHelpers.js b/src/lib/themes/blockHelpers.js index b201241eebb..696c0b57855 100644 --- a/src/lib/themes/blockHelpers.js +++ b/src/lib/themes/blockHelpers.js @@ -1,19 +1,19 @@ -import {DEFAULT_THEME, getColorsForTheme, themeMap} from '.'; +import { DEFAULT_THEME, getColorsForTheme, themeMap } from "."; -const getBlockIconURI = extensionIcons => { +const getBlockIconURI = (extensionIcons) => { if (!extensionIcons) return null; return extensionIcons.blockIconURI || extensionIcons.menuIconURI; }; -const getCategoryIconURI = extensionIcons => { +const getCategoryIconURI = (extensionIcons) => { if (!extensionIcons) return null; return extensionIcons.menuIconURI || extensionIcons.blockIconURI; }; // scratch-blocks colours has a pen property that scratch-gui uses for all extensions -const getExtensionColors = theme => getColorsForTheme(theme).pen; +const getExtensionColors = (theme) => getColorsForTheme(theme).pen; /** * Applies extension color theme to categories. @@ -33,32 +33,52 @@ const injectExtensionCategoryTheme = (dynamicBlockXML, theme) => { const parser = new DOMParser(); const serializer = new XMLSerializer(); - return dynamicBlockXML.map(extension => { - const dom = parser.parseFromString(extension.xml, 'text/xml'); + return dynamicBlockXML.map((extension) => { + const dom = parser.parseFromString(extension.xml, "text/xml"); - dom.documentElement.setAttribute('colour', extensionColors.primary); + // This element is deserialized by Blockly, which uses the UK spelling + // of "colour". + dom.documentElement.setAttribute( + "colour", + extensionColors.colourPrimary + ); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. - dom.documentElement.setAttribute('secondaryColour', extensionColors.tertiary); - - const categoryIconURI = getCategoryIconURI(extensionIcons[extension.id]); + dom.documentElement.setAttribute( + "secondaryColour", + extensionColors.colourTertiary + ); + + const categoryIconURI = getCategoryIconURI( + extensionIcons[extension.id] + ); if (categoryIconURI) { - dom.documentElement.setAttribute('iconURI', categoryIconURI); + dom.documentElement.setAttribute("iconURI", categoryIconURI); } return { ...extension, - xml: serializer.serializeToString(dom) + xml: serializer.serializeToString(dom), }; }); }; -const injectBlockIcons = (blockInfoJson, theme) => { +const injectExtensionBlockIcons = (blockInfoJson, theme) => { + // Don't do any manipulation for the default theme + if (theme === DEFAULT_THEME) return blockInfoJson; + // Block icons are the first element of `args0` - if (!blockInfoJson.args0 || blockInfoJson.args0.length < 1 || - blockInfoJson.args0[0].type !== 'field_image') return blockInfoJson; + if ( + !blockInfoJson.args0 || + blockInfoJson.args0.length < 1 || + blockInfoJson.args0[0].type !== "field_image" + ) + return blockInfoJson; const extensionIcons = themeMap[theme].extensions; - const extensionId = blockInfoJson.type.substring(0, blockInfoJson.type.indexOf('_')); + const extensionId = blockInfoJson.type.substring( + 0, + blockInfoJson.type.indexOf("_") + ); const blockIconURI = getBlockIconURI(extensionIcons[extensionId]); if (!blockIconURI) return blockInfoJson; @@ -70,35 +90,14 @@ const injectBlockIcons = (blockInfoJson, theme) => { return { ...value, - src: blockIconURI + src: blockIconURI, }; - }) - }; -}; - -/** - * Applies extension color theme to static block json. - * No changes are applied if called with the default theme, allowing extensions to provide their own colors. - * @param {object} blockInfoJson - Static block json - * @param {string} theme - Theme name - * @returns {object} Block info json with updated colors. The original blockInfoJson is not modified. - */ -const injectExtensionBlockTheme = (blockInfoJson, theme) => { - // Don't do any manipulation for the default theme - if (theme === DEFAULT_THEME) return blockInfoJson; - - const extensionColors = getExtensionColors(theme); - - return { - ...injectBlockIcons(blockInfoJson, theme), - colour: extensionColors.primary, - colourSecondary: extensionColors.secondary, - colourTertiary: extensionColors.tertiary, - colourQuaternary: extensionColors.quaternary + }), }; }; export { - injectExtensionBlockTheme, - injectExtensionCategoryTheme + injectExtensionBlockIcons, + injectExtensionCategoryTheme, + getExtensionColors, }; diff --git a/src/lib/themes/default/index.js b/src/lib/themes/default/index.js index 3a754dca37f..7047a1c7fd6 100644 --- a/src/lib/themes/default/index.js +++ b/src/lib/themes/default/index.js @@ -1,105 +1,105 @@ +// This object is passed directly to Blockly, hence the colour* fields need to +// be named exactly as they are, including the UK spelling of "colour". const blockColors = { motion: { - primary: '#4C97FF', - secondary: '#4280D7', - tertiary: '#3373CC', - quaternary: '#3373CC' + colourPrimary: "#4C97FF", + colourSecondary: "#4280D7", + colourTertiary: "#3373CC", + colourQuaternary: "#3373CC", }, looks: { - primary: '#9966FF', - secondary: '#855CD6', - tertiary: '#774DCB', - quaternary: '#774DCB' + colourPrimary: "#9966FF", + colourSecondary: "#855CD6", + colourTertiary: "#774DCB", + colourQuaternary: "#774DCB", }, sounds: { - primary: '#CF63CF', - secondary: '#C94FC9', - tertiary: '#BD42BD', - quaternary: '#BD42BD' + colourPrimary: "#CF63CF", + colourSecondary: "#C94FC9", + colourTertiary: "#BD42BD", + colourQuaternary: "#BD42BD", }, control: { - primary: '#FFAB19', - secondary: '#EC9C13', - tertiary: '#CF8B17', - quaternary: '#CF8B17' + colourPrimary: "#FFAB19", + colourSecondary: "#EC9C13", + colourTertiary: "#CF8B17", + colourQuaternary: "#CF8B17", }, event: { - primary: '#FFBF00', - secondary: '#E6AC00', - tertiary: '#CC9900', - quaternary: '#CC9900' + colourPrimary: "#FFBF00", + colourSecondary: "#E6AC00", + colourTertiary: "#CC9900", + colourQuaternary: "#CC9900", }, sensing: { - primary: '#5CB1D6', - secondary: '#47A8D1', - tertiary: '#2E8EB8', - quaternary: '#2E8EB8' + colourPrimary: "#5CB1D6", + colourSecondary: "#47A8D1", + colourTertiary: "#2E8EB8", + colourQuaternary: "#2E8EB8", }, pen: { - primary: '#0fBD8C', - secondary: '#0DA57A', - tertiary: '#0B8E69', - quaternary: '#0B8E69' + colourPrimary: "#0fBD8C", + colourSecondary: "#0DA57A", + colourTertiary: "#0B8E69", + colourQuaternary: "#0B8E69", }, operators: { - primary: '#59C059', - secondary: '#46B946', - tertiary: '#389438', - quaternary: '#389438' + colourPrimary: "#59C059", + colourSecondary: "#46B946", + colourTertiary: "#389438", + colourQuaternary: "#389438", }, data: { - primary: '#FF8C1A', - secondary: '#FF8000', - tertiary: '#DB6E00', - quaternary: '#DB6E00' + colourPrimary: "#FF8C1A", + colourSecondary: "#FF8000", + colourTertiary: "#DB6E00", + colourQuaternary: "#DB6E00", }, // This is not a new category, but rather for differentiation // between lists and scalar variables. data_lists: { - primary: '#FF661A', - secondary: '#FF5500', - tertiary: '#E64D00', - quaternary: '#E64D00' + colourPrimary: "#FF661A", + colourSecondary: "#FF5500", + colourTertiary: "#E64D00", + colourQuaternary: "#E64D00", }, more: { - primary: '#FF6680', - secondary: '#FF4D6A', - tertiary: '#FF3355', - quaternary: '#FF3355' + colourPrimary: "#FF6680", + colourSecondary: "#FF4D6A", + colourTertiary: "#FF3355", + colourQuaternary: "#FF3355", }, - text: '#FFFFFF', - workspace: '#F9F9F9', - toolboxHover: '#4C97FF', - toolboxSelected: '#E9EEF2', - toolboxText: '#575E75', - toolbox: '#FFFFFF', - flyout: '#F9F9F9', - scrollbar: '#CECDCE', - scrollbarHover: '#CECDCE', - textField: '#FFFFFF', - textFieldText: '#575E75', - insertionMarker: '#000000', + text: "#FFFFFF", + workspace: "#F9F9F9", + toolboxHover: "#4C97FF", + toolboxSelected: "#E9EEF2", + toolboxText: "#575E75", + toolbox: "#FFFFFF", + flyout: "#F9F9F9", + scrollbar: "#CECDCE", + scrollbarHover: "#CECDCE", + textField: "#FFFFFF", + textFieldText: "#575E75", + insertionMarker: "#000000", insertionMarkerOpacity: 0.2, dragShadowOpacity: 0.6, - stackGlow: '#FFF200', + stackGlow: "#FFF200", stackGlowSize: 4, stackGlowOpacity: 1, - replacementGlow: '#FFFFFF', + replacementGlow: "#FFFFFF", replacementGlowSize: 2, replacementGlowOpacity: 1, - colourPickerStroke: '#FFFFFF', + colourPickerStroke: "#FFFFFF", // CSS colours: support RGBA - fieldShadow: 'rgba(255, 255, 255, 0.3)', - dropDownShadow: 'rgba(0, 0, 0, .3)', - numPadBackground: '#547AB2', - numPadBorder: '#435F91', - numPadActiveBackground: '#435F91', - numPadText: 'white', // Do not use hex here, it cannot be inlined with data-uri SVG - valueReportBackground: '#FFFFFF', - valueReportBorder: '#AAAAAA', - menuHover: 'rgba(0, 0, 0, 0.2)' + fieldShadow: "rgba(255, 255, 255, 0.3)", + dropDownShadow: "rgba(0, 0, 0, .3)", + numPadBackground: "#547AB2", + numPadBorder: "#435F91", + numPadActiveBackground: "#435F91", + numPadText: "white", // Do not use hex here, it cannot be inlined with data-uri SVG + valueReportBackground: "#FFFFFF", + valueReportBorder: "#AAAAAA", + menuHover: "rgba(0, 0, 0, 0.2)", }; -export { - blockColors -}; +export { blockColors }; diff --git a/src/lib/themes/high-contrast/index.js b/src/lib/themes/high-contrast/index.js index 8a5dd941559..853ff7947ce 100644 --- a/src/lib/themes/high-contrast/index.js +++ b/src/lib/themes/high-contrast/index.js @@ -1,110 +1,108 @@ -import musicIcon from './extensions/musicIcon.svg'; -import penIcon from './extensions/penIcon.svg'; -import text2speechIcon from './extensions/text2speechIcon.svg'; -import translateIcon from './extensions/translateIcon.svg'; -import videoSensingIcon from './extensions/videoSensingIcon.svg'; +import musicIcon from "./extensions/musicIcon.svg"; +import penIcon from "./extensions/penIcon.svg"; +import text2speechIcon from "./extensions/text2speechIcon.svg"; +import translateIcon from "./extensions/translateIcon.svg"; +import videoSensingIcon from "./extensions/videoSensingIcon.svg"; +// This object is passed directly to Blockly, hence the colour* fields need to +// be named exactly as they are, including the UK spelling of "colour". const blockColors = { motion: { - primary: '#80B5FF', - secondary: '#B3D2FF', - tertiary: '#3373CC', - quaternary: '#CCE1FF' + colourPrimary: "#80B5FF", + colourSecondary: "#B3D2FF", + colourTertiary: "#3373CC", + colourQuaternary: "#CCE1FF", }, looks: { - primary: '#CCB3FF', - secondary: '#DDCCFF', - tertiary: '#774DCB', - quaternary: '#EEE5FF' + colourPrimary: "#CCB3FF", + colourSecondary: "#DDCCFF", + colourTertiary: "#774DCB", + colourQuaternary: "#EEE5FF", }, sounds: { - primary: '#E19DE1', - secondary: '#FFB3FF', - tertiary: '#BD42BD', - quaternary: '#FFCCFF' - + colourPrimary: "#E19DE1", + colourSecondary: "#FFB3FF", + colourTertiary: "#BD42BD", + colourQuaternary: "#FFCCFF", }, control: { - primary: '#FFBE4C', - secondary: '#FFDA99', - tertiary: '#CF8B17', - quaternary: '#FFE3B3' + colourPrimary: "#FFBE4C", + colourSecondary: "#FFDA99", + colourTertiary: "#CF8B17", + colourQuaternary: "#FFE3B3", }, event: { - primary: '#FFD966', - secondary: '#FFECB3', - tertiary: '#CC9900', - quaternary: '#FFF2CC' + colourPrimary: "#FFD966", + colourSecondary: "#FFECB3", + colourTertiary: "#CC9900", + colourQuaternary: "#FFF2CC", }, sensing: { - primary: '#85C4E0', - secondary: '#AED8EA', - tertiary: '#2E8EB8', - quaternary: '#C2E2F0' + colourPrimary: "#85C4E0", + colourSecondary: "#AED8EA", + colourTertiary: "#2E8EB8", + colourQuaternary: "#C2E2F0", }, pen: { - primary: '#13ECAF', - secondary: '#75F0CD', - tertiary: '#0B8E69', - quaternary: '#A3F5DE' + colourPrimary: "#13ECAF", + colourSecondary: "#75F0CD", + colourTertiary: "#0B8E69", + colourQuaternary: "#A3F5DE", }, operators: { - primary: '#7ECE7E', - secondary: '#B5E3B5', - tertiary: '#389438', - quaternary: '#DAF1DA' + colourPrimary: "#7ECE7E", + colourSecondary: "#B5E3B5", + colourTertiary: "#389438", + colourQuaternary: "#DAF1DA", }, data: { - primary: '#FFA54C', - secondary: '#FFCC99', - tertiary: '#DB6E00', - quaternary: '#FFE5CC' + colourPrimary: "#FFA54C", + colourSecondary: "#FFCC99", + colourTertiary: "#DB6E00", + colourQuaternary: "#FFE5CC", }, // This is not a new category, but rather for differentiation // between lists and scalar variables. data_lists: { - primary: '#FF9966', - secondary: '#FFCAB0', // I don't think this is used, b/c we don't have any droppable fields in list blocks - tertiary: '#E64D00', - quaternary: '#FFDDCC' + colourPrimary: "#FF9966", + colourSecondary: "#FFCAB0", // I don't think this is used, b/c we don't have any droppable fields in list blocks + colourTertiary: "#E64D00", + colourQuaternary: "#FFDDCC", }, more: { - primary: '#FF99AA', - secondary: '#FFCCD5', - tertiary: '#FF3355', - quaternary: '#FFE5EA' - }, - text: '#000000', - textFieldText: '#000000', // Text inside of inputs e.g. 90 in [point in direction (90)] - toolboxText: '#000000', // Toolbox text, color picker text (used to be #575E75) + colourPrimary: "#FF99AA", + colourSecondary: "#FFCCD5", + colourTertiary: "#FF3355", + colourQuaternary: "#FFE5EA", + }, + text: "#000000", + textFieldText: "#000000", // Text inside of inputs e.g. 90 in [point in direction (90)] + toolboxText: "#000000", // Toolbox text, color picker text (used to be #575E75) // The color that the category menu label (e.g. 'motion', 'looks', etc.) changes to on hover - toolboxHover: '#3373CC', - insertionMarker: '#000000', + toolboxHover: "#3373CC", + insertionMarker: "#000000", insertionMarkerOpacity: 0.2, - fieldShadow: 'rgba(255, 255, 255, 0.3)', + fieldShadow: "rgba(255, 255, 255, 0.3)", dragShadowOpacity: 0.6, - menuHover: 'rgba(255, 255, 255, 0.3)' + menuHover: "rgba(255, 255, 255, 0.3)", }; const extensions = { music: { - blockIconURI: musicIcon + blockIconURI: musicIcon, }, pen: { - blockIconURI: penIcon + blockIconURI: penIcon, }, text2speech: { - blockIconURI: text2speechIcon + blockIconURI: text2speechIcon, }, translate: { - blockIconURI: translateIcon + blockIconURI: translateIcon, }, videoSensing: { - blockIconURI: videoSensingIcon - } + blockIconURI: videoSensingIcon, + }, }; -export { - blockColors, - extensions -}; +export { blockColors, extensions }; diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index b6207a5b7dd..f059c030baa 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -85,7 +85,7 @@ const vmListenerHOC = function (WrappedComponent) { } handleKeyDown (e) { // Don't capture keys intended for Blockly inputs. - if (e.target !== document && e.target !== document.body) return; + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; const key = (!e.key || e.key === 'Dead') ? e.keyCode : e.key; this.props.vm.postIOData('keyboard', {