Insertion marker problem with custom field

449 views
Skip to first unread message

Alexandre Gonzalez

unread,
Jan 17, 2023, 11:52:57 AM1/17/23
to Blockly
Hello,

I'm a blockly newbie. I try to run an old code I didn't write myself. It include a custom field which add a button in a block.

Here is the code of the custom field which is an extends of Blockly.Field:
import Blockly from "blockly";

export default class FieldTextButton extends Blockly.Field {
onClick;

constructor(value, onClick, validator = null) {
super(value, validator);

this.SERIALIZABLE = true;
this.CURSOR = "default";
this.onClick = onClick;
}

showEditor_(_e) {
super.showEditor_(_e);

if (this.onClick) {
this.onClick();
}
}
}


Here is a block containing the custom field:
Blockly.Blocks["array"] = {
length: 0,
init: function () {
this.setColour(350);
this.setOutput(true, ["element"]);

const fieldTextValue = new FieldTextButton("+", function () {
return this.getSourceBlock().appendElementInput();
});

this.appendDummyInput("open_bracket")
.appendField(" Array ")
.appendField(fieldTextValue);

this.setInputsInline(false);
},
appendElementInput: function () {
appendElementInput(this);
},
deleteElementInput: function (inputToDelete) {
deleteElementInput(inputToDelete, this);
},
};


Here are the tow functions appendElementInput and deleteElementInput:
export const appendElementInput = (block) => {
const lastIndex = block.length++;

const fieldTextValue = new FieldTextButton("-", function () {
return block.deleteElementInput(appended_input);
});

const appended_input = block
.appendValueInput("element_" + lastIndex)
.appendField(fieldTextValue);
appendSelector(appended_input, [blocksSelectorArray], selectionArrow, "null");

block.moveInputBefore("element_" + lastIndex, null);

return appended_input;
};

export const deleteElementInput = (inputToDelete, block) => {
const inputNameToDelete = inputToDelete.name;

const subStructure = block.getInputTargetBlock(inputNameToDelete);
if (subStructure) {
subStructure.dispose(true);
}
block.removeInput(inputNameToDelete);

const inputIndexToDelete = parseInt(inputToDelete.name.match(/\d+/)[0]);
const lastIndex = --block.length;

// rename all the subsequent element-inputs
for (let i = inputIndexToDelete + 1; i <= lastIndex; i++) {
const input = block.getInput("element_" + i);
input.name = "element_" + (i - 1);
}
};


Everything works fine until I try to drag and drop the block. I get the following error: 
The insertion marker manager tried to create a marker but the result is missing an input. If you are using a mutator, make sure your domToMutation method is properly defined.

The problem seems to occur in the custom FieldTextButton. 
I'm ready to disable the insertion marker but it doesn't seem to be possible.

Do you see something wrong in the above code which can result to this error ? I'm clueless.

Thanks in advance for any help.



Maribeth Bottorff

unread,
Jan 17, 2023, 5:54:23 PM1/17/23
to Blockly
Hello,

Generally when you change the shape of the block (like changing how many inputs it has, as you're doing here) you also need to define a mutator for the block. A mutator provides a way of saving extra data when a block is serialized. We use serialization on blocks for many things: not just saving the state of a block/workspace, but also copying it and dragging/dropping it. When Blockly does these operations, it converts the code to text (either XML or JSON) and then converts the text back to a block. The serializer needs to know how many inputs a block has; by default it will look in the block definition and see that there is only one dummy input in the block definition, and in your case that doesn't match the existing block which now has multiple inputs if the button has been pressed.

So for the custom block, you'd need to include a mutator that stores how many inputs there are when serializing, and reads that data and creates the corresponding inputs when deserializing.

You might also be interested in the blocks-plus-minus plugin which provides a similar feature, using plus and minus buttons to add and remove inputs from certain blocks. You can see its source code to see the mutator it uses to store this extra data.

Maribeth

Alexandre Gonzalez

unread,
Jan 18, 2023, 1:20:12 PM1/18/23
to Blockly
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);

input.name = "element_" + (i - 1);

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.
Reply all
Reply to author
Forward
0 new messages