diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index 6ea018add..25c8984d0 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -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'], {}); diff --git a/core/field_registry.js b/core/field_registry.js index 5e6b7bde0..b7f8441b3 100644 --- a/core/field_registry.js +++ b/core/field_registry.js @@ -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} - * @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' + diff --git a/core/registry.js b/core/registry.js new file mode 100644 index 000000000..cda739079 --- /dev/null +++ b/core/registry.js @@ -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>} + */ +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.registry.Type.RENDERER = new Blockly.registry.Type('renderer'); + +/** @type {!Blockly.registry.Type} */ +Blockly.registry.Type.FIELD = new Blockly.registry.Type('field'); + +/** + * Registers a class based on a type and name. + * @param {string|Blockly.registry.Type} 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} 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} 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]; +}; diff --git a/core/renderers/common/block_rendering.js b/core/renderers/common/block_rendering.js index 8a7757cd1..83cbf9d72 100644 --- a/core/renderers/common/block_rendering.js +++ b/core/renderers/common/block_rendering.js @@ -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; }; diff --git a/tests/mocha/field_registry_test.js b/tests/mocha/field_registry_test.js index c96cd13c8..eb0d3491e 100644 --- a/tests/mocha/field_registry_test.js +++ b/tests/mocha/field_registry_test.js @@ -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; }); }); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 3e238977d..65eb54f0c 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -70,6 +70,7 @@ + diff --git a/tests/mocha/registry_test.js b/tests/mocha/registry_test.js new file mode 100644 index 000000000..b3f2d8a4e --- /dev/null +++ b/tests/mocha/registry_test.js @@ -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'); + }); + }); +});