Skip to content

Commit a2bfdcc

Browse files
author
amp
committed
feat: improve expandable block
* Remove empty array from the toolbox. * Make expandablemake empty in toolbox by default. * Fix a bug that causes shadow inputs to break in expandables.
1 parent fa94bd4 commit a2bfdcc

2 files changed

Lines changed: 140 additions & 132 deletions

File tree

packages/blocks/blocks_vertical/arrays.js

Lines changed: 139 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -267,138 +267,158 @@ Blockly.Blocks["arrays_range"] = {
267267
};
268268

269269
Blockly.Blocks['arrays_expandablemake'] = {
270-
/**
271-
* Block for creating an array with dynamic placeholders.
272-
* Fixed to prevent shadow block ghosting during drags.
273-
* @this Blockly.Block
274-
*/
275-
init: function () {
276-
this.jsonInit({
277-
"message0": "array with %1 %2",
278-
"args0": [
279-
{
280-
"type": "field_expandable_remove",
281-
"name": "REMOVE"
282-
},
283-
{
284-
"type": "field_expandable_add",
285-
"name": "ADD"
286-
}
287-
],
288-
"output": "Array",
289-
"extensions": ["colours_data_lists", "shape_square"]
290-
});
270+
init: function () {
271+
this.jsonInit({
272+
// We use a dummy input for the main label so we can change it later
273+
"message0": "%2 %3 %1",
274+
"args0": [
275+
{
276+
"type": "field_label",
277+
"text": "array",
278+
"name": "MAIN_LABEL"
279+
},
280+
{
281+
"type": "field_expandable_remove",
282+
"name": "REMOVE"
283+
},
284+
{
285+
"type": "field_expandable_add",
286+
"name": "ADD"
287+
}
288+
],
289+
"output": "Array",
290+
"extensions": ["colours_data_lists", "shape_square"]
291+
});
291292

292-
this.inputs_ = 0;
293-
this.placeholders_ = ["apple", "banana", "pear", "orange", "kiwi"];
293+
this.placeholders_ = ["apple", "banana", "pear", "orange", "kiwi"];
294+
this.inputs_ = 0; // Start at 0 to keep it clean
295+
},
294296

295-
// Initial setup
296-
for (let i = 0; i < 2; i++) {
297-
this.addInput_(i);
298-
this.inputs_++;
299-
}
300-
},
297+
/**
298+
* Internal helper to update labels and create/remove inputs.
299+
*/
300+
updateShape_: function (targetCount, isLoading) {
301+
const labelField = this.getField('MAIN_LABEL');
302+
if (labelField) {
303+
labelField.setValue(targetCount > 0 ? "array with" : Blockly.Msg.ARRAYS_EMPTY_ARRAY);
304+
}
301305

302-
/**
303-
* Save the number of inputs to the XML.
304-
*/
305-
mutationToDom: function () {
306-
const container = document.createElement('mutation');
307-
container.setAttribute('items', String(this.inputs_));
308-
return container;
306+
// Add new inputs
307+
while (this.inputs_ < targetCount) {
308+
const index = this.inputs_;
309+
this.inputs_++;
310+
const inputName = `ADD${index}`;
311+
const input = this.appendValueInput(inputName);
312+
313+
// CRITICAL FIX: Only fill shadows if NOT loading from XML
314+
if (!isLoading && (this.rendered || this.isInFlyout)) {
315+
this.fillInBlock(input.connection, index);
316+
}
317+
}
318+
319+
// Remove inputs
320+
while (this.inputs_ > targetCount) {
321+
this.removeInput(`ADD${this.inputs_ - 1}`);
322+
this.inputs_--;
323+
}
309324
},
310325

311-
/**
312-
* Restore the number of inputs from XML.
313-
*/
314-
domToMutation: function (xmlElement) {
315-
const items = parseInt(xmlElement.getAttribute('items'), 10);
316-
const newCount = isNaN(items) ? 0 : items;
317-
318-
// 1. Remove existing inputs to ensure a clean rebuild
319-
for (let i = 0; i < this.inputs_; i++) {
320-
if (this.getInput('ADD' + i)) {
321-
this.removeInput('ADD' + i);
322-
}
323-
}
326+
fillInBlock: function (connection, index) {
327+
if (connection.sourceBlock_.isInsertionMarker_) return;
324328

325-
// 2. Update count and rebuild structure
326-
this.inputs_ = newCount;
327-
for (let i = 0; i < this.inputs_; i++) {
328-
this.addInput_(i);
329-
}
330-
},
329+
const textValue = this.placeholders_[index] || "item";
331330

332-
/**
333-
* Internal helper to create the input hole.
334-
* Shadow blocks are only created when manually expanding, not during drag/load.
335-
* @param {number} index The current input index.
336-
* @private
337-
*/
338-
addInput_: function (index) {
339-
const inputName = 'ADD' + index;
340-
if (this.getInput(inputName)) return; // Prevent duplicate inputs
341-
342-
const input = this.appendValueInput(inputName);
343-
input.setCheck(null);
344-
345-
// Logic check: Only spawn new shadow blocks if this is a live user action
346-
// and NOT a re-rendering/dragging event (where recordUndo is usually false/null)
347-
const isLiveAction = Blockly.Events.isEnabled() && !this.workspace.isFlyout;
348-
349-
if (isLiveAction && this.workspace) {
350-
const placeholderText = this.placeholders_[index] || "thing";
351-
const conn = input.connection;
352-
353-
if (conn && !conn.targetConnection) {
354-
const shadowBlock = this.workspace.newBlock('text');
355-
shadowBlock.setShadow(true);
356-
if (shadowBlock.getField('TEXT')) {
357-
shadowBlock.setFieldValue(placeholderText, 'TEXT');
358-
}
359-
shadowBlock.initSvg();
360-
shadowBlock.render();
361-
conn.connect(shadowBlock.outputConnection);
362-
}
331+
// 1. Create the XML definition of the shadow block
332+
const shadowDom = document.createElement("shadow");
333+
shadowDom.setAttribute("type", "text");
334+
335+
const fieldDom = document.createElement("field");
336+
fieldDom.setAttribute("name", "TEXT");
337+
fieldDom.textContent = textValue;
338+
339+
shadowDom.appendChild(fieldDom);
340+
341+
// 2. Register the shadow XML directly to the connection
342+
connection.setShadowDom(shadowDom);
343+
344+
// 3. Spawn the shadow block visually
345+
if (connection.respawnShadow_) {
346+
// The native Scratch-Blocks way to generate a shadow from its DOM
347+
connection.respawnShadow_();
348+
} else {
349+
// Fallback just in case respawnShadow_ is missing in your specific VM
350+
const shadowBlock = Blockly.Xml.domToBlock(shadowDom, this.workspace);
351+
shadowBlock.setShadow(true);
352+
shadowBlock.outputConnection.connect(connection);
353+
if (this.rendered) {
354+
shadowBlock.initSvg();
355+
shadowBlock.render(false);
363356
}
357+
}
364358
},
365359

366-
/**
367-
* Triggered by FieldExpandable buttons.
368-
*/
369-
onExpandableButtonClicked_: function (isAdding) {
370-
if (this.workspace && this.workspace.isFlyout) {
371-
return;
372-
}
373-
Blockly.Events.setGroup(true);
374-
const oldMutation = Blockly.Xml.domToText(this.mutationToDom());
375-
376-
if (isAdding) {
377-
// Logic for adding a new input
378-
this.addInput_(this.inputs_);
379-
this.inputs_++;
380-
} else {
381-
// Logic for removing the last input
382-
if (this.inputs_ > 0) {
383-
this.inputs_--;
384-
const inputName = 'ADD' + this.inputs_;
385-
const input = this.getInput(inputName);
386-
if (input && input.connection && input.connection.targetBlock()) {
387-
const target = input.connection.targetBlock();
388-
if (target.isShadow()) {
389-
target.dispose();
390-
}
391-
}
392-
this.removeInput(inputName);
360+
mutationToDom: function () {
361+
const container = document.createElement("mutation");
362+
container.setAttribute("items", String(this.inputs_));
363+
return container;
364+
},
365+
366+
domToMutation: function (xmlElement) {
367+
const items = parseInt(xmlElement.getAttribute('items'), 10);
368+
const newCount = isNaN(items) ? 0 : items;
369+
370+
// Update label
371+
const labelField = this.getField('MAIN_LABEL');
372+
if (labelField) {
373+
labelField.setValue(newCount > 0 ? "array with" : Blockly.Msg.ARRAYS_EMPTY_ARRAY);
374+
}
375+
376+
// Remove existing inputs (dispose any live shadows first)
377+
for (let i = this.inputs_ - 1; i >= 0; i--) {
378+
const input = this.getInput('ADD' + i);
379+
if (input && input.connection) {
380+
const target = input.connection.targetBlock();
381+
if (target && target.isShadow()) {
382+
target.dispose(false, false);
393383
}
394384
}
385+
this.removeInput('ADD' + i);
386+
}
387+
this.inputs_ = 0;
395388

396-
this.initSvg();
397-
this.render();
389+
// Re-create inputs: register shadowDom for future respawning,
390+
// but do NOT call respawnShadow_() — the XML loader will attach
391+
// the saved block/shadow from the project file itself.
392+
for (let i = 0; i < newCount; i++) {
393+
const input = this.appendValueInput('ADD' + i);
394+
this.inputs_++;
398395

399-
const newMutation = Blockly.Xml.domToText(this.mutationToDom());
400-
Blockly.Events.fire(new Blockly.Events.BlockChange(this,
401-
'mutation', null, oldMutation, newMutation));
402-
Blockly.Events.setGroup(false);
396+
// Register shadow DOM so it respawns when a real block is later removed
397+
const textValue = this.placeholders_[i] || "item";
398+
const shadowDom = document.createElement("shadow");
399+
shadowDom.setAttribute("type", "text");
400+
const fieldDom = document.createElement("field");
401+
fieldDom.setAttribute("name", "TEXT");
402+
fieldDom.textContent = textValue;
403+
shadowDom.appendChild(fieldDom);
404+
input.connection.setShadowDom(shadowDom);
405+
// ← No respawnShadow_() here! XML loader does the attachment.
403406
}
407+
},
408+
409+
onExpandableButtonClicked_: function (isAdding) {
410+
Blockly.Events.setGroup(true);
411+
const oldMutation = Blockly.Xml.domToText(this.mutationToDom());
412+
413+
const newCount = isAdding ? this.inputs_ + 1 : Math.max(0, this.inputs_ - 1);
414+
this.updateShape_(newCount);
415+
416+
this.initSvg();
417+
if (this.rendered) this.render();
418+
419+
const newMutation = Blockly.Xml.domToText(this.mutationToDom());
420+
Blockly.Events.fire(new Blockly.Events.BlockChange(this,
421+
'mutation', null, oldMutation, newMutation));
422+
Blockly.Events.setGroup(false);
423+
}
404424
};

packages/gui/src/lib/make-toolbox-xml.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -913,20 +913,8 @@ const arrays = function (isInitialSetup, isStage, targetId, colors) {
913913
id="arrays"
914914
colour="${colors.primary}"
915915
secondaryColour="${colors.tertiary}">
916-
<block type="arrays_empty_array" />
917-
${blockSeparator}
918916
<block type="arrays_expandablemake">
919-
<mutation items="2"></mutation>
920-
<value name="ADD0">
921-
<shadow type="text">
922-
<field name="TEXT">apple</field>
923-
</shadow>
924-
</value>
925-
<value name="ADD1">
926-
<shadow type="text">
927-
<field name="TEXT">banana</field>
928-
</shadow>
929-
</value>
917+
<mutation items="0"></mutation>
930918
</block>
931919
<block type="arrays_delimited_to_array">
932920
<value name="TEXT">

0 commit comments

Comments
 (0)