mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10:12 +01:00
401 lines
12 KiB
TypeScript
401 lines
12 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2020 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
// Former goog.module ID: Blockly.registry
|
|
|
|
import type {Abstract} from './events/events_abstract.js';
|
|
import type {Field} from './field.js';
|
|
import type {Input} from './inputs/input.js';
|
|
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
|
import type {IConnectionPreviewer} from './interfaces/i_connection_previewer.js';
|
|
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
|
|
import type {IDragger} from './interfaces/i_dragger.js';
|
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
|
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
|
import type {IIcon} from './interfaces/i_icon.js';
|
|
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
|
import type {IPaster} from './interfaces/i_paster.js';
|
|
import type {ISerializer} from './interfaces/i_serializer.js';
|
|
import type {IToolbox} from './interfaces/i_toolbox.js';
|
|
import type {IVariableMap} from './interfaces/i_variable_map.js';
|
|
import type {
|
|
IVariableModel,
|
|
IVariableModelStatic,
|
|
IVariableState,
|
|
} from './interfaces/i_variable_model.js';
|
|
import type {Cursor} from './keyboard_nav/cursor.js';
|
|
import type {Options} from './options.js';
|
|
import type {Renderer} from './renderers/common/renderer.js';
|
|
import type {Theme} from './theme.js';
|
|
import type {ToolboxItem} from './toolbox/toolbox_item.js';
|
|
|
|
/**
|
|
* 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}}
|
|
*/
|
|
const typeMap: {
|
|
[key: string]: {
|
|
[key: string]: (new () => AnyDuringMigration) | AnyDuringMigration;
|
|
};
|
|
} = Object.create(null);
|
|
export const 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.
|
|
*/
|
|
const nameMap: {[key: string]: {[key: string]: string}} = Object.create(null);
|
|
|
|
/**
|
|
* The string used to register the default class for a type of plugin.
|
|
*/
|
|
export const DEFAULT = 'default';
|
|
|
|
/**
|
|
* A name with the type of the element stored in the generic.
|
|
*/
|
|
export class Type<_T> {
|
|
/** @param name The name of the registry type. */
|
|
constructor(private readonly name: string) {}
|
|
|
|
/**
|
|
* Returns the name of the type.
|
|
*
|
|
* @returns The name.
|
|
*/
|
|
toString(): string {
|
|
return this.name;
|
|
}
|
|
|
|
static CONNECTION_CHECKER = new Type<IConnectionChecker>('connectionChecker');
|
|
|
|
static CONNECTION_PREVIEWER = new Type<IConnectionPreviewer>(
|
|
'connectionPreviewer',
|
|
);
|
|
|
|
static CURSOR = new Type<Cursor>('cursor');
|
|
|
|
static EVENT = new Type<Abstract>('event');
|
|
|
|
static FIELD = new Type<Field>('field');
|
|
|
|
static INPUT = new Type<Input>('input');
|
|
|
|
static RENDERER = new Type<Renderer>('renderer');
|
|
|
|
static TOOLBOX = new Type<IToolbox>('toolbox');
|
|
|
|
static THEME = new Type<Theme>('theme');
|
|
|
|
static TOOLBOX_ITEM = new Type<ToolboxItem>('toolboxItem');
|
|
|
|
static FLYOUTS_VERTICAL_TOOLBOX = new Type<IFlyout>('flyoutsVerticalToolbox');
|
|
|
|
static FLYOUTS_HORIZONTAL_TOOLBOX = new Type<IFlyout>(
|
|
'flyoutsHorizontalToolbox',
|
|
);
|
|
|
|
static FLYOUT_INFLATER = new Type<IFlyoutInflater>('flyoutInflater');
|
|
|
|
static METRICS_MANAGER = new Type<IMetricsManager>('metricsManager');
|
|
|
|
/**
|
|
* Type for an IDragger. Formerly behavior was mostly covered by
|
|
* BlockDraggeers, which is why the name is inaccurate.
|
|
*/
|
|
static BLOCK_DRAGGER = new Type<IDragger>('blockDragger');
|
|
|
|
/** @internal */
|
|
static SERIALIZER = new Type<ISerializer>('serializer');
|
|
|
|
/** @internal */
|
|
static ICON = new Type<IIcon>('icon');
|
|
|
|
/** @internal */
|
|
static PASTER = new Type<IPaster<ICopyData, ICopyable<ICopyData>>>('paster');
|
|
|
|
static VARIABLE_MODEL = new Type<IVariableModelStatic<IVariableState>>(
|
|
'variableModel',
|
|
);
|
|
|
|
static VARIABLE_MAP = new Type<IVariableMap<IVariableModel<IVariableState>>>(
|
|
'variableMap',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Registers a class based on a type and name.
|
|
*
|
|
* @param type The type of the plugin.
|
|
* (e.g. Field, Renderer)
|
|
* @param name The plugin's name. (Ex. field_angle, geras)
|
|
* @param registryItem The class or object to register.
|
|
* @param 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.
|
|
*/
|
|
export function register<T>(
|
|
type: string | Type<T>,
|
|
name: string,
|
|
registryItem:
|
|
| (new (...p1: AnyDuringMigration[]) => T)
|
|
| null
|
|
| AnyDuringMigration,
|
|
opt_allowOverrides?: boolean,
|
|
): void {
|
|
if (
|
|
(!(type instanceof Type) && typeof type !== 'string') ||
|
|
`${type}`.trim() === ''
|
|
) {
|
|
throw Error(
|
|
'Invalid type "' +
|
|
type +
|
|
'". The type must be a' +
|
|
' non-empty string or a Blockly.registry.Type.',
|
|
);
|
|
}
|
|
type = `${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,
|
|
// or if we're trying to register the same item.
|
|
if (
|
|
!opt_allowOverrides &&
|
|
typeRegistry[caselessName] &&
|
|
typeRegistry[caselessName] !== registryItem
|
|
) {
|
|
throw Error(
|
|
'Name "' +
|
|
caselessName +
|
|
'" with type "' +
|
|
type +
|
|
'" already registered.',
|
|
);
|
|
}
|
|
typeRegistry[caselessName] = registryItem;
|
|
nameRegistry[caselessName] = name;
|
|
}
|
|
|
|
/**
|
|
* Checks the given registry item for properties that are required based on the
|
|
* type.
|
|
*
|
|
* @param type The type of the plugin. (e.g. Field, Renderer)
|
|
* @param registryItem A class or object that we are checking for the required
|
|
* properties.
|
|
*/
|
|
function validate(type: string, registryItem: AnyDuringMigration) {
|
|
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 type The type of the plugin.
|
|
* (e.g. Field, Renderer)
|
|
* @param name The plugin's name. (Ex. field_angle, geras)
|
|
*/
|
|
export function unregister<T>(type: string | Type<T>, name: string) {
|
|
type = `${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];
|
|
}
|
|
|
|
/**
|
|
* Gets the registry item for the given name and type. This can be either a
|
|
* class or an object.
|
|
*
|
|
* @param type The type of the plugin.
|
|
* (e.g. Field, Renderer)
|
|
* @param name The plugin's name. (Ex. field_angle, geras)
|
|
* @param opt_throwIfMissing Whether or not to throw an error if we are unable
|
|
* to find the plugin.
|
|
* @returns The class or object with the given name and type or null if none
|
|
* exists.
|
|
*/
|
|
function getItem<T>(
|
|
type: string | Type<T>,
|
|
name: string,
|
|
opt_throwIfMissing?: boolean,
|
|
): (new (...p1: AnyDuringMigration[]) => T) | null | AnyDuringMigration {
|
|
type = `${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 type The type of the plugin.
|
|
* (e.g. Field, Renderer)
|
|
* @param name The plugin's name. (Ex. field_angle, geras)
|
|
* @returns True if the registry has an item with the given type and name, false
|
|
* otherwise.
|
|
*/
|
|
export function hasItem<T>(type: string | Type<T>, name: string): boolean {
|
|
type = `${type}`.toLowerCase();
|
|
name = name.toLowerCase();
|
|
const typeRegistry = typeMap[type];
|
|
if (!typeRegistry) {
|
|
return false;
|
|
}
|
|
return !!typeRegistry[name];
|
|
}
|
|
|
|
/**
|
|
* Gets the class for the given name and type.
|
|
*
|
|
* @param type The type of the plugin.
|
|
* (e.g. Field, Renderer)
|
|
* @param name The plugin's name. (Ex. field_angle, geras)
|
|
* @param opt_throwIfMissing Whether or not to throw an error if we are unable
|
|
* to find the plugin.
|
|
* @returns The class with the given name and type or null if none exists.
|
|
*/
|
|
export function getClass<T>(
|
|
type: string | Type<T>,
|
|
name: string,
|
|
opt_throwIfMissing?: boolean,
|
|
): (new (...p1: AnyDuringMigration[]) => T) | null {
|
|
return getItem(type, name, opt_throwIfMissing) as
|
|
| (new (...p1: AnyDuringMigration[]) => T)
|
|
| null;
|
|
}
|
|
|
|
/**
|
|
* Gets the object for the given name and type.
|
|
*
|
|
* @param type The type of the plugin.
|
|
* (e.g. Category)
|
|
* @param name The plugin's name. (Ex. logic_category)
|
|
* @param opt_throwIfMissing Whether or not to throw an error if we are unable
|
|
* to find the object.
|
|
* @returns The object with the given name and type or null if none exists.
|
|
*/
|
|
export function getObject<T>(
|
|
type: string | Type<T>,
|
|
name: string,
|
|
opt_throwIfMissing?: boolean,
|
|
): T | null {
|
|
return getItem(type, name, opt_throwIfMissing) as T;
|
|
}
|
|
|
|
/**
|
|
* Returns a map of items registered with the given type.
|
|
*
|
|
* @param type The type of the plugin. (e.g. Category)
|
|
* @param opt_cased Whether or not to return a map with cased keys (rather than
|
|
* caseless keys). False by default.
|
|
* @param opt_throwIfMissing Whether or not to throw an error if we are unable
|
|
* to find the object. False by default.
|
|
* @returns A map of objects with the given type, or null if none exists.
|
|
*/
|
|
export function getAllItems<T>(
|
|
type: string | Type<T>,
|
|
opt_cased?: boolean,
|
|
opt_throwIfMissing?: boolean,
|
|
): {[key: string]: T | null | (new (...p1: AnyDuringMigration[]) => T)} | null {
|
|
type = `${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);
|
|
for (const key of Object.keys(typeRegistry)) {
|
|
casedRegistry[nameRegistry[key]] = typeRegistry[key];
|
|
}
|
|
return casedRegistry;
|
|
}
|
|
|
|
/**
|
|
* 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 The type of the plugin.
|
|
* @param options The option object to check for the given plugin.
|
|
* @param opt_throwIfMissing Whether or not to throw an error if we are unable
|
|
* to find the plugin.
|
|
* @returns The class for the plugin.
|
|
*/
|
|
export function getClassFromOptions<T>(
|
|
type: Type<T>,
|
|
options: Options,
|
|
opt_throwIfMissing?: boolean,
|
|
): (new (...p1: AnyDuringMigration[]) => T) | null {
|
|
const plugin = options.plugins[String(type)] || 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);
|
|
}
|