mirror of
https://github.com/google/blockly.git
synced 2026-01-04 15:40:08 +01:00
feat: add support for appending custom inputs (#6990)
* feat: add appendInput method * feat: enable constructing inputs from the registry * chore: reorganize into suites * chore: add new input test + fixup existing * chore: reorganize appending from registry * chore: fix input types enum * chore: fix tests
This commit is contained in:
@@ -41,6 +41,7 @@ import * as arrayUtils from './utils/array.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import * as idGenerator from './utils/idgenerator.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
import * as registry from './registry.js';
|
||||
import {Size} from './utils/size.js';
|
||||
import type {VariableModel} from './variable_model.js';
|
||||
import type {Workspace} from './workspace.js';
|
||||
@@ -1482,7 +1483,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for appending a value input row.
|
||||
* Appends a value input row.
|
||||
*
|
||||
* @param name Language-neutral identifier which may used to find this input
|
||||
* again. Should be unique to this block.
|
||||
@@ -1493,7 +1494,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for appending a statement input row.
|
||||
* Appends a statement input row.
|
||||
*
|
||||
* @param name Language-neutral identifier which may used to find this input
|
||||
* again. Should be unique to this block.
|
||||
@@ -1504,7 +1505,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for appending a dummy input row.
|
||||
* Appends a dummy input row.
|
||||
*
|
||||
* @param opt_name Language-neutral identifier which may used to find this
|
||||
* input again. Should be unique to this block.
|
||||
@@ -1514,6 +1515,33 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
return this.appendInput_(inputTypes.DUMMY, opt_name || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given input row.
|
||||
*
|
||||
* Allows for custom inputs to be appended to the block.
|
||||
*/
|
||||
appendInput(input: Input): Input {
|
||||
this.inputList.push(input);
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an input with the given input type and name to the block after
|
||||
* constructing it from the registry.
|
||||
*
|
||||
* @param type The name the input is registered under in the registry.
|
||||
* @param name The name the input will have within the block.
|
||||
* @returns The constucted input, or null if there was no constructor
|
||||
* associated with the type.
|
||||
*/
|
||||
private appendInputFromRegistry(type: string, name: string): Input|null {
|
||||
const inputConstructor =
|
||||
registry.getClass(registry.Type.INPUT, type, false);
|
||||
if (!inputConstructor) return null;
|
||||
return this.appendInput(
|
||||
new inputConstructor(inputTypes.CUSTOM, name, this, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this block using a cross-platform, internationalization-friendly
|
||||
* JSON description.
|
||||
@@ -1850,6 +1878,10 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
case 'input_dummy':
|
||||
input = this.appendDummyInput(element['name']);
|
||||
break;
|
||||
default: {
|
||||
input = this.appendInputFromRegistry(element['type'], element['name']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Should never be hit because of interpolate_'s checks, but just in case.
|
||||
if (!input) {
|
||||
@@ -1881,7 +1913,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
*/
|
||||
private isInputKeyword_(str: string): boolean {
|
||||
return str === 'input_value' || str === 'input_statement' ||
|
||||
str === 'input_dummy';
|
||||
str === 'input_dummy' || registry.hasItem(registry.Type.INPUT, str);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,12 +41,14 @@ export class Input {
|
||||
* @param name Language-neutral identifier which may used to find this input
|
||||
* again.
|
||||
* @param block The block containing this input.
|
||||
* @param connection Optional connection for this input.
|
||||
* @param connection Optional connection for this input. If this is a custom
|
||||
* input, `null` will always be passed, and then the subclass can
|
||||
* optionally construct a connection.
|
||||
*/
|
||||
constructor(
|
||||
public type: number, public name: string, block: Block,
|
||||
public connection: Connection|null) {
|
||||
if (type !== inputTypes.DUMMY && !name) {
|
||||
if ((type === inputTypes.VALUE || type === inputTypes.STATEMENT) && !name) {
|
||||
throw Error(
|
||||
'Value inputs and statement inputs must have non-empty name.');
|
||||
}
|
||||
|
||||
@@ -19,5 +19,7 @@ export enum inputTypes {
|
||||
// A down-facing block stack. E.g. 'if-do' or 'else'.
|
||||
STATEMENT = ConnectionType.NEXT_STATEMENT,
|
||||
// A dummy input. Used to add field(s) with no input.
|
||||
DUMMY = 5
|
||||
DUMMY = 5,
|
||||
// An unknown type of input defined by an external developer.
|
||||
CUSTOM = 6,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {IBlockDragger} from './interfaces/i_block_dragger.js';
|
||||
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
||||
import type {Input} from './input.js';
|
||||
import type {ISerializer} from './interfaces/i_serializer.js';
|
||||
import type {IToolbox} from './interfaces/i_toolbox.js';
|
||||
import type {Cursor} from './keyboard_nav/cursor.js';
|
||||
@@ -69,6 +70,8 @@ export class Type<_T> {
|
||||
|
||||
static FIELD = new Type<Field>('field');
|
||||
|
||||
static INPUT = new Type<Input>('input');
|
||||
|
||||
static RENDERER = new Type<Renderer>('renderer');
|
||||
|
||||
static TOOLBOX = new Type<IToolbox>('toolbox');
|
||||
|
||||
@@ -7,9 +7,18 @@
|
||||
goog.declareModuleId('Blockly.test.blockJson');
|
||||
|
||||
import {Align} from '../../build/src/core/input.js';
|
||||
|
||||
import {sharedTestSetup, sharedTestTeardown} from './test_helpers/setup_teardown.js';
|
||||
|
||||
suite('Block JSON initialization', function() {
|
||||
setup(function() {
|
||||
sharedTestSetup.call(this);
|
||||
this.workspace = new Blockly.Workspace();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
sharedTestTeardown.call(this);
|
||||
});
|
||||
|
||||
suite('validateTokens_', function() {
|
||||
setup(function() {
|
||||
this.assertError = function(tokens, count, error) {
|
||||
@@ -434,21 +443,12 @@ suite('Block JSON initialization', function() {
|
||||
|
||||
suite('inputFromJson_', function() {
|
||||
setup(function() {
|
||||
const Input = function(type) {
|
||||
this.type = type;
|
||||
this.setCheck = sinon.fake();
|
||||
this.setAlign = sinon.fake();
|
||||
};
|
||||
const Block = function() {
|
||||
this.type = 'test';
|
||||
this.appendDummyInput = sinon.fake.returns(new Input());
|
||||
this.appendValueInput = sinon.fake.returns(new Input());
|
||||
this.appendStatementInput = sinon.fake.returns(new Input());
|
||||
this.inputFromJson_ = Blockly.Block.prototype.inputFromJson_;
|
||||
};
|
||||
|
||||
this.assertInput = function(json, type, check, align) {
|
||||
const block = new Block();
|
||||
const block = this.workspace.newBlock('test_basic_empty');
|
||||
sinon.spy(block, 'appendDummyInput');
|
||||
sinon.spy(block, 'appendValueInput');
|
||||
sinon.spy(block, 'appendStatementInput');
|
||||
|
||||
const input = block.inputFromJson_(json);
|
||||
switch (type) {
|
||||
case 'input_dummy':
|
||||
@@ -474,126 +474,146 @@ suite('Block JSON initialization', function() {
|
||||
return;
|
||||
}
|
||||
if (check) {
|
||||
chai.assert.isTrue(input.setCheck.calledWith(check),
|
||||
'Expected setCheck to be called with', check);
|
||||
} else {
|
||||
chai.assert.isTrue(input.setCheck.notCalled,
|
||||
'Expected setCheck to not be called');
|
||||
if (Array.isArray(check)) {
|
||||
chai.assert.deepEqual(check, input.connection.getCheck());
|
||||
} else {
|
||||
chai.assert.deepEqual([check], input.connection.getCheck());
|
||||
}
|
||||
}
|
||||
if (align !== undefined) {
|
||||
chai.assert.isTrue(input.setAlign.calledWith(align),
|
||||
'Expected setAlign to be called with', align);
|
||||
} else {
|
||||
chai.assert.isTrue(input.setAlign.notCalled,
|
||||
'Expected setAlign to not be called');
|
||||
chai.assert.equal(align, input.align);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
test('Dummy', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
},
|
||||
'input_dummy');
|
||||
suite('input types', function() {
|
||||
test('Dummy', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
},
|
||||
'input_dummy');
|
||||
});
|
||||
|
||||
test('Value', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
'input_value');
|
||||
});
|
||||
|
||||
test('Statement', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'NAME',
|
||||
},
|
||||
'input_statement');
|
||||
});
|
||||
|
||||
test('Bad input type', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_bad',
|
||||
},
|
||||
'input_bad');
|
||||
});
|
||||
|
||||
test('custom input types are constructed from the registry', function() {
|
||||
class CustomInput extends Blockly.Input { }
|
||||
Blockly.registry.register(
|
||||
Blockly.registry.Type.INPUT, 'custom', CustomInput);
|
||||
const block = this.workspace.newBlock('test_basic_empty');
|
||||
block.inputFromJson_({'type': 'custom'});
|
||||
chai.assert.instanceOf(
|
||||
block.inputList[0],
|
||||
CustomInput,
|
||||
'Expected the registered input to be constructed');
|
||||
});
|
||||
});
|
||||
|
||||
test('Value', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_value',
|
||||
},
|
||||
'input_value');
|
||||
suite('connection checks', function() {
|
||||
test('String Check', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
'check': 'Integer',
|
||||
},
|
||||
'input_value',
|
||||
'Integer');
|
||||
});
|
||||
|
||||
test('Array check', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
'check': ['Integer', 'Number'],
|
||||
},
|
||||
'input_value',
|
||||
['Integer', 'Number']);
|
||||
});
|
||||
|
||||
test('Empty check', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
'check': '',
|
||||
},
|
||||
'input_value');
|
||||
});
|
||||
|
||||
test('Null check', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
'check': null,
|
||||
},
|
||||
'input_value');
|
||||
});
|
||||
});
|
||||
|
||||
test('Statement', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_statement',
|
||||
},
|
||||
'input_statement');
|
||||
});
|
||||
|
||||
test('Bad input type', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_bad',
|
||||
},
|
||||
'input_bad');
|
||||
});
|
||||
|
||||
test('String Check', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'check': 'Integer',
|
||||
},
|
||||
'input_dummy',
|
||||
'Integer');
|
||||
});
|
||||
|
||||
test('Array check', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'check': ['Integer', 'Number'],
|
||||
},
|
||||
'input_dummy',
|
||||
['Integer', 'Number']);
|
||||
});
|
||||
|
||||
test('Empty check', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'check': '',
|
||||
},
|
||||
'input_dummy');
|
||||
});
|
||||
|
||||
test('Null check', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'check': null,
|
||||
},
|
||||
'input_dummy');
|
||||
});
|
||||
|
||||
test('"Left" align', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'align': 'LEFT',
|
||||
},
|
||||
'input_dummy', undefined, Align.LEFT);
|
||||
});
|
||||
|
||||
test('"Right" align', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'align': 'RIGHT',
|
||||
},
|
||||
'input_dummy', undefined, Align.RIGHT);
|
||||
});
|
||||
|
||||
test('"Center" align', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'align': 'CENTER',
|
||||
},
|
||||
'input_dummy', undefined, Align.CENTRE);
|
||||
});
|
||||
|
||||
test('"Centre" align', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'align': 'CENTRE',
|
||||
},
|
||||
'input_dummy', undefined, Align.CENTRE);
|
||||
suite('alignment', function() {
|
||||
test('"Left" align', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'align': 'LEFT',
|
||||
},
|
||||
'input_dummy', undefined, Align.LEFT);
|
||||
});
|
||||
|
||||
test('"Right" align', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'align': 'RIGHT',
|
||||
},
|
||||
'input_dummy', undefined, Align.RIGHT);
|
||||
});
|
||||
|
||||
test('"Center" align', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'align': 'CENTER',
|
||||
},
|
||||
'input_dummy', undefined, Align.CENTRE);
|
||||
});
|
||||
|
||||
test('"Centre" align', function() {
|
||||
this.assertInput(
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'align': 'CENTRE',
|
||||
},
|
||||
'input_dummy', undefined, Align.CENTRE);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user