Files
blockly/blocks/loops.ts
Christopher Allen 4ab8d00099 refactor(generators): Migrate JavaScript generators to TypeScript (#7602)
* refactor(generators): Migrate javascript_generator.js to TypeScript

* refactor(generators): Simplify getAdjusted

  Slightly simplify the implementation of getAdjusted, in part to
  make it more readable.  Also improve its JSDoc comment.

* refactor(generators): Migrate generators/javascript/* to TypeScript

  First pass doing very mechanistic migration, not attempting to fix
  all the resulting type errors.

* fix(generators): Fix type errors in generator functions

  This consists almost entirely of adding casts, so the code output
  by tsc should be as similar as possible to the pre-migration .js
  source files.

* refactor(generators): Migrate generators/javascript.js to TypeScript

  The way the generator functions are added to
  javascriptGenerator.forBlock has been modified so that incorrect
  generator function signatures will cause tsc to generate a type
  error.

* chore(generator): Format

  One block protected with // prettier-ignore to preserve careful
  comment formatting.

  Where there are repeated concatenations prettier has made a pretty
  mess of things, but the correct fix is probably to use template
  literals instead (rather than just locally disabling prettier).
  This has been added to the to-do list in #7600.

* fix(generators): Fixes for PR #7602

* fix(generators): Fix syntax error
2023-10-30 08:16:13 +00:00

392 lines
11 KiB
TypeScript

/**
* @license
* Copyright 2012 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// Former goog.module ID: Blockly.libraryBlocks.loops
import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js';
import type {Block} from '../core/block.js';
import * as ContextMenu from '../core/contextmenu.js';
import type {
ContextMenuOption,
LegacyContextMenuOption,
} from '../core/contextmenu_registry.js';
import * as Events from '../core/events/events.js';
import * as Extensions from '../core/extensions.js';
import {Msg} from '../core/msg.js';
import {
createBlockDefinitionsFromJsonArray,
defineBlocks,
} from '../core/common.js';
import '../core/field_dropdown.js';
import '../core/field_label.js';
import '../core/field_number.js';
import '../core/field_variable.js';
import '../core/icons/warning_icon.js';
import {FieldVariable} from '../core/field_variable.js';
import {WorkspaceSvg} from '../core/workspace_svg.js';
/**
* A dictionary of the block definitions provided by this module.
*/
export const blocks = createBlockDefinitionsFromJsonArray([
// Block for repeat n times (external number).
{
'type': 'controls_repeat_ext',
'message0': '%{BKY_CONTROLS_REPEAT_TITLE}',
'args0': [
{
'type': 'input_value',
'name': 'TIMES',
'check': 'Number',
},
],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [
{
'type': 'input_statement',
'name': 'DO',
},
],
'previousStatement': null,
'nextStatement': null,
'style': 'loop_blocks',
'tooltip': '%{BKY_CONTROLS_REPEAT_TOOLTIP}',
'helpUrl': '%{BKY_CONTROLS_REPEAT_HELPURL}',
},
// Block for repeat n times (internal number).
// The 'controls_repeat_ext' block is preferred as it is more flexible.
{
'type': 'controls_repeat',
'message0': '%{BKY_CONTROLS_REPEAT_TITLE}',
'args0': [
{
'type': 'field_number',
'name': 'TIMES',
'value': 10,
'min': 0,
'precision': 1,
},
],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [
{
'type': 'input_statement',
'name': 'DO',
},
],
'previousStatement': null,
'nextStatement': null,
'style': 'loop_blocks',
'tooltip': '%{BKY_CONTROLS_REPEAT_TOOLTIP}',
'helpUrl': '%{BKY_CONTROLS_REPEAT_HELPURL}',
},
// Block for 'do while/until' loop.
{
'type': 'controls_whileUntil',
'message0': '%1 %2',
'args0': [
{
'type': 'field_dropdown',
'name': 'MODE',
'options': [
['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}', 'WHILE'],
['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}', 'UNTIL'],
],
},
{
'type': 'input_value',
'name': 'BOOL',
'check': 'Boolean',
},
],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [
{
'type': 'input_statement',
'name': 'DO',
},
],
'previousStatement': null,
'nextStatement': null,
'style': 'loop_blocks',
'helpUrl': '%{BKY_CONTROLS_WHILEUNTIL_HELPURL}',
'extensions': ['controls_whileUntil_tooltip'],
},
// Block for 'for' loop.
{
'type': 'controls_for',
'message0': '%{BKY_CONTROLS_FOR_TITLE}',
'args0': [
{
'type': 'field_variable',
'name': 'VAR',
'variable': null,
},
{
'type': 'input_value',
'name': 'FROM',
'check': 'Number',
'align': 'RIGHT',
},
{
'type': 'input_value',
'name': 'TO',
'check': 'Number',
'align': 'RIGHT',
},
{
'type': 'input_value',
'name': 'BY',
'check': 'Number',
'align': 'RIGHT',
},
],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [
{
'type': 'input_statement',
'name': 'DO',
},
],
'inputsInline': true,
'previousStatement': null,
'nextStatement': null,
'style': 'loop_blocks',
'helpUrl': '%{BKY_CONTROLS_FOR_HELPURL}',
'extensions': ['contextMenu_newGetVariableBlock', 'controls_for_tooltip'],
},
// Block for 'for each' loop.
{
'type': 'controls_forEach',
'message0': '%{BKY_CONTROLS_FOREACH_TITLE}',
'args0': [
{
'type': 'field_variable',
'name': 'VAR',
'variable': null,
},
{
'type': 'input_value',
'name': 'LIST',
'check': 'Array',
},
],
'message1': '%{BKY_CONTROLS_REPEAT_INPUT_DO} %1',
'args1': [
{
'type': 'input_statement',
'name': 'DO',
},
],
'previousStatement': null,
'nextStatement': null,
'style': 'loop_blocks',
'helpUrl': '%{BKY_CONTROLS_FOREACH_HELPURL}',
'extensions': [
'contextMenu_newGetVariableBlock',
'controls_forEach_tooltip',
],
},
// Block for flow statements: continue, break.
{
'type': 'controls_flow_statements',
'message0': '%1',
'args0': [
{
'type': 'field_dropdown',
'name': 'FLOW',
'options': [
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}', 'BREAK'],
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}', 'CONTINUE'],
],
},
],
'previousStatement': null,
'style': 'loop_blocks',
'helpUrl': '%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}',
'suppressPrefixSuffix': true,
'extensions': ['controls_flow_tooltip', 'controls_flow_in_loop_check'],
},
]);
/**
* Tooltips for the 'controls_whileUntil' block, keyed by MODE value.
*
* @see {Extensions#buildTooltipForDropdown}
*/
const WHILE_UNTIL_TOOLTIPS = {
'WHILE': '%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}',
'UNTIL': '%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL}',
};
Extensions.register(
'controls_whileUntil_tooltip',
Extensions.buildTooltipForDropdown('MODE', WHILE_UNTIL_TOOLTIPS),
);
/**
* Tooltips for the 'controls_flow_statements' block, keyed by FLOW value.
*
* @see {Extensions#buildTooltipForDropdown}
*/
const BREAK_CONTINUE_TOOLTIPS = {
'BREAK': '%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK}',
'CONTINUE': '%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE}',
};
Extensions.register(
'controls_flow_tooltip',
Extensions.buildTooltipForDropdown('FLOW', BREAK_CONTINUE_TOOLTIPS),
);
/** Type of a block that has CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN */
type CustomContextMenuBlock = Block & CustomContextMenuMixin;
interface CustomContextMenuMixin extends CustomContextMenuMixinType {}
type CustomContextMenuMixinType =
typeof CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN;
/**
* Mixin to add a context menu item to create a 'variables_get' block.
* Used by blocks 'controls_for' and 'controls_forEach'.
*/
const CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN = {
/**
* Add context menu option to create getter block for the loop's variable.
* (customContextMenu support limited to web BlockSvg.)
*
* @param options List of menu options to add to.
*/
customContextMenu: function (
this: CustomContextMenuBlock,
options: Array<ContextMenuOption | LegacyContextMenuOption>,
) {
if (this.isInFlyout) {
return;
}
const varField = this.getField('VAR') as FieldVariable;
const variable = varField.getVariable()!;
const varName = variable.name;
if (!this.isCollapsed() && varName !== null) {
const getVarBlockState = {
type: 'variables_get',
fields: {VAR: varField.saveState(true)},
};
options.push({
enabled: true,
text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', varName),
callback: ContextMenu.callbackFactory(this, getVarBlockState),
});
}
},
};
Extensions.registerMixin(
'contextMenu_newGetVariableBlock',
CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN,
);
Extensions.register(
'controls_for_tooltip',
Extensions.buildTooltipWithFieldText('%{BKY_CONTROLS_FOR_TOOLTIP}', 'VAR'),
);
Extensions.register(
'controls_forEach_tooltip',
Extensions.buildTooltipWithFieldText(
'%{BKY_CONTROLS_FOREACH_TOOLTIP}',
'VAR',
),
);
/**
* List of block types that are loops and thus do not need warnings.
* To add a new loop type add this to your code:
*
* // If using the Blockly npm package and es6 import syntax:
* import {loops} from 'blockly/blocks';
* loops.loopTypes.add('custom_loop');
*
* // Else if using Closure Compiler and goog.modules:
* const {loopTypes} = goog.require('Blockly.libraryBlocks.loops');
* loopTypes.add('custom_loop');
*
* // Else if using blockly_compressed + blockss_compressed.js in browser:
* Blockly.libraryBlocks.loopTypes.add('custom_loop');
*/
export const loopTypes: Set<string> = new Set([
'controls_repeat',
'controls_repeat_ext',
'controls_forEach',
'controls_for',
'controls_whileUntil',
]);
/**
* Type of a block that has CONTROL_FLOW_IN_LOOP_CHECK_MIXIN
*
* @internal
*/
export type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin;
interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {}
type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN;
/**
* This mixin adds a check to make sure the 'controls_flow_statements' block
* is contained in a loop. Otherwise a warning is added to the block.
*/
const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
/**
* Is this block enclosed (at any level) by a loop?
*
* @returns The nearest surrounding loop, or null if none.
*/
getSurroundLoop: function (this: ControlFlowInLoopBlock): Block | null {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let block: Block | null = this;
do {
if (loopTypes.has(block.type)) {
return block;
}
block = block.getSurroundParent();
} while (block);
return null;
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
*/
onchange: function (this: ControlFlowInLoopBlock, e: AbstractEvent) {
const ws = this.workspace as WorkspaceSvg;
// Don't change state if:
// * It's at the start of a drag.
// * It's not a move event.
if (!ws.isDragging || ws.isDragging() || e.type !== Events.BLOCK_MOVE) {
return;
}
const enabled = !!this.getSurroundLoop();
this.setWarningText(
enabled ? null : Msg['CONTROLS_FLOW_STATEMENTS_WARNING'],
);
if (!this.isInFlyout) {
const group = Events.getGroup();
// Makes it so the move and the disable event get undone together.
Events.setGroup(e.group);
this.setEnabled(enabled);
Events.setGroup(group);
}
},
};
Extensions.registerMixin(
'controls_flow_in_loop_check',
CONTROL_FLOW_IN_LOOP_CHECK_MIXIN,
);
// Register provided blocks.
defineBlocks(blocks);