diff --git a/src/extension-support/argument-type.js b/src/extension-support/argument-type.js index 0589e9fb6..538a04f5d 100644 --- a/src/extension-support/argument-type.js +++ b/src/extension-support/argument-type.js @@ -1,8 +1,31 @@ +/** + * Block argument types + * @enum {string} + */ const ArgumentType = { + /** + * Numeric value with angle picker + */ ANGLE: 'angle', + + /** + * Boolean value with hexagonal placeholder + */ BOOLEAN: 'Boolean', + + /** + * Numeric value with color picker + */ COLOR: 'color', + + /** + * Numeric value with text field + */ NUMBER: 'number', + + /** + * String value with text field + */ STRING: 'string' }; diff --git a/src/extension-support/block-type.js b/src/extension-support/block-type.js index 587f7d598..9a87b745f 100644 --- a/src/extension-support/block-type.js +++ b/src/extension-support/block-type.js @@ -1,8 +1,31 @@ +/** + * Types of block + * @enum {string} + */ const BlockType = { + /** + * Boolean reporter with hexagonal shape + */ BOOLEAN: 'Boolean', + + /** + * Command block + */ COMMAND: 'command', + + /** + * Specialized command block which may or may not run a child branch + */ CONDITIONAL: 'conditional', + + /** + * Hat block which conditionally starts a block stack + */ HAT: 'hat', + + /** + * General reporter with numeric or string value + */ REPORTER: 'reporter' }; diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index d49be91cf..bf988adb9 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -246,7 +246,7 @@ class ExtensionManager { extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus); return extensionInfo; } - + /** * Prepare extension menus. e.g. setup binding for dynamic menu functions. * @param {string} serviceName - the name of the service hosting this extension block @@ -264,13 +264,36 @@ class ExtensionManager { // we can use it later when converting the menu for Scratch Blocks. if (typeof menus[item] === 'string') { const serviceObject = dispatch.services[serviceName]; - // TODO: Fix this to use dispatch.call when extensions are running in workers. - menus[item] = serviceObject[menus[item]].bind(serviceObject); + const menuName = menus[item]; + menus[item] = this._getExtensionMenuItems.bind(this, serviceObject, menuName); } } return menus; } + /** + * Fetch the items for a particular extension menu, providing the target ID for context. + * @param {object} extensionObject - the extension object providing the menu. + * @param {string} menuName - the name of the menu function to call. + * @returns {Array} menu items ready for scratch-blocks. + * @private + */ + _getExtensionMenuItems (extensionObject, menuName) { + // Fetch the items appropriate for the target currently being edited. This assumes that menus only + // collect items when opened by the user while editing a particular target. + const editingTarget = this.runtime.getEditingTarget(); + const editingTargetID = editingTarget ? editingTarget.id : null; + + // TODO: Fix this to use dispatch.call when extensions are running in workers. + const menuFunc = extensionObject[menuName]; + const menuItems = menuFunc.call(extensionObject, editingTargetID); + + if (!menuItems || menuItems.length < 1) { + throw new Error(`Extension menu returned no items: ${menuName}`); + } + return menuItems; + } + /** * Apply defaults for optional block fields. * @param {string} serviceName - the name of the service hosting this extension block @@ -297,7 +320,12 @@ class ExtensionManager { blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func); } else { const serviceObject = dispatch.services[serviceName]; - blockInfo.func = serviceObject[blockInfo.func].bind(serviceObject); + const func = serviceObject[blockInfo.func]; + if (func) { + blockInfo.func = func.bind(serviceObject); + } else { + throw new Error(`Could not find extension block function called ${blockInfo.func}`); + } } return blockInfo; } diff --git a/src/extension-support/extension-metadata.js b/src/extension-support/extension-metadata.js new file mode 100644 index 000000000..81dbe5bf6 --- /dev/null +++ b/src/extension-support/extension-metadata.js @@ -0,0 +1,60 @@ +/** + * @typedef {object} ExtensionMetadata + * All the metadata needed to register an extension. + * @property {string} id - a unique alphanumeric identifier for this extension. No special characters allowed. + * @property {string} name - the human-readable name of this extension. + * @property {string} blockIconURI - URI for an image to be placed on each block in this extension. Data URI ok. + * @property {string} menuIconURI - URI for an image to be placed on this extension's category menu entry. Data URI ok. + * @property {string} docsURI - link to documentation content for this extension. + * @property {Array.} - the blocks provided by this extension, with optional separators. + * @property {Object.} - map of menu name to metadata about each of this extension's menus. + */ + +/** + * @typedef {object} ExtensionBlockMetadata + * All the metadata needed to register an extension block. + * @property {string} opcode - a unique alphanumeric identifier for this block. No special characters allowed. + * @property {BlockType} blockType - the type of block (command, reporter, etc.) being described. + * @property {string} func - the name of the function implementing this block. Can be shared with other blocks/opcodes. + * @property {Boolean} hideFromPalette - true if this block should not appear in the block palette. + * @property {ReporterScope} reporterScope - if this block is a reporter, this is the scope/context for its value. + * @property {Boolean} terminal - true if the block ends a stack - no blocks can be connected after it. + * @property {string} text - the text on the block, with [PLACEHOLDERS] for arguments. + * @property {Object.} arguments - map of argument placeholder to metadata about each arg. + */ + +/** + * @typedef {object} ExtensionArgumentMetadata + * All the metadata needed to register an argument for an extension block. + * @property {ArgumentType} type - the type of the argument (number, string, etc.) + * @property {*} defaultValue - the default value of this argument. + * @property {string} menu - the name of the menu to use for this argument, if any. + */ + +/** + * @typedef {ExtensionDynamicMenu|ExtensionMenuItems} ExtensionMenuMetadata + * All the metadata needed to register an extension drop-down menu. + */ + +/** + * @typedef {string} ExtensionDynamicMenu + * The string name of a function which returns menu items. + * @see {ExtensionMenuItems} - the type of data expected to be returned by the specified function. + */ + +/** + * @typedef {Array.} ExtensionMenuItems + * Items in an extension menu. + */ + +/** + * @typedef {string} ExtensionMenuItemSimple + * A menu item for which the label and value are identical strings. + */ + +/** + * @typedef {object} ExtensionMenuItemComplex + * A menu item for which the label and value can differ. + * @property {*} value - the value of the block argument when this menu item is selected. + * @property {string} text - the human-readable label of this menu item in the menu. + */ diff --git a/src/extension-support/reporter-scope.js b/src/extension-support/reporter-scope.js new file mode 100644 index 000000000..6157d1c5f --- /dev/null +++ b/src/extension-support/reporter-scope.js @@ -0,0 +1,18 @@ +/** + * Indicate the scope for a reporter's value. + * @enum {string} + */ +const ReporterScope = { + /** + * This reporter's value is global and does not depend on context. + */ + GLOBAL: 'global', + + /** + * This reporter's value is specific to a particular target/sprite. + * Another target may have a different value or may not even have a value. + */ + TARGET: 'target' +}; + +module.exports = ReporterScope;