From ec712618132d379e6352e38d4ce68c5edccc32a4 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Wed, 21 Jun 2023 21:51:14 +0100 Subject: [PATCH] refactor(blocks): Migrate `blocks/procedures.js` to TypeScript (#7192) * refactor(blocks): Auto-migration of blocks/procedures.js to ts * fix(blocks): Revert obvious automigration fails in blocks/procedures.js * fix(blocks): Manually migrate & fix types in procecures.ts * fix(blocks): Remove lint * chore(blocks): Format * fix(7192): Corrections for PR #7192 --- blocks/{procedures.js => procedures.ts} | 738 ++++++++++++++---------- 1 file changed, 435 insertions(+), 303 deletions(-) rename blocks/{procedures.js => procedures.ts} (64%) diff --git a/blocks/procedures.js b/blocks/procedures.ts similarity index 64% rename from blocks/procedures.js rename to blocks/procedures.ts index 505c80736..58095c81d 100644 --- a/blocks/procedures.js +++ b/blocks/procedures.ts @@ -4,51 +4,62 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * @fileoverview Procedure blocks for Blockly. - * @suppress {checkTypes|visibility} - */ -'use strict'; +import * as goog from '../closure/goog/goog.js'; +goog.declareModuleId('Blockly.libraryBlocks.procedures'); -goog.module('Blockly.libraryBlocks.procedures'); +import * as ContextMenu from '../core/contextmenu.js'; +import * as Events from '../core/events/events.js'; +import * as Procedures from '../core/procedures.js'; +import * as Variables from '../core/variables.js'; +import * as Xml from '../core/xml.js'; +import * as fieldRegistry from '../core/field_registry.js'; +import * as xmlUtils from '../core/utils/xml.js'; +import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js'; +import {Align} from '../core/inputs/input.js'; +import type {Block} from '../core/block.js'; +import type {BlockSvg} from '../core/block_svg.js'; +import type {BlockCreate} from '../core/events/events_block_create.js'; +import type {BlockChange} from '../core/events/events_block_change.js'; +import type {BlockDefinition} from '../core/blocks.js'; +import type {Connection} from '../core/connection.js'; +import type { + ContextMenuOption, + LegacyContextMenuOption, +} from '../core/contextmenu_registry.js'; +import {FieldCheckbox} from '../core/field_checkbox.js'; +import {FieldLabel} from '../core/field_label.js'; +import {FieldTextInput} from '../core/field_textinput.js'; +import {Msg} from '../core/msg.js'; +import {MutatorIcon as Mutator} from '../core/icons/mutator_icon.js'; +import {Names} from '../core/names.js'; +import type {VariableModel} from '../core/variable_model.js'; +import type {Workspace} from '../core/workspace.js'; +import type {WorkspaceSvg} from '../core/workspace_svg.js'; +import {config} from '../core/config.js'; +import {defineBlocks} from '../core/common.js'; +import '../core/icons/comment_icon.js'; +import '../core/icons/warning_icon.js'; -/* eslint-disable-next-line no-unused-vars */ -const AbstractEvent = goog.requireType('Blockly.Events.Abstract'); -const ContextMenu = goog.require('Blockly.ContextMenu'); -const Events = goog.require('Blockly.Events'); -const Procedures = goog.require('Blockly.Procedures'); -const Variables = goog.require('Blockly.Variables'); -const Xml = goog.require('Blockly.Xml'); -const fieldRegistry = goog.require('Blockly.fieldRegistry'); -const xmlUtils = goog.require('Blockly.utils.xml'); -const {Align} = goog.require('Blockly.Input'); -/* eslint-disable-next-line no-unused-vars */ -const {Block} = goog.requireType('Blockly.Block'); -// const {BlockDefinition} = goog.requireType('Blockly.blocks'); -// TODO (6248): Properly import the BlockDefinition type. -/* eslint-disable-next-line no-unused-vars */ -const BlockDefinition = Object; -const {config} = goog.require('Blockly.config'); -const {Msg} = goog.require('Blockly.Msg'); -const {MutatorIcon: Mutator} = goog.require('Blockly.Mutator'); -const {Names} = goog.require('Blockly.Names'); -/* eslint-disable-next-line no-unused-vars */ -const {VariableModel} = goog.requireType('Blockly.VariableModel'); -/* eslint-disable-next-line no-unused-vars */ -const {Workspace} = goog.requireType('Blockly.Workspace'); -const {defineBlocks} = goog.require('Blockly.common'); -/** @suppress {extraRequire} */ -goog.require('Blockly.Comment'); -/** @suppress {extraRequire} */ -goog.require('Blockly.Warning'); +/** A dictionary of the block definitions provided by this module. */ +export const blocks: {[key: string]: BlockDefinition} = {}; +/** Type of a block using the PROCEDURE_DEF_COMMON mixin. */ +type ProcedureBlock = Block & ProcedureMixin; +interface ProcedureMixin extends ProcedureMixinType { + arguments_: string[]; + argumentVarModels_: VariableModel[]; + callType_: string; + paramIds_: string[]; + hasStatements_: boolean; + statementConnection_: Connection | null; +} +type ProcedureMixinType = typeof PROCEDURE_DEF_COMMON; -/** - * A dictionary of the block definitions provided by this module. - * @type {!Object} - */ -const blocks = {}; -exports.blocks = blocks; +/** Extra state for serialising procedure blocks. */ +type ProcedureExtraState = { + params?: Array<{name: string; id: string}>; + hasStatements: boolean; +}; /** * Common properties for the procedure_defnoreturn and @@ -57,16 +68,17 @@ exports.blocks = blocks; const PROCEDURE_DEF_COMMON = { /** * Add or remove the statement block from this function definition. - * @param {boolean} hasStatements True if a statement block is needed. - * @this {Block} + * + * @param hasStatements True if a statement block is needed. */ - setStatements_: function(hasStatements) { + setStatements_: function (this: ProcedureBlock, hasStatements: boolean) { if (this.hasStatements_ === hasStatements) { return; } if (hasStatements) { this.appendStatementInput('STACK').appendField( - Msg['PROCEDURES_DEFNORETURN_DO']); + Msg['PROCEDURES_DEFNORETURN_DO'] + ); if (this.getInput('RETURN')) { this.moveInputBefore('STACK', 'RETURN'); } @@ -77,15 +89,15 @@ const PROCEDURE_DEF_COMMON = { }, /** * Update the display of parameters for this procedure definition block. - * @private - * @this {Block} + * + * @internal */ - updateParams_: function() { + updateParams_: function (this: ProcedureBlock) { // Merge the arguments into a human-readable list. let paramString = ''; if (this.arguments_.length) { paramString = - Msg['PROCEDURES_BEFORE_PARAMS'] + ' ' + this.arguments_.join(', '); + Msg['PROCEDURES_BEFORE_PARAMS'] + ' ' + this.arguments_.join(', '); } // The params field is deterministic based on the mutation, // no need to fire a change event. @@ -99,12 +111,15 @@ const PROCEDURE_DEF_COMMON = { /** * Create XML to represent the argument inputs. * Backwards compatible serialization implementation. - * @param {boolean=} opt_paramIds If true include the IDs of the parameter + * + * @param opt_paramIds If true include the IDs of the parameter * quarks. Used by Procedures.mutateCallers for reconnection. - * @return {!Element} XML storage element. - * @this {Block} + * @returns XML storage element. */ - mutationToDom: function(opt_paramIds) { + mutationToDom: function ( + this: ProcedureBlock, + opt_paramIds: boolean + ): Element { const container = xmlUtils.createElement('mutation'); if (opt_paramIds) { container.setAttribute('name', this.getFieldValue('NAME')); @@ -129,25 +144,32 @@ const PROCEDURE_DEF_COMMON = { /** * Parse XML to restore the argument inputs. * Backwards compatible serialization implementation. - * @param {!Element} xmlElement XML storage element. - * @this {Block} + * + * @param xmlElement XML storage element. */ - domToMutation: function(xmlElement) { + domToMutation: function (this: ProcedureBlock, xmlElement: Element) { this.arguments_ = []; this.argumentVarModels_ = []; for (let i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) { if (childNode.nodeName.toLowerCase() === 'arg') { - const varName = childNode.getAttribute('name'); + const childElement = childNode as Element; + const varName = childElement.getAttribute('name')!; const varId = - childNode.getAttribute('varid') || childNode.getAttribute('varId'); + childElement.getAttribute('varid') || + childElement.getAttribute('varId'); this.arguments_.push(varName); const variable = Variables.getOrCreateVariablePackage( - this.workspace, varId, varName, ''); + this.workspace, + varId, + varName, + '' + ); if (variable !== null) { this.argumentVarModels_.push(variable); } else { console.log( - `Failed to create a variable named "${varName}", ignoring.`); + `Failed to create a variable named "${varName}", ignoring.` + ); } } } @@ -159,11 +181,10 @@ const PROCEDURE_DEF_COMMON = { }, /** * Returns the state of this block as a JSON serializable object. - * @return {?{params: (!Array<{name: string, id: string}>|undefined), - * hasStatements: (boolean|undefined)}} The state of this block, eg the - * parameters and statements. + * + * @returns The state of this block, eg the parameters and statements. */ - saveExtraState: function() { + saveExtraState: function (this: ProcedureBlock): ProcedureExtraState | null { if (!this.argumentVarModels_.length && this.hasStatements_) { return null; } @@ -186,17 +207,22 @@ const PROCEDURE_DEF_COMMON = { }, /** * Applies the given state to this block. - * @param {*} state The state to apply to this block, eg the parameters and - * statements. + * + * @param state The state to apply to this block, eg the parameters + * and statements. */ - loadExtraState: function(state) { + loadExtraState: function (this: ProcedureBlock, state: ProcedureExtraState) { this.arguments_ = []; this.argumentVarModels_ = []; if (state['params']) { for (let i = 0; i < state['params'].length; i++) { const param = state['params'][i]; const variable = Variables.getOrCreateVariablePackage( - this.workspace, param['id'], param['name'], ''); + this.workspace, + param['id'], + param['name'], + '' + ); this.arguments_.push(variable.name); this.argumentVarModels_.push(variable); } @@ -207,11 +233,14 @@ const PROCEDURE_DEF_COMMON = { }, /** * Populate the mutator's dialog with this block's components. - * @param {!Workspace} workspace Mutator's workspace. - * @return {!Block} Root block in mutator. - * @this {Block} + * + * @param workspace Mutator's workspace. + * @returns Root block in mutator. */ - decompose: function(workspace) { + decompose: function ( + this: ProcedureBlock, + workspace: Workspace + ): ContainerBlock { /* * Creates the following XML: * @@ -246,7 +275,10 @@ const PROCEDURE_DEF_COMMON = { node = nextNode; } - const containerBlock = Xml.domToBlock(containerBlockNode, workspace); + const containerBlock = Xml.domToBlock( + containerBlockNode, + workspace + ) as ContainerBlock; if (this.type === 'procedures_defreturn') { containerBlock.setFieldValue(this.hasStatements_, 'STATEMENTS'); @@ -260,10 +292,10 @@ const PROCEDURE_DEF_COMMON = { }, /** * Reconfigure this block based on the mutator dialog's components. - * @param {!Block} containerBlock Root block in mutator. - * @this {Block} + * + * @param containerBlock Root block in mutator. */ - compose: function(containerBlock) { + compose: function (this: ProcedureBlock, containerBlock: ContainerBlock) { // Parameter list. this.arguments_ = []; this.paramIds_ = []; @@ -272,12 +304,12 @@ const PROCEDURE_DEF_COMMON = { while (paramBlock && !paramBlock.isInsertionMarker()) { const varName = paramBlock.getFieldValue('NAME'); this.arguments_.push(varName); - const variable = this.workspace.getVariable(varName, ''); + const variable = this.workspace.getVariable(varName, '')!; this.argumentVarModels_.push(variable); this.paramIds_.push(paramBlock.id); paramBlock = - paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); + paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); } this.updateParams_(); Procedures.mutateCallers(this); @@ -294,10 +326,10 @@ const PROCEDURE_DEF_COMMON = { this.statementConnection_ = null; } else { // Save the stack, then disconnect it. - const stackConnection = this.getInput('STACK').connection; - this.statementConnection_ = stackConnection.targetConnection; + const stackConnection = this.getInput('STACK')!.connection; + this.statementConnection_ = stackConnection!.targetConnection; if (this.statementConnection_) { - const stackBlock = stackConnection.targetBlock(); + const stackBlock = stackConnection!.targetBlock()!; stackBlock.unplug(); stackBlock.bumpNeighbours(); } @@ -308,38 +340,41 @@ const PROCEDURE_DEF_COMMON = { }, /** * Return all variables referenced by this block. - * @return {!Array} List of variable names. - * @this {Block} + * + * @returns List of variable names. */ - getVars: function() { + getVars: function (this: ProcedureBlock): string[] { return this.arguments_; }, /** * Return all variables referenced by this block. - * @return {!Array} List of variable models. - * @this {Block} + * + * @returns List of variable models. */ - getVarModels: function() { + getVarModels: function (this: ProcedureBlock): VariableModel[] { return this.argumentVarModels_; }, /** * Notification that a variable is renaming. * If the ID matches one of this block's variables, rename it. - * @param {string} oldId ID of variable to rename. - * @param {string} newId ID of new variable. May be the same as oldId, but - * with an updated name. Guaranteed to be the same type as the old - * variable. - * @override - * @this {Block} + * + * @param oldId ID of variable to rename. + * @param newId ID of new variable. May be the same as oldId, but + * with an updated name. Guaranteed to be the same type as the + * old variable. */ - renameVarById: function(oldId, newId) { - const oldVariable = this.workspace.getVariableById(oldId); + renameVarById: function ( + this: ProcedureBlock & BlockSvg, + oldId: string, + newId: string + ) { + const oldVariable = this.workspace.getVariableById(oldId)!; if (oldVariable.type !== '') { // Procedure arguments always have the empty type. return; } const oldName = oldVariable.name; - const newVar = this.workspace.getVariableById(newId); + const newVar = this.workspace.getVariableById(newId)!; let change = false; for (let i = 0; i < this.argumentVarModels_.length; i++) { @@ -357,12 +392,13 @@ const PROCEDURE_DEF_COMMON = { /** * Notification that a variable is renaming but keeping the same ID. If the * variable is in use on this block, rerender to show the new name. - * @param {!VariableModel} variable The variable being renamed. - * @package - * @override - * @this {Block} + * + * @param variable The variable being renamed. */ - updateVarName: function(variable) { + updateVarName: function ( + this: ProcedureBlock & BlockSvg, + variable: VariableModel + ) { const newName = variable.name; let change = false; let oldName; @@ -374,26 +410,32 @@ const PROCEDURE_DEF_COMMON = { } } if (change) { - this.displayRenamedVar_(oldName, newName); + this.displayRenamedVar_(oldName as string, newName); Procedures.mutateCallers(this); } }, /** * Update the display to reflect a newly renamed argument. - * @param {string} oldName The old display name of the argument. - * @param {string} newName The new display name of the argument. - * @private - * @this {Block} + * + * @internal + * @param oldName The old display name of the argument. + * @param newName The new display name of the argument. */ - displayRenamedVar_: function(oldName, newName) { + displayRenamedVar_: function ( + this: ProcedureBlock & BlockSvg, + oldName: string, + newName: string + ) { this.updateParams_(); // Update the mutator's variables if the mutator is open. const mutator = this.getIcon(Mutator.TYPE); if (mutator && mutator.bubbleIsVisible()) { - const blocks = mutator.getWorkspace().getAllBlocks(false); + const blocks = mutator.getWorkspace()!.getAllBlocks(false); for (let i = 0, block; (block = blocks[i]); i++) { - if (block.type === 'procedures_mutatorarg' && - Names.equals(oldName, block.getFieldValue('NAME'))) { + if ( + block.type === 'procedures_mutatorarg' && + Names.equals(oldName, block.getFieldValue('NAME')) + ) { block.setFieldValue(newName, 'NAME'); } } @@ -401,17 +443,18 @@ const PROCEDURE_DEF_COMMON = { }, /** * Add custom menu options to this block's context menu. - * @param {!Array} options List of menu options to add to. - * @this {Block} + * + * @param options List of menu options to add to. */ - customContextMenu: function(options) { + customContextMenu: function ( + this: ProcedureBlock, + options: Array + ) { if (this.isInFlyout) { return; } // Add option to create caller. - const option = {enabled: true}; const name = this.getFieldValue('NAME'); - option.text = Msg['PROCEDURES_CREATE_DO'].replace('%1', name); const xmlMutation = xmlUtils.createElement('mutation'); xmlMutation.setAttribute('name', name); for (let i = 0; i < this.arguments_.length; i++) { @@ -420,25 +463,27 @@ const PROCEDURE_DEF_COMMON = { xmlMutation.appendChild(xmlArg); } const xmlBlock = xmlUtils.createElement('block'); - xmlBlock.setAttribute('type', this.callType_); + xmlBlock.setAttribute('type', (this as AnyDuringMigration).callType_); xmlBlock.appendChild(xmlMutation); - option.callback = ContextMenu.callbackFactory(this, xmlBlock); - options.push(option); + options.push({ + enabled: true, + text: Msg['PROCEDURES_CREATE_DO'].replace('%1', name), + callback: ContextMenu.callbackFactory(this, xmlBlock), + }); // Add options to create getters for each parameter. if (!this.isCollapsed()) { for (let i = 0; i < this.argumentVarModels_.length; i++) { - const argOption = {enabled: true}; const argVar = this.argumentVarModels_[i]; - argOption.text = - Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.name); - const argXmlField = Variables.generateVariableFieldDom(argVar); const argXmlBlock = xmlUtils.createElement('block'); argXmlBlock.setAttribute('type', 'variables_get'); argXmlBlock.appendChild(argXmlField); - argOption.callback = ContextMenu.callbackFactory(this, argXmlBlock); - options.push(argOption); + options.push({ + enabled: true, + text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.name), + callback: ContextMenu.callbackFactory(this, argXmlBlock), + }); } } }, @@ -448,25 +493,26 @@ blocks['procedures_defnoreturn'] = { ...PROCEDURE_DEF_COMMON, /** * Block for defining a procedure with no return value. - * @this {Block} */ - init: function() { + init: function (this: ProcedureBlock & BlockSvg) { const initName = Procedures.findLegalName('', this); const nameField = fieldRegistry.fromJson({ type: 'field_input', text: initName, - }); - nameField.setValidator(Procedures.rename); + }) as FieldTextInput; + nameField!.setValidator(Procedures.rename); nameField.setSpellcheck(false); this.appendDummyInput() - .appendField(Msg['PROCEDURES_DEFNORETURN_TITLE']) - .appendField(nameField, 'NAME') - .appendField('', 'PARAMS'); + .appendField(Msg['PROCEDURES_DEFNORETURN_TITLE']) + .appendField(nameField, 'NAME') + .appendField('', 'PARAMS'); this.setMutator(new Mutator(['procedures_mutatorarg'], this)); - if ((this.workspace.options.comments || - (this.workspace.options.parentWorkspace && + if ( + (this.workspace.options.comments || + (this.workspace.options.parentWorkspace && this.workspace.options.parentWorkspace.options.comments)) && - Msg['PROCEDURES_DEFNORETURN_COMMENT']) { + Msg['PROCEDURES_DEFNORETURN_COMMENT'] + ) { this.setCommentText(Msg['PROCEDURES_DEFNORETURN_COMMENT']); } this.setStyle('procedure_blocks'); @@ -479,13 +525,13 @@ blocks['procedures_defnoreturn'] = { }, /** * Return the signature of this procedure definition. - * @return {!Array} Tuple containing three elements: + * + * @returns Tuple containing three elements: * - the name of the defined procedure, * - a list of all its arguments, * - that it DOES NOT have a return value. - * @this {Block} */ - getProcedureDef: function() { + getProcedureDef: function (this: ProcedureBlock): [string, string[], false] { return [this.getFieldValue('NAME'), this.arguments_, false]; }, callType_: 'procedures_callnoreturn', @@ -495,28 +541,29 @@ blocks['procedures_defreturn'] = { ...PROCEDURE_DEF_COMMON, /** * Block for defining a procedure with a return value. - * @this {Block} */ - init: function() { + init: function (this: ProcedureBlock & BlockSvg) { const initName = Procedures.findLegalName('', this); const nameField = fieldRegistry.fromJson({ type: 'field_input', text: initName, - }); + }) as FieldTextInput; nameField.setValidator(Procedures.rename); nameField.setSpellcheck(false); this.appendDummyInput() - .appendField(Msg['PROCEDURES_DEFRETURN_TITLE']) - .appendField(nameField, 'NAME') - .appendField('', 'PARAMS'); + .appendField(Msg['PROCEDURES_DEFRETURN_TITLE']) + .appendField(nameField, 'NAME') + .appendField('', 'PARAMS'); this.appendValueInput('RETURN') - .setAlign(Align.RIGHT) - .appendField(Msg['PROCEDURES_DEFRETURN_RETURN']); + .setAlign(Align.RIGHT) + .appendField(Msg['PROCEDURES_DEFRETURN_RETURN']); this.setMutator(new Mutator(['procedures_mutatorarg'], this)); - if ((this.workspace.options.comments || - (this.workspace.options.parentWorkspace && + if ( + (this.workspace.options.comments || + (this.workspace.options.parentWorkspace && this.workspace.options.parentWorkspace.options.comments)) && - Msg['PROCEDURES_DEFRETURN_COMMENT']) { + Msg['PROCEDURES_DEFRETURN_COMMENT'] + ) { this.setCommentText(Msg['PROCEDURES_DEFRETURN_COMMENT']); } this.setStyle('procedure_blocks'); @@ -529,67 +576,81 @@ blocks['procedures_defreturn'] = { }, /** * Return the signature of this procedure definition. - * @return {!Array} Tuple containing three elements: + * + * @returns Tuple containing three elements: * - the name of the defined procedure, * - a list of all its arguments, * - that it DOES have a return value. - * @this {Block} */ - getProcedureDef: function() { + getProcedureDef: function (this: ProcedureBlock): [string, string[], true] { return [this.getFieldValue('NAME'), this.arguments_, true]; }, callType_: 'procedures_callreturn', }; -blocks['procedures_mutatorcontainer'] = { +/** Type of a procedures_mutatorcontainer block. */ +type ContainerBlock = Block & ContainerMixin; +interface ContainerMixin extends ContainerMixinType {} +type ContainerMixinType = typeof PROCEDURES_MUTATORCONTAINER; + +const PROCEDURES_MUTATORCONTAINER = { /** * Mutator block for procedure container. - * @this {Block} */ - init: function() { + init: function (this: ContainerBlock) { this.appendDummyInput().appendField( - Msg['PROCEDURES_MUTATORCONTAINER_TITLE']); + Msg['PROCEDURES_MUTATORCONTAINER_TITLE'] + ); this.appendStatementInput('STACK'); this.appendDummyInput('STATEMENT_INPUT') - .appendField(Msg['PROCEDURES_ALLOW_STATEMENTS']) - .appendField( - fieldRegistry.fromJson({ - type: 'field_checkbox', - checked: true, - }), - 'STATEMENTS'); + .appendField(Msg['PROCEDURES_ALLOW_STATEMENTS']) + .appendField( + fieldRegistry.fromJson({ + type: 'field_checkbox', + checked: true, + }) as FieldCheckbox, + 'STATEMENTS' + ); this.setStyle('procedure_blocks'); this.setTooltip(Msg['PROCEDURES_MUTATORCONTAINER_TOOLTIP']); this.contextMenu = false; }, }; +blocks['procedures_mutatorcontainer'] = PROCEDURES_MUTATORCONTAINER; -blocks['procedures_mutatorarg'] = { +/** Type of a procedures_mutatorarg block. */ +type ArgumentBlock = Block & ArgumentMixin; +interface ArgumentMixin extends ArgumentMixinType {} +type ArgumentMixinType = typeof PROCEDURES_MUTATORARGUMENT; + +// TODO(#6920): This is kludgy. +type FieldTextInputForArgument = FieldTextInput & { + oldShowEditorFn_(_e?: Event, quietInput?: boolean): void; + createdVariables_: VariableModel[]; +}; + +const PROCEDURES_MUTATORARGUMENT = { /** * Mutator block for procedure argument. - * @this {Block} */ - init: function() { + init: function (this: ArgumentBlock) { const field = fieldRegistry.fromJson({ type: 'field_input', text: Procedures.DEFAULT_ARG, - }); + }) as FieldTextInputForArgument; field.setValidator(this.validator_); // Hack: override showEditor to do just a little bit more work. // We don't have a good place to hook into the start of a text edit. - field.oldShowEditorFn_ = field.showEditor_; - /** - * @this {FieldTextInput} - */ - const newShowEditorFn = function() { + field.oldShowEditorFn_ = (field as AnyDuringMigration).showEditor_; + const newShowEditorFn = function (this: typeof field) { this.createdVariables_ = []; this.oldShowEditorFn_(); }; - field.showEditor_ = newShowEditorFn; + (field as AnyDuringMigration).showEditor_ = newShowEditorFn; this.appendDummyInput() - .appendField(Msg['PROCEDURES_MUTATORARG_TITLE']) - .appendField(field, 'NAME'); + .appendField(Msg['PROCEDURES_MUTATORARG_TITLE']) + .appendField(field, 'NAME'); this.setPreviousStatement(true); this.setNextStatement(true); this.setStyle('procedure_blocks'); @@ -610,14 +671,17 @@ blocks['procedures_mutatorarg'] = { * necessary. * Merge runs of whitespace. Strip leading and trailing whitespace. * Beyond this, all names are legal. - * @param {string} varName User-supplied name. - * @return {?string} Valid name, or null if a name was not specified. - * @private - * @this {FieldTextInput} + * + * @internal + * @param varName User-supplied name. + * @returns Valid name, or null if a name was not specified. */ - validator_: function(varName) { - const sourceBlock = this.getSourceBlock(); - const outerWs = sourceBlock.workspace.getRootWorkspace(); + validator_: function ( + this: FieldTextInputForArgument, + varName: string + ): string | null { + const sourceBlock = this.getSourceBlock()!; + const outerWs = sourceBlock!.workspace.getRootWorkspace()!; varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); if (!varName) { return null; @@ -625,11 +689,12 @@ blocks['procedures_mutatorarg'] = { // Prevents duplicate parameter names in functions const workspace = - sourceBlock.workspace.targetWorkspace || sourceBlock.workspace; + (sourceBlock.workspace as WorkspaceSvg).targetWorkspace || + sourceBlock.workspace; const blocks = workspace.getAllBlocks(false); const caselessName = varName.toLowerCase(); for (let i = 0; i < blocks.length; i++) { - if (blocks[i].id === this.getSourceBlock().id) { + if (blocks[i].id === this.getSourceBlock()!.id) { continue; } // Other blocks values may not be set yet when this is loaded. @@ -663,12 +728,15 @@ blocks['procedures_mutatorarg'] = { * Called when focusing away from the text field. * Deletes all variables that were created as the user typed their intended * variable name. - * @param {string} newText The new variable name. - * @private - * @this {FieldTextInput} + * + * @internal + * @param newText The new variable name. */ - deleteIntermediateVars_: function(newText) { - const outerWs = this.getSourceBlock().workspace.getRootWorkspace(); + deleteIntermediateVars_: function ( + this: FieldTextInputForArgument, + newText: string + ) { + const outerWs = this.getSourceBlock()!.workspace.getRootWorkspace(); if (!outerWs) { return; } @@ -680,6 +748,25 @@ blocks['procedures_mutatorarg'] = { } }, }; +blocks['procedures_mutatorarg'] = PROCEDURES_MUTATORARGUMENT; + +/** Type of a block using the PROCEDURE_CALL_COMMON mixin. */ +type CallBlock = Block & CallMixin; +interface CallMixin extends CallMixinType { + argumentVarModels_: VariableModel[]; + arguments_: string[]; + defType_: string; + quarkIds_: string[] | null; + quarkConnections_: {[id: string]: Connection}; + previousEnabledState_: boolean; +} +type CallMixinType = typeof PROCEDURE_CALL_COMMON; + +/** Extra state for serialising call blocks. */ +type CallExtraState = { + name: string; + params?: string[]; +}; /** * Common properties for the procedure_callnoreturn and @@ -688,39 +775,47 @@ blocks['procedures_mutatorarg'] = { const PROCEDURE_CALL_COMMON = { /** * Returns the name of the procedure this block calls. - * @return {string} Procedure name. - * @this {Block} + * + * @returns Procedure name. */ - getProcedureCall: function() { + getProcedureCall: function (this: CallBlock): string { // The NAME field is guaranteed to exist, null will never be returned. - return /** @type {string} */ (this.getFieldValue('NAME')); + return this.getFieldValue('NAME'); }, /** * Notification that a procedure is renaming. * If the name matches this block's procedure, rename it. - * @param {string} oldName Previous name of procedure. - * @param {string} newName Renamed procedure. - * @this {Block} + * + * @param oldName Previous name of procedure. + * @param newName Renamed procedure. */ - renameProcedure: function(oldName, newName) { + renameProcedure: function ( + this: CallBlock, + oldName: string, + newName: string + ) { if (Names.equals(oldName, this.getProcedureCall())) { this.setFieldValue(newName, 'NAME'); - const baseMsg = this.outputConnection ? - Msg['PROCEDURES_CALLRETURN_TOOLTIP'] : - Msg['PROCEDURES_CALLNORETURN_TOOLTIP']; + const baseMsg = this.outputConnection + ? Msg['PROCEDURES_CALLRETURN_TOOLTIP'] + : Msg['PROCEDURES_CALLNORETURN_TOOLTIP']; this.setTooltip(baseMsg.replace('%1', newName)); } }, /** * Notification that the procedure's parameters have changed. - * @param {!Array} paramNames New param names, e.g. ['x', 'y', 'z']. - * @param {!Array} paramIds IDs of params (consistent for each - * parameter through the life of a mutator, regardless of param renaming), + * + * @internal + * @param paramNames New param names, e.g. ['x', 'y', 'z']. + * @param paramIds IDs of params (consistent for each parameter + * through the life of a mutator, regardless of param renaming), * e.g. ['piua', 'f8b_', 'oi.o']. - * @private - * @this {Block} */ - setProcedureParameters_: function(paramNames, paramIds) { + setProcedureParameters_: function ( + this: CallBlock, + paramNames: string[], + paramIds: string[] + ) { // Data structures: // this.arguments = ['x', 'y'] // Existing param names. @@ -730,11 +825,12 @@ const PROCEDURE_CALL_COMMON = { // Existing param IDs. // Note that quarkConnections_ may include IDs that no longer exist, but // which might reappear if a param is reattached in the mutator. - const defBlock = - Procedures.getDefinition(this.getProcedureCall(), this.workspace); + const defBlock = Procedures.getDefinition( + this.getProcedureCall(), + this.workspace + ); const mutatorIcon = defBlock && defBlock.getIcon(Mutator.TYPE); - const mutatorOpen = - mutatorIcon && mutatorIcon.bubbleIsVisible(); + const mutatorOpen = mutatorIcon && mutatorIcon.bubbleIsVisible(); if (!mutatorOpen) { this.quarkConnections_ = {}; this.quarkIds_ = null; @@ -762,10 +858,13 @@ const PROCEDURE_CALL_COMMON = { for (let i = 0; i < this.arguments_.length; i++) { const input = this.getInput('ARG' + i); if (input) { - const connection = input.connection.targetConnection; + const connection = input.connection!.targetConnection!; this.quarkConnections_[this.quarkIds_[i]] = connection; - if (mutatorOpen && connection && - paramIds.indexOf(this.quarkIds_[i]) === -1) { + if ( + mutatorOpen && + connection && + paramIds.indexOf(this.quarkIds_[i]) === -1 + ) { // This connection should no longer be attached to this block. connection.disconnect(); connection.getSourceBlock().bumpNeighbours(); @@ -773,12 +872,16 @@ const PROCEDURE_CALL_COMMON = { } } // Rebuild the block's arguments. - this.arguments_ = [].concat(paramNames); + this.arguments_ = ([] as string[]).concat(paramNames); // And rebuild the argument model list. this.argumentVarModels_ = []; for (let i = 0; i < this.arguments_.length; i++) { const variable = Variables.getOrCreateVariablePackage( - this.workspace, null, this.arguments_[i], ''); + this.workspace, + null, + this.arguments_[i], + '' + ); this.argumentVarModels_.push(variable); } @@ -787,9 +890,10 @@ const PROCEDURE_CALL_COMMON = { // Reconnect any child blocks. if (this.quarkIds_) { for (let i = 0; i < this.arguments_.length; i++) { - const quarkId = this.quarkIds_[i]; + const quarkId: string = this.quarkIds_[i]; // TODO(#6920) if (quarkId in this.quarkConnections_) { - const connection = this.quarkConnections_[quarkId]; + // TODO(#6920): investigate claimed circular initialisers. + const connection: Connection = this.quarkConnections_[quarkId]; if (!connection?.reconnect(this, 'ARG' + i)) { // Block no longer exists or has been attached elsewhere. delete this.quarkConnections_[quarkId]; @@ -800,10 +904,10 @@ const PROCEDURE_CALL_COMMON = { }, /** * Modify this block to have the correct number of arguments. - * @private - * @this {Block} + * + * @internal */ - updateShape_: function() { + updateShape_: function (this: CallBlock) { for (let i = 0; i < this.arguments_.length; i++) { const argField = this.getField('ARGNAME' + i); if (argField) { @@ -821,10 +925,10 @@ const PROCEDURE_CALL_COMMON = { const newField = fieldRegistry.fromJson({ type: 'field_label', text: this.arguments_[i], - }); + }) as FieldLabel; const input = this.appendValueInput('ARG' + i) - .setAlign(Align.RIGHT) - .appendField(newField, 'ARGNAME' + i); + .setAlign(Align.RIGHT) + .appendField(newField, 'ARGNAME' + i); input.init(); } } @@ -850,10 +954,10 @@ const PROCEDURE_CALL_COMMON = { /** * Create XML to represent the (non-editable) name and arguments. * Backwards compatible serialization implementation. - * @return {!Element} XML storage element. - * @this {Block} + * + * @returns XML storage element. */ - mutationToDom: function() { + mutationToDom: function (this: CallBlock): Element { const container = xmlUtils.createElement('mutation'); container.setAttribute('name', this.getProcedureCall()); for (let i = 0; i < this.arguments_.length; i++) { @@ -866,28 +970,28 @@ const PROCEDURE_CALL_COMMON = { /** * Parse XML to restore the (non-editable) name and parameters. * Backwards compatible serialization implementation. - * @param {!Element} xmlElement XML storage element. - * @this {Block} + * + * @param xmlElement XML storage element. */ - domToMutation: function(xmlElement) { - const name = xmlElement.getAttribute('name'); + domToMutation: function (this: CallBlock, xmlElement: Element) { + const name = xmlElement.getAttribute('name')!; this.renameProcedure(this.getProcedureCall(), name); - const args = []; + const args: string[] = []; const paramIds = []; for (let i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) { if (childNode.nodeName.toLowerCase() === 'arg') { - args.push(childNode.getAttribute('name')); - paramIds.push(childNode.getAttribute('paramId')); + args.push((childNode as Element).getAttribute('name')!); + paramIds.push((childNode as Element).getAttribute('paramId')!); } } this.setProcedureParameters_(args, paramIds); }, /** * Returns the state of this block as a JSON serializable object. - * @return {{name: string, params:(!Array|undefined)}} The state of - * this block, ie the params and procedure name. + * + * @returns The state of this block, ie the params and procedure name. */ - saveExtraState: function() { + saveExtraState: function (this: CallBlock): CallExtraState { const state = Object.create(null); state['name'] = this.getProcedureCall(); if (this.arguments_.length) { @@ -897,42 +1001,43 @@ const PROCEDURE_CALL_COMMON = { }, /** * Applies the given state to this block. - * @param {*} state The state to apply to this block, ie the params and + * + * @param state The state to apply to this block, ie the params and * procedure name. */ - loadExtraState: function(state) { + loadExtraState: function (this: CallBlock, state: CallExtraState) { this.renameProcedure(this.getProcedureCall(), state['name']); const params = state['params']; if (params) { - const ids = []; + const ids: string[] = []; ids.length = params.length; - ids.fill(null); + ids.fill(null as unknown as string); // TODO(#6920) this.setProcedureParameters_(params, ids); } }, /** * Return all variables referenced by this block. - * @return {!Array} List of variable names. - * @this {Block} + * + * @returns List of variable names. */ - getVars: function() { + getVars: function (this: CallBlock): string[] { return this.arguments_; }, /** * Return all variables referenced by this block. - * @return {!Array} List of variable models. - * @this {Block} + * + * @returns List of variable models. */ - getVarModels: function() { + getVarModels: function (this: CallBlock): VariableModel[] { return this.argumentVarModels_; }, /** * Procedure calls cannot exist without the corresponding procedure * definition. Enforce this link whenever an event is fired. - * @param {!AbstractEvent} event Change event. - * @this {Block} + * + * @param event Change event. */ - onchange: function(event) { + onchange: function (this: CallBlock, event: AbstractEvent) { if (!this.workspace || this.workspace.isFlyout) { // Block is deleted or is in a flyout. return; @@ -941,16 +1046,20 @@ const PROCEDURE_CALL_COMMON = { // Events not generated by user. Skip handling. return; } - if (event.type === Events.BLOCK_CREATE && - event.ids.indexOf(this.id) !== -1) { + if ( + event.type === Events.BLOCK_CREATE && + (event as BlockCreate).ids!.indexOf(this.id) !== -1 + ) { // Look for the case where a procedure call was created (usually through // paste) and there is no matching definition. In this case, create // an empty definition block with the correct signature. const name = this.getProcedureCall(); let def = Procedures.getDefinition(name, this.workspace); - if (def && - (def.type !== this.defType_ || - JSON.stringify(def.getVars()) !== JSON.stringify(this.arguments_))) { + if ( + def && + (def.type !== this.defType_ || + JSON.stringify(def.getVars()) !== JSON.stringify(this.arguments_)) + ) { // The signatures don't match. def = null; } @@ -973,8 +1082,8 @@ const PROCEDURE_CALL_COMMON = { const xy = this.getRelativeToSurfaceXY(); const x = xy.x + config.snapRadius * (this.RTL ? -1 : 1); const y = xy.y + config.snapRadius * 2; - block.setAttribute('x', x); - block.setAttribute('y', y); + block.setAttribute('x', `${x}`); + block.setAttribute('y', `${y}`); const mutation = this.mutationToDom(); block.appendChild(mutation); const field = xmlUtils.createElement('field'); @@ -1001,10 +1110,14 @@ const PROCEDURE_CALL_COMMON = { this.dispose(true); Events.setGroup(false); } - } else if (event.type === Events.CHANGE && event.element === 'disabled') { + } else if ( + event.type === Events.BLOCK_CHANGE && + (event as BlockChange).element === 'disabled' + ) { + const blockChangeEvent = event as BlockChange; const name = this.getProcedureCall(); const def = Procedures.getDefinition(name, this.workspace); - if (def && def.id === event.blockId) { + if (def && def.id === blockChangeEvent.blockId) { // in most cases the old group should be '' const oldGroup = Events.getGroup(); if (oldGroup) { @@ -1013,10 +1126,11 @@ const PROCEDURE_CALL_COMMON = { // investigate. If the use ends up being valid we may need to reorder // events in the undo stack. console.log( - 'Saw an existing group while responding to a definition change'); + 'Saw an existing group while responding to a definition change' + ); } Events.setGroup(event.group); - if (event.newValue) { + if (blockChangeEvent.newValue) { this.previousEnabledState_ = this.isEnabled(); this.setEnabled(false); } else { @@ -1028,28 +1142,32 @@ const PROCEDURE_CALL_COMMON = { }, /** * Add menu option to find the definition block for this call. - * @param {!Array} options List of menu options to add to. - * @this {Block} + * + * @param options List of menu options to add to. */ - customContextMenu: function(options) { - if (!this.workspace.isMovable()) { + customContextMenu: function ( + this: CallBlock, + options: Array + ) { + if (!(this.workspace as WorkspaceSvg).isMovable()) { // If we center on the block and the workspace isn't movable we could // loose blocks at the edges of the workspace. return; } - const option = {enabled: true}; - option.text = Msg['PROCEDURES_HIGHLIGHT_DEF']; const name = this.getProcedureCall(); const workspace = this.workspace; - option.callback = function() { - const def = Procedures.getDefinition(name, workspace); - if (def) { - workspace.centerOnBlock(def.id); - def.select(); - } - }; - options.push(option); + options.push({ + enabled: true, + text: Msg['PROCEDURES_HIGHLIGHT_DEF'], + callback: function () { + const def = Procedures.getDefinition(name, workspace); + if (def) { + (workspace as WorkspaceSvg).centerOnBlock(def.id); + (def as BlockSvg).select(); + } + }, + }); }, }; @@ -1057,9 +1175,8 @@ blocks['procedures_callnoreturn'] = { ...PROCEDURE_CALL_COMMON, /** * Block for calling a procedure with no return value. - * @this {Block} */ - init: function() { + init: function (this: CallBlock) { this.appendDummyInput('TOPROW').appendField('', 'NAME'); this.setPreviousStatement(true); this.setNextStatement(true); @@ -1080,9 +1197,8 @@ blocks['procedures_callreturn'] = { ...PROCEDURE_CALL_COMMON, /** * Block for calling a procedure with a return value. - * @this {Block} */ - init: function() { + init: function (this: CallBlock) { this.appendDummyInput('TOPROW').appendField('', 'NAME'); this.setOutput(true); this.setStyle('procedure_blocks'); @@ -1098,17 +1214,24 @@ blocks['procedures_callreturn'] = { defType_: 'procedures_defreturn', }; -blocks['procedures_ifreturn'] = { +/** Type of a procedures_ifreturn block. */ +type IfReturnBlock = Block & IfReturnMixin; +interface IfReturnMixin extends IfReturnMixinType { + hasReturnValue_: boolean; +} +type IfReturnMixinType = typeof PROCEDURES_IFRETURN; + +const PROCEDURES_IFRETURN = { /** * Block for conditionally returning a value from a procedure. - * @this {Block} */ - init: function() { + init: function (this: IfReturnBlock) { this.appendValueInput('CONDITION') - .setCheck('Boolean') - .appendField(Msg['CONTROLS_IF_MSG_IF']); + .setCheck('Boolean') + .appendField(Msg['CONTROLS_IF_MSG_IF']); this.appendValueInput('VALUE').appendField( - Msg['PROCEDURES_DEFRETURN_RETURN']); + Msg['PROCEDURES_DEFRETURN_RETURN'] + ); this.setInputsInline(true); this.setPreviousStatement(true); this.setNextStatement(true); @@ -1119,26 +1242,27 @@ blocks['procedures_ifreturn'] = { }, /** * Create XML to represent whether this block has a return value. - * @return {!Element} XML storage element. - * @this {Block} + * + * @returns XML storage element. */ - mutationToDom: function() { + mutationToDom: function (this: IfReturnBlock): Element { const container = xmlUtils.createElement('mutation'); - container.setAttribute('value', Number(this.hasReturnValue_)); + container.setAttribute('value', String(Number(this.hasReturnValue_))); return container; }, /** * Parse XML to restore whether this block has a return value. - * @param {!Element} xmlElement XML storage element. - * @this {Block} + * + * @param xmlElement XML storage element. */ - domToMutation: function(xmlElement) { + domToMutation: function (this: IfReturnBlock, xmlElement: Element) { const value = xmlElement.getAttribute('value'); - this.hasReturnValue_ = (value === '1'); + this.hasReturnValue_ = value === '1'; if (!this.hasReturnValue_) { this.removeInput('VALUE'); this.appendDummyInput('VALUE').appendField( - Msg['PROCEDURES_DEFRETURN_RETURN']); + Msg['PROCEDURES_DEFRETURN_RETURN'] + ); } }, @@ -1150,36 +1274,43 @@ blocks['procedures_ifreturn'] = { /** * Called whenever anything on the workspace changes. * Add warning if this flow block is not nested inside a loop. - * @param {!AbstractEvent} e Move event. - * @this {Block} + * + * @param e Move event. */ - onchange: function(e) { - if (this.workspace.isDragging && this.workspace.isDragging() || - e.type !== Events.BLOCK_MOVE) { - return; // Don't change state at the start of a drag. + onchange: function (this: IfReturnBlock, e: AbstractEvent) { + if ( + ((this.workspace as WorkspaceSvg).isDragging && + (this.workspace as WorkspaceSvg).isDragging()) || + e.type !== Events.BLOCK_MOVE + ) { + return; // Don't change state at the start of a drag. } let legal = false; // Is the block nested in a procedure? - let block = this; + let block = this; // eslint-disable-line @typescript-eslint/no-this-alias do { if (this.FUNCTION_TYPES.indexOf(block.type) !== -1) { legal = true; break; } - block = block.getSurroundParent(); + block = block.getSurroundParent()!; } while (block); if (legal) { // If needed, toggle whether this block has a return value. if (block.type === 'procedures_defnoreturn' && this.hasReturnValue_) { this.removeInput('VALUE'); this.appendDummyInput('VALUE').appendField( - Msg['PROCEDURES_DEFRETURN_RETURN']); + Msg['PROCEDURES_DEFRETURN_RETURN'] + ); this.hasReturnValue_ = false; } else if ( - block.type === 'procedures_defreturn' && !this.hasReturnValue_) { + block.type === 'procedures_defreturn' && + !this.hasReturnValue_ + ) { this.removeInput('VALUE'); this.appendValueInput('VALUE').appendField( - Msg['PROCEDURES_DEFRETURN_RETURN']); + Msg['PROCEDURES_DEFRETURN_RETURN'] + ); this.hasReturnValue_ = true; } this.setWarningText(null); @@ -1201,6 +1332,7 @@ blocks['procedures_ifreturn'] = { */ FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn'], }; +blocks['procedures_ifreturn'] = PROCEDURES_IFRETURN; // Register provided blocks. defineBlocks(blocks);