Thanks Maribeth for the clear explanation !
Based on the
blocks-plus-minus plugin, I succeed to make it works ! I don't know if I get it right, though. As I use Javascript block definition, I adapted the code to fit my case.
Here is the code of the mutator:
import Blockly from "blockly/core";
import {
appendSelector,
blocksSelectorArray,
keyValueArrow,
selectionArrow,
} from "./custom-blocks-definition-utils";
import FieldMinusButton from "./field-minus-button";
const PlusMinusMutator = {
/**
* Number of item inputs the block has.
* @type {number}
*/
length: 0,
isKeyValueInput: false,
/**
* Creates XML to represent number of text inputs.
* @return {!Element} XML storage element.
* @this {Blockly.Block}
*/
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("items", this.length);
return container;
},
/**
* Parses XML to restore the text inputs.
* @param {!Element} xmlElement XML storage element.
* @this {Blockly.Block}
*/
domToMutation: function (xmlElement) {
const targetCount = parseInt(xmlElement.getAttribute("items"), 10);
this.updateShape_(targetCount);
},
/**
* Returns the state of this block as a JSON serializable object.
* @return {{itemCount: number}} The state of this block, ie the item count.
*/
saveExtraState: function () {
return {
itemCount: this.length,
};
},
/**
* Applies the given state to this block.
* @param {*} state The state to apply to this block, ie the item count.
*/
loadExtraState: function (state) {
this.updateShape_(state["itemCount"]);
},
/**
* Adds inputs to the block until it reaches the target number of inputs.
* @param {number} targetCount The target number of inputs for the block.
* @this {Blockly.Block}
* @private
*/
updateShape_: function (targetCount) {
while (this.length < targetCount) {
this.addPart_(this.isKeyValueInput);
}
while (this.length > targetCount) {
this.removePart_();
}
},
/**
* Callback for the plus image. Adds an input to the end of the block and
* updates the state of the minus.
*/
plus: function (isKeyValueInput = false) {
this.addPart_(isKeyValueInput);
},
/**
* Callback for the minus image. Removes an input from the end of the block
* and updates the state of the minus.
*/
minus: function (inputToDelete) {
if (this.length === 0) {
return;
}
this.removePart_(inputToDelete);
},
/**
* Adds an input to the end of the block. If the block currently has no
* inputs it updates the top 'EMPTY' input to receive a block.
* @this {Blockly.Block}
* @private
*/
addPart_: function (isKeyValueInput = false) {
const lastIndex = this.length++;
const fieldTextValue = new FieldMinusButton("-");
const appended_input = this.appendValueInput(
"element_" + lastIndex
).appendField(fieldTextValue);
if (isKeyValueInput) {
appended_input
.appendField(
new Blockly.FieldTextInput("key_" + lastIndex),
"key_field_" + lastIndex
)
.appendField(keyValueArrow);
}
appendSelector(appended_input, blocksSelectorArray, selectionArrow, "null");
this.moveInputBefore("element_" + lastIndex, null);
},
/**
* Removes an input from the end of the block. If we are removing the last
* input this updates the block to have an 'EMPTY' top input.
* @this {Blockly.Block}
* @private
*/
removePart_: function (inputToDelete) {
this.length--;
const inputNameToDelete = inputToDelete.name;
const subStructure = this.getInputTargetBlock(inputNameToDelete);
if (subStructure) {
subStructure.dispose(true);
}
this.removeInput(inputNameToDelete);
const inputIndexToDelete = parseInt(inputToDelete.name.match(/\d+/)[0]);
// rename all the subsequent element-inputs
for (let i = inputIndexToDelete + 1; i <= this.length; i++) {
const input = this.getInput("element_" + i);
const keyField = this.getField("key_field_" + i);
if (keyField) {
keyField.name = "key_field_" + (i - 1);
}
}
},
};
export default PlusMinusMutator;
Here is the FieldPlusButton class:
import Blockly from "blockly";
import { getExtraBlockState } from "./serialization_helper";
export default class FieldPlusButton extends Blockly.Field {
isKeyValueInput = false;
constructor(value, isKeyValueInput = false, validator = null) {
super(value, validator);
this.SERIALIZABLE = true;
this.CURSOR = "default";
this.isKeyValueInput = isKeyValueInput;
}
showEditor_(_e) {
super.showEditor_(_e);
this.onClick_(this.isKeyValueInput);
}
/**
* Calls block.plus(args) when the plus field is clicked.
* @private
* @param isKeyValueInput true if input is a {key: value} input
*/
onClick_(isKeyValueInput = false) {
// TODO: This is a dupe of the mutator code, anyway to unify?
const block = this.getSourceBlock();
if (block.isInFlyout) {
return;
}
Blockly.Events.setGroup(true);
const oldExtraState = getExtraBlockState(block);
block.plus(isKeyValueInput);
const newExtraState = getExtraBlockState(block);
if (oldExtraState !== newExtraState) {
Blockly.Events.fire(
new Blockly.Events.BlockChange(
block,
"mutation",
null,
oldExtraState,
newExtraState
)
);
}
Blockly.Events.setGroup(false);
}
}
And here is the definition of the block:
Blockly.Blocks["array"] = {
init: function () {
this.setColour(350);
this.setOutput(true, ["element"]);
const fieldTextValue = new FieldPlusButton("+");
this.appendDummyInput("open_bracket")
.appendField(" Array ")
.appendField(fieldTextValue);
this.setInputsInline(false);
},
saveExtraState: PlusMinusMutator.saveExtraState,
loadExtraState: PlusMinusMutator.loadExtraState,
mutationToDom: PlusMinusMutator.mutationToDom,
domToMutation: PlusMinusMutator.domToMutation,
updateShape_: PlusMinusMutator.updateShape_,
plus: PlusMinusMutator.plus,
minus: PlusMinusMutator.minus,
addPart_: PlusMinusMutator.addPart_,
removePart_: PlusMinusMutator.removePart_,
length: PlusMinusMutator.length,
};
I didn't need to register the Mutator and I defined every functions on each block, that's what makes me say that I didn't do it right.
What do you think about it ?
Thanks again for your useful help.