Files
blockly/core/registry.js
Aaron Dodson 3851b14627 refactor: Migrate to named exports (#5623)
* refactor: Migrate to named exports

* fix: Sort requires

* fix: Remove duplicate deps
2021-10-20 15:53:23 -07:00

382 lines
12 KiB
JavaScript

/**
* @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.
*/
'use strict';
/**
* This file is a universal registry that provides generic methods
* for registering and unregistering different types of classes.
* @namespace Blockly.registry
*/
goog.module('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const Abstract = goog.requireType('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {Cursor} = goog.requireType('Blockly.Cursor');
/* eslint-disable-next-line no-unused-vars */
const {Field} = goog.requireType('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {IBlockDragger} = goog.requireType('Blockly.IBlockDragger');
/* eslint-disable-next-line no-unused-vars */
const {IConnectionChecker} = goog.requireType('Blockly.IConnectionChecker');
/* eslint-disable-next-line no-unused-vars */
const {IFlyout} = goog.requireType('Blockly.IFlyout');
/* eslint-disable-next-line no-unused-vars */
const {IMetricsManager} = goog.requireType('Blockly.IMetricsManager');
/* eslint-disable-next-line no-unused-vars */
const {ISerializer} = goog.requireType('Blockly.serialization.ISerializer');
/* eslint-disable-next-line no-unused-vars */
const {IToolbox} = goog.requireType('Blockly.IToolbox');
/* eslint-disable-next-line no-unused-vars */
const {Options} = goog.requireType('Blockly.Options');
/* eslint-disable-next-line no-unused-vars */
const {Renderer} = goog.requireType('Blockly.blockRendering.Renderer');
/* eslint-disable-next-line no-unused-vars */
const {Theme} = goog.requireType('Blockly.Theme');
/* eslint-disable-next-line no-unused-vars */
const {ToolboxItem} = goog.requireType('Blockly.ToolboxItem');
/**
* 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.
* e.g. {'field': {'field_angle': Blockly.FieldAngle}}
*
* @type {!Object<string, !Object<string, (function(new:?)|!Object)>>}
*/
const typeMap = Object.create(null);
exports.TEST_ONLY = {typeMap};
/**
* A map of maps. With the keys being the type and caseless name of the class we
* are registring, and the value being the most recent cased name for that
* registration.
* @type {!Object<string, !Object<string, string>>}
*/
const nameMap = Object.create(null);
/**
* The string used to register the default class for a type of plugin.
* @type {string}
* @alias Blockly.registry.DEFAULT
*/
const DEFAULT = 'default';
exports.DEFAULT = DEFAULT;
/**
* A name with the type of the element stored in the generic.
* @param {string} name The name of the registry type.
* @constructor
* @template T
* @alias Blockly.registry.Type
*/
const Type = function(name) {
/**
* @type {string}
* @private
*/
this.name_ = name;
};
exports.Type = Type;
/**
* Returns the name of the type.
* @return {string} The name.
* @override
*/
Type.prototype.toString = function() {
return this.name_;
};
/** @type {!Type<IConnectionChecker>} */
Type.CONNECTION_CHECKER = new Type('connectionChecker');
/** @type {!Type<Cursor>} */
Type.CURSOR = new Type('cursor');
/** @type {!Type<Abstract>} */
Type.EVENT = new Type('event');
/** @type {!Type<Field>} */
Type.FIELD = new Type('field');
/** @type {!Type<Renderer>} */
Type.RENDERER = new Type('renderer');
/** @type {!Type<IToolbox>} */
Type.TOOLBOX = new Type('toolbox');
/** @type {!Type<Theme>} */
Type.THEME = new Type('theme');
/** @type {!Type<ToolboxItem>} */
Type.TOOLBOX_ITEM = new Type('toolboxItem');
/** @type {!Type<IFlyout>} */
Type.FLYOUTS_VERTICAL_TOOLBOX = new Type('flyoutsVerticalToolbox');
/** @type {!Type<IFlyout>} */
Type.FLYOUTS_HORIZONTAL_TOOLBOX = new Type('flyoutsHorizontalToolbox');
/** @type {!Type<IMetricsManager>} */
Type.METRICS_MANAGER = new Type('metricsManager');
/** @type {!Type<IBlockDragger>} */
Type.BLOCK_DRAGGER = new Type('blockDragger');
/**
* @type {!Type<ISerializer>}
* @package
*/
Type.SERIALIZER = new Type('serializer');
/**
* Registers a class based on a type and name.
* @param {string|!Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @param {?function(new:T, ...?)|Object} registryItem The class or object to
* register.
* @param {boolean=} opt_allowOverrides True to prevent an error when overriding
* an already registered item.
* @throws {Error} if the type or name is empty, a name with the given type has
* already been registered, or if the given class or object is not valid for
* its type.
* @template T
* @alias Blockly.registry.register
*/
const register = function(type, name, registryItem, opt_allowOverrides) {
if ((!(type instanceof 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.');
}
const caselessName = name.toLowerCase();
if (!registryItem) {
throw Error('Can not register a null value');
}
let typeRegistry = typeMap[type];
let nameRegistry = nameMap[type];
// If the type registry has not been created, create it.
if (!typeRegistry) {
typeRegistry = typeMap[type] = Object.create(null);
nameRegistry = nameMap[type] = Object.create(null);
}
// Validate that the given class has all the required properties.
validate(type, registryItem);
// Don't throw an error if opt_allowOverrides is true.
if (!opt_allowOverrides && typeRegistry[caselessName]) {
throw Error(
'Name "' + caselessName + '" with type "' + type +
'" already registered.');
}
typeRegistry[caselessName] = registryItem;
nameRegistry[caselessName] = name;
};
exports.register = register;
/**
* Checks the given registry item for properties that are required based on the
* type.
* @param {string} type The type of the plugin. (e.g. Field, Renderer)
* @param {Function|Object} registryItem A class or object that we are checking
* for the required properties.
*/
const validate = function(type, registryItem) {
switch (type) {
case String(Type.FIELD):
if (typeof registryItem.fromJson !== 'function') {
throw Error('Type "' + type + '" must have a fromJson function');
}
break;
}
};
/**
* Unregisters the registry item with the given type and name.
* @param {string|!Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @template T
* @alias Blockly.registry.unregister
*/
const unregister = function(type, name) {
type = String(type).toLowerCase();
name = name.toLowerCase();
const typeRegistry = typeMap[type];
if (!typeRegistry || !typeRegistry[name]) {
console.warn(
'Unable to unregister [' + name + '][' + type + '] from the ' +
'registry.');
return;
}
delete typeMap[type][name];
delete nameMap[type][name];
};
exports.unregister = unregister;
/**
* Gets the registry item for the given name and type. This can be either a
* class or an object.
* @param {string|!Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @param {boolean=} opt_throwIfMissing Whether or not to throw an error if we
* are unable to find the plugin.
* @return {?function(new:T, ...?)|Object} The class or object with the given
* name and type or null if none exists.
* @template T
*/
const getItem = function(type, name, opt_throwIfMissing) {
type = String(type).toLowerCase();
name = name.toLowerCase();
const typeRegistry = typeMap[type];
if (!typeRegistry || !typeRegistry[name]) {
const msg = 'Unable to find [' + name + '][' + type + '] in the registry.';
if (opt_throwIfMissing) {
throw new Error(
msg + ' You must require or register a ' + type + ' plugin.');
} else {
console.warn(msg);
}
return null;
}
return typeRegistry[name];
};
/**
* Returns whether or not the registry contains an item with the given type and
* name.
* @param {string|!Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @return {boolean} True if the registry has an item with the given type and
* name, false otherwise.
* @template T
* @alias Blockly.registry.hasItem
*/
const hasItem = function(type, name) {
type = String(type).toLowerCase();
name = name.toLowerCase();
const typeRegistry = typeMap[type];
if (!typeRegistry) {
return false;
}
return !!(typeRegistry[name]);
};
exports.hasItem = hasItem;
/**
* Gets the class for the given name and type.
* @param {string|!Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @param {boolean=} opt_throwIfMissing Whether or not to throw an error if we
* are unable to find the plugin.
* @return {?function(new:T, ...?)} The class with the given name and type or
* null if none exists.
* @template T
* @alias Blockly.registry.getClass
*/
const getClass = function(type, name, opt_throwIfMissing) {
return /** @type {?function(new:T, ...?)} */ (
getItem(type, name, opt_throwIfMissing));
};
exports.getClass = getClass;
/**
* Gets the object for the given name and type.
* @param {string|!Type<T>} type The type of the plugin.
* (e.g. Category)
* @param {string} name The plugin's name. (Ex. logic_category)
* @param {boolean=} opt_throwIfMissing Whether or not to throw an error if we
* are unable to find the object.
* @return {?T} The object with the given name and type or null if none exists.
* @template T
* @alias Blockly.registry.getObject
*/
const getObject = function(type, name, opt_throwIfMissing) {
return /** @type {T} */ (getItem(type, name, opt_throwIfMissing));
};
exports.getObject = getObject;
/**
* Returns a map of items registered with the given type.
* @param {string|!Type<T>} type The type of the plugin. (e.g. Category)
* @param {boolean} opt_cased Whether or not to return a map with cased keys
* (rather than caseless keys). False by default.
* @param {boolean=} opt_throwIfMissing Whether or not to throw an error if we
* are unable to find the object. False by default.
* @return {?Object<string, ?T|?function(new:T, ...?)>} A map of objects with
* the given type, or null if none exists.
* @template T
* @alias Blockly.registry.getAllItems
*/
const getAllItems = function(type, opt_cased, opt_throwIfMissing) {
type = String(type).toLowerCase();
const typeRegistry = typeMap[type];
if (!typeRegistry) {
const msg = `Unable to find [${type}] in the registry.`;
if (opt_throwIfMissing) {
throw new Error(`${msg} You must require or register a ${type} plugin.`);
} else {
console.warn(msg);
}
return null;
}
if (!opt_cased) {
return typeRegistry;
}
const nameRegistry = nameMap[type];
const casedRegistry = Object.create(null);
const keys = Object.keys(typeRegistry);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
casedRegistry[nameRegistry[key]] = typeRegistry[key];
}
return casedRegistry;
};
exports.getAllItems = getAllItems;
/**
* Gets the class from Blockly options for the given type.
* This is used for plugins that override a built in feature. (e.g. Toolbox)
* @param {!Type<T>} type The type of the plugin.
* @param {!Options} options The option object to check for the given
* plugin.
* @param {boolean=} opt_throwIfMissing Whether or not to throw an error if we
* are unable to find the plugin.
* @return {?function(new:T, ...?)} The class for the plugin.
* @template T
* @alias Blockly.registry.getClassFromOptions
*/
const getClassFromOptions = function(type, options, opt_throwIfMissing) {
const typeName = type.toString();
const plugin = options.plugins[typeName] || DEFAULT;
// If the user passed in a plugin class instead of a registered plugin name.
if (typeof plugin === 'function') {
return plugin;
}
return getClass(type, plugin, opt_throwIfMissing);
};
exports.getClassFromOptions = getClassFromOptions;