There are several ways you can customize the ways connections look, each with increasing difficulty. All of them require creating a custom renderer.
Basic dimensions
You can customize connections by changing their width or height, while maintaining the same basic shape. To do this, you need to create a custom constant provider component, and override some constants.
Different renderers define and use different constants, so check out the reference documentation for your super class:
For the base renderer, you can override NOTCH_WIDTH
and
NOTCH_HEIGHT
for next and previous connections, and
TAB_WIDTH
and TAB_HEIGHT
for input and output
connections.
class CustomConstantProvider extends Blockly.blockRendering.ConstantProvider {
constructor() {
super();
this.NOTCH_WIDTH = 20;
this.NOTCH_HEIGHT = 10;
this.TAB_HEIGHT = 8;
}
}
Basic shapes
You can customize connections by overriding their basic shape. Basic shapes have a height, a width, and two paths.
Each path draws the same shape, but from opposite ends!
This is necessary because as the drawer draws the outline of the block, it draws each kind of connection in both directions. For example, previous connections are drawn from left to right, but next connections are drawn from right to left. So you need to provide paths for both of those cases.
You can override the makeNotch
method for next and previous
connections, and the makePuzzleTab
method for input and
output connections.
class CustomConstantProvider extends Blockly.blockRendering.ConstantProvider {
makePuzzleTab() {
const width = this.TAB_WIDTH;
const height = this.TAB_HEIGHT;
return {
type: this.SHAPES.PUZZLE,
width,
height,
pathUp: Blockly.utils.svgPaths.line([
Blockly.utils.svgPaths.point(-width, -height / 2),
Blockly.utils.svgPaths.point(width, -height / 2)]),
pathDown: Blockly.utils.svgPaths.line([
Blockly.utils.svgPaths.point(-width, height / 2),
Blockly.utils.svgPaths.point(width, height / 2)]),
};
}
}
Check out the MDN SVG path documentation for information about how
to define path strings. The Blockly.utils.svgPaths
namespace is provided
as a thin wrapper around these strings to make them more readable.
Shapes for connection checks
You can customize connections by changing the shape based on the connection's connection check.
This lets you create different shapes to represent different data types. For example, strings could be represented by triangular connections, while booleans are represented by round connections.
To supply different shapes for different connection checks, you need to override
the shapeFor
method. The shapes returned should be initialized
in init
.
See basic shapes for information about what kinds of shapes are supported.
export class ConstantProvider extends Blockly.blockRendering.BaseConstantProvider {
shapeFor(connection) {
let check = connection.getCheck();
// For connections with no check, match any child block.
if (!check && connection.targetConnection) {
check = connection.targetConnection.getCheck();
}
if (check && check.includes('String')) return this.TRIANGULAR_TAB;
if (check && check.includes('Boolean')) return this.ROUND_TAB;
return super.shapeFor(connection);
}
}
Custom inputs
You can customize connection shapes by creating an entirely custom input. This is only done if you want some connections to look different from others, but you don't want it to be based on the connection check.
For example, if you want some value inputs to be indented like statement inputs, you can create a custom input to support this.
Create a custom input class
Follow the steps for creating a custom input.
Create a measurable
You need to create a measurable to represent your custom input.
Your custom input measurable should inherit from
Blockly.blockRendering.InputConnection
. It can also include
whatever extra measurement data you need to draw the input's shape.
export class CustomInputMeasurable extends Blockly.blockRendering.InputConnection {
constructor(constants, input) {
super(constants, input);
// Any extra measurement data...
}
}
Instantiate your measurable
Your render info needs to instantiate your custom
measurable. To do this, you need to override the addInput_
method.
export class RenderInfo extends Blockly.blockRendering.RenderInfo {
addInput_(input, activeRow) {
if (input instanceof CustomInput) {
activeRow.elements.push(new CustomInputMeasurable(this.constants_, input));
}
super.addInput_(input, activeRow);
}
}
Optionally create a row
By default, inputs don't create new rows. If you want your input
to trigger the end of a row, you need to override the
shouldStartNewRow_
method of your
render info.
export class RenderInfo extends Blockly.blockRendering.RenderInfo {
shouldStartNewRow_(currInput, prevInput) {
if (prevInput instanceof CustomInput) return true;
return super.shouldStartNewRow_(currInput, prevInput);
}
}
Optionally create a shape for your input
It is a good idea to store the shape of your input in a constant, just like we do for notches and puzzle tabs. This keeps your code organized, and makes it easier to modify later.
Draw the input
Lastly, you need to modify your drawer to draw the shape.
Custom inputs can either:
Affect the outline of your block, like statement inputs
Or affect the internals of your block, like inline value inputs
If the input affects the outline of your block, override
drawOutline_
, otherwise, override
drawInternals_
.
export class Drawer extends Blockly.blockRendering.Drawer {
drawOutline_() {
this.drawTop_();
for (let r = 1; r < this.info_.rows.length - 1; r++) {
const row = this.info_.rows[r];
// Insert checks for your input here!
if (row.getLastInput() instanceof CustomInputMeasurable) {
this.drawCustomInput(row);
} else if (row.hasJaggedEdge) {
this.drawJaggedEdge_(row);
} else if (row.hasStatement) {
this.drawStatementInput_(row);
} else if (row.hasExternalInput) {
this.drawValueInput_(row);
} else {
this.drawRightSideRow_(row);
}
}
this.drawBottom_();
this.drawLeft_();
}
protected drawInternals_() {
for (const row of rows) {
for (const elem of row) {
// Insert checks for your input here!
if (elem instanceof CustomInputMeasurable) {
this.drawCustomInput(elem);
}
if (Types.isInlineInput(elem)) {
this.drawInlineInput_(elem as InlineInput);
} else if (Types.isIcon(elem) || Types.isField(elem)) {
this.layoutField_(elem as Field | Icon);
}
}
}
}
}