mirror of
https://github.com/google/blockly.git
synced 2026-01-05 08:00:09 +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 {Coordinate} from './utils/coordinate.js';
|
||||||
import * as idGenerator from './utils/idgenerator.js';
|
import * as idGenerator from './utils/idgenerator.js';
|
||||||
import * as parsing from './utils/parsing.js';
|
import * as parsing from './utils/parsing.js';
|
||||||
|
import * as registry from './registry.js';
|
||||||
import {Size} from './utils/size.js';
|
import {Size} from './utils/size.js';
|
||||||
import type {VariableModel} from './variable_model.js';
|
import type {VariableModel} from './variable_model.js';
|
||||||
import type {Workspace} from './workspace.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
|
* @param name Language-neutral identifier which may used to find this input
|
||||||
* again. Should be unique to this block.
|
* 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
|
* @param name Language-neutral identifier which may used to find this input
|
||||||
* again. Should be unique to this block.
|
* 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
|
* @param opt_name Language-neutral identifier which may used to find this
|
||||||
* input again. Should be unique to this block.
|
* 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 || '');
|
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
|
* Initialize this block using a cross-platform, internationalization-friendly
|
||||||
* JSON description.
|
* JSON description.
|
||||||
@@ -1850,6 +1878,10 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
case 'input_dummy':
|
case 'input_dummy':
|
||||||
input = this.appendDummyInput(element['name']);
|
input = this.appendDummyInput(element['name']);
|
||||||
break;
|
break;
|
||||||
|
default: {
|
||||||
|
input = this.appendInputFromRegistry(element['type'], element['name']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Should never be hit because of interpolate_'s checks, but just in case.
|
// Should never be hit because of interpolate_'s checks, but just in case.
|
||||||
if (!input) {
|
if (!input) {
|
||||||
@@ -1881,7 +1913,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
*/
|
*/
|
||||||
private isInputKeyword_(str: string): boolean {
|
private isInputKeyword_(str: string): boolean {
|
||||||
return str === 'input_value' || str === 'input_statement' ||
|
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
|
* @param name Language-neutral identifier which may used to find this input
|
||||||
* again.
|
* again.
|
||||||
* @param block The block containing this input.
|
* @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(
|
constructor(
|
||||||
public type: number, public name: string, block: Block,
|
public type: number, public name: string, block: Block,
|
||||||
public connection: Connection|null) {
|
public connection: Connection|null) {
|
||||||
if (type !== inputTypes.DUMMY && !name) {
|
if ((type === inputTypes.VALUE || type === inputTypes.STATEMENT) && !name) {
|
||||||
throw Error(
|
throw Error(
|
||||||
'Value inputs and statement inputs must have non-empty name.');
|
'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'.
|
// A down-facing block stack. E.g. 'if-do' or 'else'.
|
||||||
STATEMENT = ConnectionType.NEXT_STATEMENT,
|
STATEMENT = ConnectionType.NEXT_STATEMENT,
|
||||||
// A dummy input. Used to add field(s) with no input.
|
// 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 {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
||||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||||
import type {IMetricsManager} from './interfaces/i_metrics_manager.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 {ISerializer} from './interfaces/i_serializer.js';
|
||||||
import type {IToolbox} from './interfaces/i_toolbox.js';
|
import type {IToolbox} from './interfaces/i_toolbox.js';
|
||||||
import type {Cursor} from './keyboard_nav/cursor.js';
|
import type {Cursor} from './keyboard_nav/cursor.js';
|
||||||
@@ -69,6 +70,8 @@ export class Type<_T> {
|
|||||||
|
|
||||||
static FIELD = new Type<Field>('field');
|
static FIELD = new Type<Field>('field');
|
||||||
|
|
||||||
|
static INPUT = new Type<Input>('input');
|
||||||
|
|
||||||
static RENDERER = new Type<Renderer>('renderer');
|
static RENDERER = new Type<Renderer>('renderer');
|
||||||
|
|
||||||
static TOOLBOX = new Type<IToolbox>('toolbox');
|
static TOOLBOX = new Type<IToolbox>('toolbox');
|
||||||
|
|||||||
@@ -7,9 +7,18 @@
|
|||||||
goog.declareModuleId('Blockly.test.blockJson');
|
goog.declareModuleId('Blockly.test.blockJson');
|
||||||
|
|
||||||
import {Align} from '../../build/src/core/input.js';
|
import {Align} from '../../build/src/core/input.js';
|
||||||
|
import {sharedTestSetup, sharedTestTeardown} from './test_helpers/setup_teardown.js';
|
||||||
|
|
||||||
suite('Block JSON initialization', function() {
|
suite('Block JSON initialization', function() {
|
||||||
|
setup(function() {
|
||||||
|
sharedTestSetup.call(this);
|
||||||
|
this.workspace = new Blockly.Workspace();
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(function() {
|
||||||
|
sharedTestTeardown.call(this);
|
||||||
|
});
|
||||||
|
|
||||||
suite('validateTokens_', function() {
|
suite('validateTokens_', function() {
|
||||||
setup(function() {
|
setup(function() {
|
||||||
this.assertError = function(tokens, count, error) {
|
this.assertError = function(tokens, count, error) {
|
||||||
@@ -434,21 +443,12 @@ suite('Block JSON initialization', function() {
|
|||||||
|
|
||||||
suite('inputFromJson_', function() {
|
suite('inputFromJson_', function() {
|
||||||
setup(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) {
|
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);
|
const input = block.inputFromJson_(json);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'input_dummy':
|
case 'input_dummy':
|
||||||
@@ -474,126 +474,146 @@ suite('Block JSON initialization', function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (check) {
|
if (check) {
|
||||||
chai.assert.isTrue(input.setCheck.calledWith(check),
|
if (Array.isArray(check)) {
|
||||||
'Expected setCheck to be called with', check);
|
chai.assert.deepEqual(check, input.connection.getCheck());
|
||||||
} else {
|
} else {
|
||||||
chai.assert.isTrue(input.setCheck.notCalled,
|
chai.assert.deepEqual([check], input.connection.getCheck());
|
||||||
'Expected setCheck to not be called');
|
}
|
||||||
}
|
}
|
||||||
if (align !== undefined) {
|
if (align !== undefined) {
|
||||||
chai.assert.isTrue(input.setAlign.calledWith(align),
|
chai.assert.equal(align, input.align);
|
||||||
'Expected setAlign to be called with', align);
|
|
||||||
} else {
|
|
||||||
chai.assert.isTrue(input.setAlign.notCalled,
|
|
||||||
'Expected setAlign to not be called');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Dummy', function() {
|
suite('input types', function() {
|
||||||
this.assertInput(
|
test('Dummy', function() {
|
||||||
{
|
this.assertInput(
|
||||||
'type': 'input_dummy',
|
{
|
||||||
},
|
'type': 'input_dummy',
|
||||||
'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() {
|
suite('connection checks', function() {
|
||||||
this.assertInput(
|
test('String Check', function() {
|
||||||
{
|
this.assertInput(
|
||||||
'type': 'input_value',
|
{
|
||||||
},
|
'type': 'input_value',
|
||||||
'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() {
|
suite('alignment', function() {
|
||||||
this.assertInput(
|
test('"Left" align', function() {
|
||||||
{
|
this.assertInput(
|
||||||
'type': 'input_statement',
|
{
|
||||||
},
|
'type': 'input_dummy',
|
||||||
'input_statement');
|
'align': 'LEFT',
|
||||||
});
|
},
|
||||||
|
'input_dummy', undefined, Align.LEFT);
|
||||||
test('Bad input type', function() {
|
});
|
||||||
this.assertInput(
|
|
||||||
{
|
test('"Right" align', function() {
|
||||||
'type': 'input_bad',
|
this.assertInput(
|
||||||
},
|
{
|
||||||
'input_bad');
|
'type': 'input_dummy',
|
||||||
});
|
'align': 'RIGHT',
|
||||||
|
},
|
||||||
test('String Check', function() {
|
'input_dummy', undefined, Align.RIGHT);
|
||||||
this.assertInput(
|
});
|
||||||
{
|
|
||||||
'type': 'input_dummy',
|
test('"Center" align', function() {
|
||||||
'check': 'Integer',
|
this.assertInput(
|
||||||
},
|
{
|
||||||
'input_dummy',
|
'type': 'input_dummy',
|
||||||
'Integer');
|
'align': 'CENTER',
|
||||||
});
|
},
|
||||||
|
'input_dummy', undefined, Align.CENTRE);
|
||||||
test('Array check', function() {
|
});
|
||||||
this.assertInput(
|
|
||||||
{
|
test('"Centre" align', function() {
|
||||||
'type': 'input_dummy',
|
this.assertInput(
|
||||||
'check': ['Integer', 'Number'],
|
{
|
||||||
},
|
'type': 'input_dummy',
|
||||||
'input_dummy',
|
'align': 'CENTRE',
|
||||||
['Integer', 'Number']);
|
},
|
||||||
});
|
'input_dummy', undefined, Align.CENTRE);
|
||||||
|
});
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user