Create a global registry that fields, renderers and future objects can use
This commit is contained in:
alschmiedt
2020-05-26 11:21:42 -07:00
committed by GitHub
parent a540fd8676
commit 61054ee55a
7 changed files with 234 additions and 73 deletions

View File

@@ -55,7 +55,7 @@ goog.addDependency('../../core/field_label.js', ['Blockly.FieldLabel'], ['Blockl
goog.addDependency('../../core/field_label_serializable.js', ['Blockly.FieldLabelSerializable'], ['Blockly.FieldLabel', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.object'], {});
goog.addDependency('../../core/field_multilineinput.js', ['Blockly.FieldMultilineInput'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.FieldTextInput', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {'lang': 'es5'});
goog.addDependency('../../core/field_number.js', ['Blockly.FieldNumber'], ['Blockly.FieldTextInput', 'Blockly.fieldRegistry', 'Blockly.utils.aria', 'Blockly.utils.object'], {});
goog.addDependency('../../core/field_registry.js', ['Blockly.fieldRegistry'], [], {});
goog.addDependency('../../core/field_registry.js', ['Blockly.fieldRegistry'], ['Blockly.registry'], {});
goog.addDependency('../../core/field_textinput.js', ['Blockly.FieldTextInput'], ['Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.Msg', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.Size', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/field_variable.js', ['Blockly.FieldVariable'], ['Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.FieldDropdown', 'Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.Xml', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.object'], {});
goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.Block', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutCursor', 'Blockly.Gesture', 'Blockly.Marker', 'Blockly.Scrollbar', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom'], {});
@@ -70,10 +70,14 @@ goog.addDependency('../../core/icon.js', ['Blockly.Icon'], ['Blockly.utils', 'Bl
goog.addDependency('../../core/inject.js', ['Blockly.inject'], ['Blockly.BlockDragSurfaceSvg', 'Blockly.Component', 'Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Grid', 'Blockly.Msg', 'Blockly.Options', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.WorkspaceDragSurfaceSvg', 'Blockly.WorkspaceSvg', 'Blockly.user.keyMap', 'Blockly.utils', 'Blockly.utils.dom', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/input.js', ['Blockly.Input'], ['Blockly.Connection', 'Blockly.FieldLabel'], {});
goog.addDependency('../../core/insertion_marker_manager.js', ['Blockly.InsertionMarkerManager'], ['Blockly.Events', 'Blockly.blockAnimations'], {'lang': 'es5'});
goog.addDependency('../../core/interfaces/i_accessibility.js', ['Blockly.IASTNodeLocation', 'Blockly.IASTNodeLocationSvg', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.IBlocklyActionable'], [], {});
goog.addDependency('../../core/interfaces/i_bounded_element.js', ['Blockly.IBoundedElement'], [], {});
goog.addDependency('../../core/interfaces/i_copyable.js', ['Blockly.ICopyable'], ['Blockly.ISelectable'], {});
goog.addDependency('../../core/interfaces/i_deletable.js', ['Blockly.IDeletable'], [], {});
goog.addDependency('../../core/interfaces/i_deletearea.js', ['Blockly.IDeleteArea'], [], {});
goog.addDependency('../../core/interfaces/i_disposable.js', ['Blockly.IDisposable'], ['Blockly.Inheritance'], {});
goog.addDependency('../../core/interfaces/i_movable.js', ['Blockly.IMovable'], [], {});
goog.addDependency('../../core/interfaces/i_registrable.js', ['Blockly.IRegistrable'], [], {});
goog.addDependency('../../core/interfaces/i_selectable.js', ['Blockly.ISelectable'], ['Blockly.IDeletable', 'Blockly.IMovable'], {});
goog.addDependency('../../core/keyboard_nav/action.js', ['Blockly.Action'], [], {});
goog.addDependency('../../core/keyboard_nav/ast_node.js', ['Blockly.ASTNode'], ['Blockly.utils.Coordinate'], {'lang': 'es5'});
@@ -92,8 +96,9 @@ goog.addDependency('../../core/mutator.js', ['Blockly.Mutator'], ['Blockly.Bubbl
goog.addDependency('../../core/names.js', ['Blockly.Names'], ['Blockly.Msg'], {});
goog.addDependency('../../core/options.js', ['Blockly.Options'], ['Blockly.Theme', 'Blockly.Themes.Classic', 'Blockly.Xml', 'Blockly.user.keyMap', 'Blockly.utils.Metrics', 'Blockly.utils.toolbox', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/procedures.js', ['Blockly.Procedures'], ['Blockly.Blocks', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.Msg', 'Blockly.Names', 'Blockly.Workspace', 'Blockly.Xml', 'Blockly.constants', 'Blockly.utils.xml'], {});
goog.addDependency('../../core/registry.js', ['Blockly.registry'], [], {});
goog.addDependency('../../core/rendered_connection.js', ['Blockly.RenderedConnection'], ['Blockly.Connection', 'Blockly.Events', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom', 'Blockly.utils.object'], {});
goog.addDependency('../../core/renderers/common/block_rendering.js', ['Blockly.blockRendering'], ['Blockly.utils.object'], {});
goog.addDependency('../../core/renderers/common/block_rendering.js', ['Blockly.blockRendering'], ['Blockly.registry', 'Blockly.utils.object'], {});
goog.addDependency('../../core/renderers/common/constants.js', ['Blockly.blockRendering.ConstantProvider'], ['Blockly.utils', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.svgPaths', 'Blockly.utils.userAgent'], {'lang': 'es5'});
goog.addDependency('../../core/renderers/common/debugger.js', ['Blockly.blockRendering.Debug'], ['Blockly.blockRendering.BottomRow', 'Blockly.blockRendering.InputRow', 'Blockly.blockRendering.Measurable', 'Blockly.blockRendering.RenderInfo', 'Blockly.blockRendering.Row', 'Blockly.blockRendering.SpacerRow', 'Blockly.blockRendering.TopRow', 'Blockly.blockRendering.Types'], {'lang': 'es5'});
goog.addDependency('../../core/renderers/common/drawer.js', ['Blockly.blockRendering.Drawer'], ['Blockly.blockRendering.BottomRow', 'Blockly.blockRendering.InputRow', 'Blockly.blockRendering.Measurable', 'Blockly.blockRendering.RenderInfo', 'Blockly.blockRendering.Row', 'Blockly.blockRendering.SpacerRow', 'Blockly.blockRendering.TopRow', 'Blockly.blockRendering.Types', 'Blockly.utils.svgPaths'], {});

View File

@@ -14,39 +14,23 @@
goog.provide('Blockly.fieldRegistry');
goog.require('Blockly.registry');
/**
* The set of all registered fields, keyed by field type as used in the JSON
* definition of a block.
* @type {!Object<string, !{fromJson: Function}>}
* @private
*/
Blockly.fieldRegistry.typeMap_ = {};
/**
* Registers a field type.
* Blockly.fieldRegistry.fromJson uses this registry to
* find the appropriate field type.
* @param {string} type The field type name as used in the JSON definition.
* @param {!{fromJson: Function}} fieldClass The field class containing a
* fromJson function that can construct an instance of the field.
* @param {?function(new:Blockly.Field, ...?)} fieldClass The field class
* containing a fromJson function that can construct an instance of the
* field.
* @throws {Error} if the type name is empty, the field is already
* registered, or the fieldClass is not an object containing a fromJson
* function.
*/
Blockly.fieldRegistry.register = function(type, fieldClass) {
if ((typeof type != 'string') || (type.trim() == '')) {
throw Error('Invalid field type "' + type + '". The type must be a' +
' non-empty string.');
}
if (Blockly.fieldRegistry.typeMap_[type]) {
throw Error('Error: Field "' + type + '" is already registered.');
}
if (!fieldClass || (typeof fieldClass.fromJson != 'function')) {
throw Error('Field "' + fieldClass + '" must have a fromJson function');
}
type = type.toLowerCase();
Blockly.fieldRegistry.typeMap_[type] = fieldClass;
Blockly.registry.register(Blockly.registry.Type.FIELD, type, fieldClass);
};
/**
@@ -54,12 +38,7 @@ Blockly.fieldRegistry.register = function(type, fieldClass) {
* @param {string} type The field type name as used in the JSON definition.
*/
Blockly.fieldRegistry.unregister = function(type) {
if (Blockly.fieldRegistry.typeMap_[type]) {
delete Blockly.fieldRegistry.typeMap_[type];
} else {
console.warn('No field mapping for type "' + type +
'" found to unregister');
}
Blockly.registry.unregister(Blockly.registry.Type.FIELD, type);
};
/**
@@ -73,8 +52,8 @@ Blockly.fieldRegistry.unregister = function(type) {
* @package
*/
Blockly.fieldRegistry.fromJson = function(options) {
var type = options['type'].toLowerCase();
var fieldClass = Blockly.fieldRegistry.typeMap_[type];
var fieldClass = /** @type {{fromJson:function(!Object):!Blockly.Field}} */ (
Blockly.registry.getClass(Blockly.registry.Type.FIELD, options['type']));
if (!fieldClass) {
console.warn('Blockly could not create a field of type ' + options['type'] +
'. The field is probably not being registered. This could be because' +

154
core/registry.js Normal file
View File

@@ -0,0 +1,154 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview This file is a universal registry that provides generic methods
* for registering and unregistering different types of classes.
* @author aschmiedt@google.com (Abby Schmiedt)
*/
'use strict';
goog.provide('Blockly.registry');
/**
* A map of maps. With the keys being the type and name of the class we are
* registering and the value being the constructor function.
* Ex: {'field': {'field_angle': Blockly.FieldAngle}}
*
* @type {Object<string, Object<string, function(new:?)>>}
*/
Blockly.registry.typeMap_ = {};
/**
* A name with the type of the element stored in the generic.
* @param {string} name The name of the registry type.
* @constructor
* @template T
*/
Blockly.registry.Type = function(name) {
/** @private {string} */
this.name_ = name;
};
/**
* Returns the name of the type.
* @return {string} The name.
* @override
*/
Blockly.registry.Type.prototype.toString = function() {
return this.name_;
};
/** @type {!Blockly.registry.Type<Blockly.blockRendering.Renderer>} */
Blockly.registry.Type.RENDERER = new Blockly.registry.Type('renderer');
/** @type {!Blockly.registry.Type<Blockly.Field>} */
Blockly.registry.Type.FIELD = new Blockly.registry.Type('field');
/**
* Registers a class based on a type and name.
* @param {string|Blockly.registry.Type<T>} type The type of the plugin.
* (Ex: Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @param {?function(new:T, ...?)} registryClass The class to register.
* @throws {Error} if the type or name is empty, a name with the given type has
* already been registered, or if the given class is not valid for it's type.
* @template T
*/
Blockly.registry.register = function(type, name, registryClass) {
if ((!(type instanceof Blockly.registry.Type) && typeof type != 'string') || String(type).trim() == '') {
throw Error('Invalid type "' + type + '". The type must be a' +
' non-empty string or a Blockly.registry.Type.');
}
type = String(type).toLowerCase();
if ((typeof name != 'string') || (name.trim() == '')) {
throw Error('Invalid name "' + name + '". The name must be a' +
' non-empty string.');
}
name = name.toLowerCase();
if (!registryClass) {
throw Error('Can not register a null value');
}
var typeRegistry = Blockly.registry.typeMap_[type];
// If the type registry has not been created, create it.
if (!typeRegistry) {
typeRegistry = Blockly.registry.typeMap_[type] = {};
}
// Validate that the given class has all the required properties.
Blockly.registry.validate_(type, registryClass);
// If the name already exists throw an error.
if (typeRegistry[name]) {
throw Error('Name "' + name + '" with type "' + type + '" already registered.');
}
typeRegistry[name] = registryClass;
};
/**
* Checks the given class for properties that are required based on the type.
* @param {string} type The type of the plugin. (Ex: Field, Renderer)
* @param {Function} registryClass A class that we are checking for the required
* properties.
* @private
*/
Blockly.registry.validate_ = function(type, registryClass) {
switch (type) {
case String(Blockly.registry.Type.FIELD):
if (typeof registryClass['fromJson'] != 'function') {
throw Error('Type "' + type + '" must have a fromJson function');
}
break;
}
};
/**
* Unregisters the class with the given type and name.
* @param {string|Blockly.registry.Type<T>} type The type of the plugin.
* (eg: Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @template T
*/
Blockly.registry.unregister = function(type, name) {
type = String(type).toLowerCase();
name = name.toLowerCase();
var typeRegistry = Blockly.registry.typeMap_[type];
if (!typeRegistry) {
console.warn('No type "' + type + '" found');
return;
}
if (!typeRegistry[name]) {
console.warn('No name "' + name + '" with type "' + type + '" found');
return;
}
delete Blockly.registry.typeMap_[type][name];
};
/**
* Get the class for the given name and type.
* @param {string|Blockly.registry.Type<T>} type The type of the plugin.
* (eg: Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @return {?function(new:T, ...?)} The class with the given name and type or
* null if none exists.
* @template T
*/
Blockly.registry.getClass = function(type, name) {
type = String(type).toLowerCase();
name = name.toLowerCase();
var typeRegistry = Blockly.registry.typeMap_[type];
if (!typeRegistry) {
console.warn('No type "' + type + '" found');
return null;
}
if (!typeRegistry[name]) {
console.warn('No name "' + name + '" with type "' + type + '" found');
return null;
}
return typeRegistry[name];
};

View File

@@ -16,6 +16,7 @@
*/
goog.provide('Blockly.blockRendering');
goog.require('Blockly.registry');
goog.require('Blockly.utils.object');
@@ -41,10 +42,8 @@ Blockly.blockRendering.useDebugger = false;
* @throws {Error} if a renderer with the same name has already been registered.
*/
Blockly.blockRendering.register = function(name, rendererClass) {
if (Blockly.blockRendering.rendererMap_[name]) {
throw Error('Renderer has already been registered.');
}
Blockly.blockRendering.rendererMap_[name] = rendererClass;
Blockly.registry.register(Blockly.registry.Type.RENDERER, name,
rendererClass);
};
/**
@@ -52,14 +51,8 @@ Blockly.blockRendering.register = function(name, rendererClass) {
* @param {string} name The name of the renderer.
*/
Blockly.blockRendering.unregister = function(name) {
if (Blockly.blockRendering.rendererMap_[name]) {
delete Blockly.blockRendering.rendererMap_[name];
} else {
console.warn('No renderer mapping for name "' + name +
'" found to unregister');
}
Blockly.registry.unregister(Blockly.registry.Type.RENDERER, name);
};
/**
* Turn on the blocks debugger.
* @package
@@ -85,12 +78,11 @@ Blockly.blockRendering.stopDebugger = function() {
* Already initialized.
* @package
*/
Blockly.blockRendering.init = function(name, theme, opt_rendererOverrides) {
if (!Blockly.blockRendering.rendererMap_[name]) {
throw Error('Renderer not registered: ', name);
}
var renderer = (/** @type {!Blockly.blockRendering.Renderer} */ (
new Blockly.blockRendering.rendererMap_[name](name)));
var rendererClass = Blockly.registry.getClass(
Blockly.registry.Type.RENDERER, name);
var renderer = new rendererClass(name);
renderer.init(theme, opt_rendererOverrides);
return renderer;
};

View File

@@ -20,46 +20,25 @@ suite('Field Registry', function() {
};
teardown(function() {
if (Blockly.fieldRegistry.typeMap_['field_custom_test']) {
delete Blockly.fieldRegistry.typeMap_['field_custom_test'];
if (Blockly.registry.typeMap_['field']['field_custom_test']) {
delete Blockly.registry.typeMap_['field']['field_custom_test'];
}
});
suite('Registration', function() {
test('Simple', function() {
Blockly.fieldRegistry.register('field_custom_test', CustomFieldType);
});
test('Empty String Key', function() {
chai.assert.throws(function() {
Blockly.fieldRegistry.register('', CustomFieldType);
}, 'Invalid field type');
});
test('Class as Key', function() {
chai.assert.throws(function() {
Blockly.fieldRegistry.register(CustomFieldType, '');
}, 'Invalid field type');
});
test('fromJson as Key', function() {
chai.assert.throws(function() {
Blockly.fieldRegistry.register(CustomFieldType.fromJson, '');
}, 'Invalid field type');
});
test('Overwrite a Key', function() {
Blockly.fieldRegistry.register('field_custom_test', CustomFieldType);
chai.assert.throws(function() {
Blockly.fieldRegistry.register('field_custom_test', CustomFieldType);
}, 'already registered');
});
test('Null Value', function() {
chai.assert.throws(function() {
Blockly.fieldRegistry.register('field_custom_test', null);
}, 'fromJson function');
}, 'Invalid name');
});
test('No fromJson', function() {
var fromJson = CustomFieldType.fromJson;
delete CustomFieldType.fromJson;
chai.assert.throws(function() {
Blockly.fieldRegistry.register('field_custom_test', CustomFieldType);
}, 'fromJson function');
}, 'must have a fromJson function');
CustomFieldType.fromJson = fromJson;
});
test('fromJson not a function', function() {
@@ -67,7 +46,7 @@ suite('Field Registry', function() {
CustomFieldType.fromJson = true;
chai.assert.throws(function() {
Blockly.fieldRegistry.register('field_custom_test', CustomFieldType);
}, 'fromJson function');
}, 'must have a fromJson function');
CustomFieldType.fromJson = fromJson;
});
});

View File

@@ -70,6 +70,7 @@
<script src="navigation_modify_test.js"></script>
<script src="navigation_test.js"></script>
<script src="procedures_test.js"></script>
<script src="registry_test.js"></script>
<script src="theme_test.js"></script>
<script src="toolbox_test.js"></script>
<script src="trashcan_test.js"></script>

View File

@@ -0,0 +1,51 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Tests for Blockly.registry
* @author aschmiedt@google.com (Abby Schmiedt)
*/
'use strict';
suite('Registry', function() {
var TestClass = function() {};
TestClass.prototype.testMethod = function() {
return 'something';
};
teardown(function() {
if (Blockly.registry.typeMap_['test'] &&
Blockly.registry.typeMap_['test']['test_name']) {
delete Blockly.registry.typeMap_['test']['test_name'];
}
});
suite('Registration', function() {
test('Simple', function() {
Blockly.registry.register('test', 'test_name', TestClass);
});
test('Empty String Key', function() {
chai.assert.throws(function() {
Blockly.registry.register('test', '', TestClass);
}, 'Invalid name');
});
test('Class as Key', function() {
chai.assert.throws(function() {
Blockly.registry.register('test', TestClass, '');
}, 'Invalid name');
});
test('Overwrite a Key', function() {
Blockly.registry.register('test', 'test_name', TestClass);
chai.assert.throws(function() {
Blockly.registry.register('test', 'test_name', TestClass);
}, 'already registered');
});
test('Null Value', function() {
chai.assert.throws(function() {
Blockly.registry.register('test', 'field_custom_test', null);
}, 'Can not register a null value');
});
});
});