Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const CustomProcedures = props => {
<Modal
className={styles.modalContent}
contentLabel={intl.formatMessage(messages.myblockModalTitle)}
id="customProceduresModal"
onRequestClose={props.onCancel}
>
<Box
Expand Down
78 changes: 76 additions & 2 deletions packages/scratch-gui/src/containers/blocks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,30 @@ class Blocks extends React.Component {
if (!this.props.paletteVisible) {
this._applyPaletteVisibility(false);
}

// === Smalruby: scroll the flyout to a newly added extension category.
// `handleExtensionAdded` flags the extension id; once the toolbox has
// been rebuilt (now), look the category up and scroll to it.
const pendingId = this._pendingScrollToCategoryId;
if (pendingId) {
this._pendingScrollToCategoryId = null;
const toolbox = this.workspace?.getToolbox?.();
const items = toolbox?.getToolboxItems?.() || [];
const item = items.find(it => it.toolboxItemDef_?.id === pendingId);
const name = item?.toolboxItemDef_?.name || item?.name_;
if (item && name) {
if (typeof toolbox.selectCategoryByName === 'function') {
toolbox.selectCategoryByName(name);
}
// ContinuousToolbox.selectCategoryByName updates the toolbox
// selection but does not scroll the flyout. The continuous
// flyout exposes scrollToCategory(item) for that.
const pendingFlyout = this.workspace.getFlyout?.();
if (pendingFlyout && typeof pendingFlyout.scrollToCategory === 'function') {
pendingFlyout.scrollToCategory(item);
}
}
}
}

withToolboxUpdates (fn) {
Expand Down Expand Up @@ -1006,6 +1030,18 @@ class Blocks extends React.Component {
if (toolboxXML) {
this.props.updateToolboxState(toolboxXML);
}

// After the toolbox finishes its async rebuild, scroll the flyout to
// the newly added extension category. In scratch-blocks v1 the flyout
// automatically focused the just-added category, but the v2
// continuous toolbox does not do this on its own — the flyout stays
// scrolled to wherever it was, so the user never sees the new blocks.
//
// `updateToolboxState` only dispatches the Redux update; the actual
// `workspace.updateToolbox(...)` rebuild happens later from
// `componentDidUpdate` -> `requestToolboxUpdate` (setTimeout 0). Mark
// the pending category and let the post-rebuild path scroll to it.
this._pendingScrollToCategoryId = categoryInfo.id;
}
handleBlocksInfoUpdate (categoryInfo) {
// @todo Later we should replace this to avoid all the warnings from redefining blocks.
Expand Down Expand Up @@ -1066,8 +1102,46 @@ class Blocks extends React.Component {
handleCustomProceduresClose (data) {
this.props.onRequestCloseCustomProcedures(data);
const ws = this.workspace;
ws.refreshToolboxSelection_();
ws.toolbox_.scrollToCategoryById('myBlocks');
// scratch-blocks v2 renamed `refreshToolboxSelection_` → `refreshToolboxSelection`.
if (typeof ws.refreshToolboxSelection === 'function') {
ws.refreshToolboxSelection();
}
// The new `procedures_definition` block has been created on the
// workspace by `createProcedureCallbackFactory`. The "My Blocks"
// toolbox category is dynamic (`custom="PROCEDURE"`), and its
// `procedures_call` flyout entry only appears after the toolbox is
// rebuilt. In scratch-blocks v1 `ContinuousToolbox.refreshSelection`
// rebuilt the flyout on every BLOCK_CREATE; v2 made that a no-op,
// so we must explicitly force a rebuild here. Defer until after the
// pending block-create renders flush, otherwise `forceRerender`
// sees the workspace mid-update.
const toolbox = ws.getToolbox?.();
const myBlocksId = 'myBlocks';
const scrollMyBlocks = () => {
const items = toolbox?.getToolboxItems?.() || [];
const item = items.find(it => it.toolboxItemDef_?.toolboxitemid === myBlocksId);
const name = item?.toolboxItemDef_?.name || item?.name_;
if (name && typeof toolbox.selectCategoryByName === 'function') {
toolbox.selectCategoryByName(name);
}
const flyout = ws.getFlyout?.();
if (item && flyout && typeof flyout.scrollToCategory === 'function') {
flyout.scrollToCategory(item);
}
};
if (toolbox && typeof toolbox.forceRerender === 'function') {
setTimeout(() => {
try {
toolbox.forceRerender();
} catch (err) {
// forceRerender can throw if dispose paths race; the
// surrounding scroll still works without a rebuild.
}
scrollMyBlocks();
}, 0);
} else {
scrollMyBlocks();
}
}
handleDrop (dragInfo) {
fetch(dragInfo.payload.bodyUrl)
Expand Down
22 changes: 20 additions & 2 deletions packages/scratch-gui/src/containers/custom-procedures.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import React from 'react';
import CustomProceduresComponent from '../components/custom-procedures/custom-procedures.jsx';
import * as ScratchBlocks from 'scratch-blocks';
import {connect} from 'react-redux';
import {DEFAULT_MODE, getColorsForMode} from '../lib/settings/color-mode';
import {CAT_BLOCKS_THEME} from '../lib/settings/theme';

class CustomProcedures extends React.Component {
constructor (props) {
Expand Down Expand Up @@ -46,10 +48,22 @@ class CustomProcedures extends React.Component {
setBlocks (blocksRef) {
if (!blocksRef) return;
this.blocks = blocksRef;
// scratch-blocks v2 renders blocks with the default (black) theme
// unless an explicit theme is supplied to `inject`. The main editor
// workspace passes `theme` + `scratchTheme`; we mirror that here so
// the procedure declaration block in the modal uses the same colours
// as the rest of the editor.
const workspaceConfig = defaultsDeep({},
CustomProcedures.defaultOptions,
this.props.options,
{rtl: this.props.isRtl}
{
rtl: this.props.isRtl,
theme: new ScratchBlocks.Theme(
this.props.colorMode || DEFAULT_MODE,
getColorsForMode(this.props.colorMode || DEFAULT_MODE),
),
scratchTheme: this.props.useCatBlocks ? 'catblocks' : 'classic',
}
);

// @todo This is a hack to make there be no toolbox.
Expand Down Expand Up @@ -173,6 +187,8 @@ CustomProcedures.propTypes = {
isRtl: PropTypes.bool,
mutator: PropTypes.instanceOf(Element),
onRequestClose: PropTypes.func.isRequired,
colorMode: PropTypes.string,
useCatBlocks: PropTypes.bool,
options: PropTypes.shape({
media: PropTypes.string,
zoom: PropTypes.shape({
Expand Down Expand Up @@ -202,7 +218,9 @@ CustomProcedures.defaultProps = {

const mapStateToProps = state => ({
isRtl: state.locales.isRtl,
mutator: state.scratchGui.customProcedures.mutator
mutator: state.scratchGui.customProcedures.mutator,
colorMode: state.scratchGui.settings.colorMode,
useCatBlocks: state.scratchGui.settings.theme === CAT_BLOCKS_THEME,
});

export default connect(
Expand Down
19 changes: 16 additions & 3 deletions packages/scratch-gui/src/lib/define-dynamic-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,12 +422,25 @@ const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extend
this.needsBlockInfoUpdate = true;
},
mutationToDom: function () {
const container = document.createElement('mutation');
container.setAttribute('blockInfo', this.blockInfoText);
// `document.createElement('mutation')` creates an HTMLUnknownElement,
// which lowercases attribute names. After XMLSerializer round-trip
// (used by Blockly v2's JSON serialization for `extraState`), the
// camelCase attribute name `blockInfo` becomes `blockinfo`, and
// `getAttribute('blockInfo')` then returns `null` when re-parsed as
// XML. Use Blockly's `utils.xml.createElement` so the element is in
// the XML namespace and attribute case is preserved through the
// round-trip — and also fall back to lowercase `blockinfo` on the
// read side for any state already written with the lowercase form.
const xmlUtils = ScratchBlocks.utils && ScratchBlocks.utils.xml;
const container = xmlUtils && typeof xmlUtils.createElement === 'function' ?
xmlUtils.createElement('mutation') :
document.createElement('mutation');
container.setAttribute('blockinfo', this.blockInfoText);
return container;
},
domToMutation: function (xmlElement) {
const blockInfoText = xmlElement.getAttribute('blockInfo');
const blockInfoText =
xmlElement.getAttribute('blockinfo') || xmlElement.getAttribute('blockInfo');
if (!blockInfoText) return;

// === Smalruby: Start of argumentsByMethod support ===
Expand Down
21 changes: 19 additions & 2 deletions packages/scratch-gui/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,30 @@ const baseConfig = new ScratchWebpackConfigBuilder(
}))
.addPlugin(new CopyWebpackPlugin({
patterns: [
// scratch-blocks v2 (Blockly v12) re-exports some media (e.g.
// `disconnect.mp3`) from the underlying `blockly/media` folder
// rather than re-shipping them in `scratch-blocks/media`. Copy
// those first so that the scratch-blocks-specific assets below
// overwrite anything they need to customise.
{
from: '../../node_modules/blockly/media',
to: 'static/blocks-media/default',
noErrorOnMissing: true
},
{
from: '../../node_modules/blockly/media',
to: 'static/blocks-media/high-contrast',
noErrorOnMissing: true
},
{
from: '../../node_modules/scratch-blocks/media',
to: 'static/blocks-media/default'
to: 'static/blocks-media/default',
force: true
},
{
from: '../../node_modules/scratch-blocks/media',
to: 'static/blocks-media/high-contrast'
to: 'static/blocks-media/high-contrast',
force: true
},
{
// overwrite some of the default block media with high-contrast versions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"smalrubyRuby.arrayMethod": "配列 [RECEIVER] . [METHOD]",
"smalrubyRuby.hashMethod": "ハッシュ [RECEIVER] . [METHOD]",
"smalrubyRuby.arrayMethodWithBlock": "配列 [RECEIVER] . [METHOD] do",
"smalrubyRuby.hashMethodWithBlock": "ハッシュ [RECEIVER] . [METHOD] do",
"smalrubyRuby.numberMethodWithBlock": "数値 [RECEIVER] . [METHOD] do",
"smalrubyRuby.blockParam": "ブロックパラメーター [PARAM]",
"smalrubyRuby.returnValue": "戻り値",
Expand All @@ -16,6 +17,7 @@
"smalrubyRuby.arrayMethod": "はいれつ [RECEIVER] . [METHOD]",
"smalrubyRuby.hashMethod": "はっしゅ [RECEIVER] . [METHOD]",
"smalrubyRuby.arrayMethodWithBlock": "はいれつ [RECEIVER] . [METHOD] do",
"smalrubyRuby.hashMethodWithBlock": "はっしゅ [RECEIVER] . [METHOD] do",
"smalrubyRuby.numberMethodWithBlock": "すうち [RECEIVER] . [METHOD] do",
"smalrubyRuby.blockParam": "ぶろっくぱらめーたー [PARAM]",
"smalrubyRuby.returnValue": "もどりち",
Expand Down
Loading