Files
blockly/blocks/logic.js
Beka Westberg 29e1f0cb03 fix: tsc errors picked up from develop (#6224)
* fix: relative path for deprecation utils

* fix: checking if properties exist in svg_math

* fix: set all timeout PIDs to AnyDuringMigration

* fix: make nullability errors explicity in block drag surface

* fix: make null check in events_block_change explicit

* fix: make getEventWorkspace_ internal so we can access it from CommentCreateDeleteHelper

* fix: rename DIV -> containerDiv in tooltip

* fix: ignore backwards compat check in category

* fix: set block styles to AnyDuringMigration

* fix: type typo in KeyboardShortcut

* fix: constants name in row measurables

* fix: typecast in mutator

* fix: populateProcedures type of flattened array

* fix: ignore errors related to workspace comment deserialization

* chore: format files

* fix: renaming imports missing file extensions

* fix: remove check for sound.play

* fix: temporarily remove bad requireType.

All `export type` statements are stripped when tsc is run. This means
that when we attempt to require BlockDefinition from the block files, we
get an error because it does not exist.

We decided to temporarily remove the require, because this will no
longer be a problem when we conver the blocks to typescript, and
everything gets compiled together.

* fix: bad jsdoc in array

* fix: silence missing property errors

Closure was complaining about inexistant properties, but they actually
do exist, they're just not being transpiled by tsc in a way that closure
understands.

I.E. if things are initialized in a function called by the constructor,
rather than in a class field or in the custructor itself, closure would
error.

It would also error on enums, because they are transpiled to a weird
IIFE.

* fix: context menu action handler not knowing the type of this.

this: TypeX information gets stripped when tsc is run, so closure could
not know that this was not global. Fixed this by reorganizing to use the
option object directly instead of passing it to onAction to be bound to
this.

* fix: readd getDeveloperVars checks (should not be part of migration)

This was found because ALL_DEVELOPER_VARS_WARNINGS_BY_BLOCK_TYPE was no
longer being accessed.

* fix: silence closure errors about overriding supertype props

We propertly define the overrides in typescript, but these get removed
from the compiled output, so closure doesn't know they exist.

* fix: silence globalThis errors

this: TypeX annotations get stripped from the compiled output, so
closure can't know that we're accessing the correct things. However,
typescript makes sure that this always has the correct properties, so
silencing this should be fine.

* fix: bad jsdoc name

* chore: attempt compiling with blockly.js

* fix: attempt moving the import statement above the namespace line

* chore: add todo comments to block def files

* chore: remove todo from context menu

* chore: add comments abotu disabled errors
2022-06-27 09:25:56 -07:00

666 lines
20 KiB
JavaScript

/**
* @license
* Copyright 2012 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Logic blocks for Blockly.
* @suppress {checkTypes}
*/
'use strict';
goog.module('Blockly.libraryBlocks.logic');
/* eslint-disable-next-line no-unused-vars */
const AbstractEvent = goog.requireType('Blockly.Events.Abstract');
const Events = goog.require('Blockly.Events');
const Extensions = goog.require('Blockly.Extensions');
const xmlUtils = goog.require('Blockly.utils.xml');
/* 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 {Msg} = goog.require('Blockly.Msg');
const {Mutator} = goog.require('Blockly.Mutator');
/* eslint-disable-next-line no-unused-vars */
const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldDropdown');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');
/**
* A dictionary of the block definitions provided by this module.
* @type {!Object<string, !BlockDefinition>}
*/
const blocks = createBlockDefinitionsFromJsonArray([
// Block for boolean data type: true and false.
{
'type': 'logic_boolean',
'message0': '%1',
'args0': [
{
'type': 'field_dropdown',
'name': 'BOOL',
'options': [
['%{BKY_LOGIC_BOOLEAN_TRUE}', 'TRUE'],
['%{BKY_LOGIC_BOOLEAN_FALSE}', 'FALSE'],
],
},
],
'output': 'Boolean',
'style': 'logic_blocks',
'tooltip': '%{BKY_LOGIC_BOOLEAN_TOOLTIP}',
'helpUrl': '%{BKY_LOGIC_BOOLEAN_HELPURL}',
},
// Block for if/elseif/else condition.
{
'type': 'controls_if',
'message0': '%{BKY_CONTROLS_IF_MSG_IF} %1',
'args0': [
{
'type': 'input_value',
'name': 'IF0',
'check': 'Boolean',
},
],
'message1': '%{BKY_CONTROLS_IF_MSG_THEN} %1',
'args1': [
{
'type': 'input_statement',
'name': 'DO0',
},
],
'previousStatement': null,
'nextStatement': null,
'style': 'logic_blocks',
'helpUrl': '%{BKY_CONTROLS_IF_HELPURL}',
'suppressPrefixSuffix': true,
'mutator': 'controls_if_mutator',
'extensions': ['controls_if_tooltip'],
},
// If/else block that does not use a mutator.
{
'type': 'controls_ifelse',
'message0': '%{BKY_CONTROLS_IF_MSG_IF} %1',
'args0': [
{
'type': 'input_value',
'name': 'IF0',
'check': 'Boolean',
},
],
'message1': '%{BKY_CONTROLS_IF_MSG_THEN} %1',
'args1': [
{
'type': 'input_statement',
'name': 'DO0',
},
],
'message2': '%{BKY_CONTROLS_IF_MSG_ELSE} %1',
'args2': [
{
'type': 'input_statement',
'name': 'ELSE',
},
],
'previousStatement': null,
'nextStatement': null,
'style': 'logic_blocks',
'tooltip': '%{BKYCONTROLS_IF_TOOLTIP_2}',
'helpUrl': '%{BKY_CONTROLS_IF_HELPURL}',
'suppressPrefixSuffix': true,
'extensions': ['controls_if_tooltip'],
},
// Block for comparison operator.
{
'type': 'logic_compare',
'message0': '%1 %2 %3',
'args0': [
{
'type': 'input_value',
'name': 'A',
},
{
'type': 'field_dropdown',
'name': 'OP',
'options': [
['=', 'EQ'],
['\u2260', 'NEQ'],
['\u200F<', 'LT'],
['\u200F\u2264', 'LTE'],
['\u200F>', 'GT'],
['\u200F\u2265', 'GTE'],
],
},
{
'type': 'input_value',
'name': 'B',
},
],
'inputsInline': true,
'output': 'Boolean',
'style': 'logic_blocks',
'helpUrl': '%{BKY_LOGIC_COMPARE_HELPURL}',
'extensions': ['logic_compare', 'logic_op_tooltip'],
},
// Block for logical operations: 'and', 'or'.
{
'type': 'logic_operation',
'message0': '%1 %2 %3',
'args0': [
{
'type': 'input_value',
'name': 'A',
'check': 'Boolean',
},
{
'type': 'field_dropdown',
'name': 'OP',
'options': [
['%{BKY_LOGIC_OPERATION_AND}', 'AND'],
['%{BKY_LOGIC_OPERATION_OR}', 'OR'],
],
},
{
'type': 'input_value',
'name': 'B',
'check': 'Boolean',
},
],
'inputsInline': true,
'output': 'Boolean',
'style': 'logic_blocks',
'helpUrl': '%{BKY_LOGIC_OPERATION_HELPURL}',
'extensions': ['logic_op_tooltip'],
},
// Block for negation.
{
'type': 'logic_negate',
'message0': '%{BKY_LOGIC_NEGATE_TITLE}',
'args0': [
{
'type': 'input_value',
'name': 'BOOL',
'check': 'Boolean',
},
],
'output': 'Boolean',
'style': 'logic_blocks',
'tooltip': '%{BKY_LOGIC_NEGATE_TOOLTIP}',
'helpUrl': '%{BKY_LOGIC_NEGATE_HELPURL}',
},
// Block for null data type.
{
'type': 'logic_null',
'message0': '%{BKY_LOGIC_NULL}',
'output': null,
'style': 'logic_blocks',
'tooltip': '%{BKY_LOGIC_NULL_TOOLTIP}',
'helpUrl': '%{BKY_LOGIC_NULL_HELPURL}',
},
// Block for ternary operator.
{
'type': 'logic_ternary',
'message0': '%{BKY_LOGIC_TERNARY_CONDITION} %1',
'args0': [
{
'type': 'input_value',
'name': 'IF',
'check': 'Boolean',
},
],
'message1': '%{BKY_LOGIC_TERNARY_IF_TRUE} %1',
'args1': [
{
'type': 'input_value',
'name': 'THEN',
},
],
'message2': '%{BKY_LOGIC_TERNARY_IF_FALSE} %1',
'args2': [
{
'type': 'input_value',
'name': 'ELSE',
},
],
'output': null,
'style': 'logic_blocks',
'tooltip': '%{BKY_LOGIC_TERNARY_TOOLTIP}',
'helpUrl': '%{BKY_LOGIC_TERNARY_HELPURL}',
'extensions': ['logic_ternary'],
},
// Block representing the if statement in the controls_if mutator.
{
'type': 'controls_if_if',
'message0': '%{BKY_CONTROLS_IF_IF_TITLE_IF}',
'nextStatement': null,
'enableContextMenu': false,
'style': 'logic_blocks',
'tooltip': '%{BKY_CONTROLS_IF_IF_TOOLTIP}',
},
// Block representing the else-if statement in the controls_if mutator.
{
'type': 'controls_if_elseif',
'message0': '%{BKY_CONTROLS_IF_ELSEIF_TITLE_ELSEIF}',
'previousStatement': null,
'nextStatement': null,
'enableContextMenu': false,
'style': 'logic_blocks',
'tooltip': '%{BKY_CONTROLS_IF_ELSEIF_TOOLTIP}',
},
// Block representing the else statement in the controls_if mutator.
{
'type': 'controls_if_else',
'message0': '%{BKY_CONTROLS_IF_ELSE_TITLE_ELSE}',
'previousStatement': null,
'enableContextMenu': false,
'style': 'logic_blocks',
'tooltip': '%{BKY_CONTROLS_IF_ELSE_TOOLTIP}',
},
]);
exports.blocks = blocks;
/**
* Tooltip text, keyed by block OP value. Used by logic_compare and
* logic_operation blocks.
* @see {Extensions#buildTooltipForDropdown}
* @readonly
*/
const TOOLTIPS_BY_OP = {
// logic_compare
'EQ': '%{BKY_LOGIC_COMPARE_TOOLTIP_EQ}',
'NEQ': '%{BKY_LOGIC_COMPARE_TOOLTIP_NEQ}',
'LT': '%{BKY_LOGIC_COMPARE_TOOLTIP_LT}',
'LTE': '%{BKY_LOGIC_COMPARE_TOOLTIP_LTE}',
'GT': '%{BKY_LOGIC_COMPARE_TOOLTIP_GT}',
'GTE': '%{BKY_LOGIC_COMPARE_TOOLTIP_GTE}',
// logic_operation
'AND': '%{BKY_LOGIC_OPERATION_TOOLTIP_AND}',
'OR': '%{BKY_LOGIC_OPERATION_TOOLTIP_OR}',
};
Extensions.register(
'logic_op_tooltip',
Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP));
/**
* Mutator methods added to controls_if blocks.
* @mixin
* @augments Block
* @readonly
*/
const CONTROLS_IF_MUTATOR_MIXIN = {
elseifCount_: 0,
elseCount_: 0,
/**
* Create XML to represent the number of else-if and else inputs.
* Backwards compatible serialization implementation.
* @return {Element} XML storage element.
* @this {Block}
*/
mutationToDom: function() {
if (!this.elseifCount_ && !this.elseCount_) {
return null;
}
const container = xmlUtils.createElement('mutation');
if (this.elseifCount_) {
container.setAttribute('elseif', this.elseifCount_);
}
if (this.elseCount_) {
container.setAttribute('else', 1);
}
return container;
},
/**
* Parse XML to restore the else-if and else inputs.
* Backwards compatible serialization implementation.
* @param {!Element} xmlElement XML storage element.
* @this {Block}
*/
domToMutation: function(xmlElement) {
this.elseifCount_ = parseInt(xmlElement.getAttribute('elseif'), 10) || 0;
this.elseCount_ = parseInt(xmlElement.getAttribute('else'), 10) || 0;
this.rebuildShape_();
},
/**
* Returns the state of this block as a JSON serializable object.
* @return {?{elseIfCount: (number|undefined), haseElse: (boolean|undefined)}}
* The state of this block, ie the else if count and else state.
*/
saveExtraState: function() {
if (!this.elseifCount_ && !this.elseCount_) {
return null;
}
const state = Object.create(null);
if (this.elseifCount_) {
state['elseIfCount'] = this.elseifCount_;
}
if (this.elseCount_) {
state['hasElse'] = true;
}
return state;
},
/**
* Applies the given state to this block.
* @param {*} state The state to apply to this block, ie the else if count and
* else state.
*/
loadExtraState: function(state) {
this.elseifCount_ = state['elseIfCount'] || 0;
this.elseCount_ = state['hasElse'] ? 1 : 0;
this.updateShape_();
},
/**
* Populate the mutator's dialog with this block's components.
* @param {!Workspace} workspace Mutator's workspace.
* @return {!Block} Root block in mutator.
* @this {Block}
*/
decompose: function(workspace) {
const containerBlock = workspace.newBlock('controls_if_if');
containerBlock.initSvg();
let connection = containerBlock.nextConnection;
for (let i = 1; i <= this.elseifCount_; i++) {
const elseifBlock = workspace.newBlock('controls_if_elseif');
elseifBlock.initSvg();
connection.connect(elseifBlock.previousConnection);
connection = elseifBlock.nextConnection;
}
if (this.elseCount_) {
const elseBlock = workspace.newBlock('controls_if_else');
elseBlock.initSvg();
connection.connect(elseBlock.previousConnection);
}
return containerBlock;
},
/**
* Reconfigure this block based on the mutator dialog's components.
* @param {!Block} containerBlock Root block in mutator.
* @this {Block}
*/
compose: function(containerBlock) {
let clauseBlock = containerBlock.nextConnection.targetBlock();
// Count number of inputs.
this.elseifCount_ = 0;
this.elseCount_ = 0;
const valueConnections = [null];
const statementConnections = [null];
let elseStatementConnection = null;
while (clauseBlock) {
if (clauseBlock.isInsertionMarker()) {
clauseBlock = clauseBlock.getNextBlock();
continue;
}
switch (clauseBlock.type) {
case 'controls_if_elseif':
this.elseifCount_++;
valueConnections.push(clauseBlock.valueConnection_);
statementConnections.push(clauseBlock.statementConnection_);
break;
case 'controls_if_else':
this.elseCount_++;
elseStatementConnection = clauseBlock.statementConnection_;
break;
default:
throw TypeError('Unknown block type: ' + clauseBlock.type);
}
clauseBlock = clauseBlock.getNextBlock();
}
this.updateShape_();
// Reconnect any child blocks.
this.reconnectChildBlocks_(
valueConnections, statementConnections, elseStatementConnection);
},
/**
* Store pointers to any connected child blocks.
* @param {!Block} containerBlock Root block in mutator.
* @this {Block}
*/
saveConnections: function(containerBlock) {
let clauseBlock = containerBlock.nextConnection.targetBlock();
let i = 1;
while (clauseBlock) {
if (clauseBlock.isInsertionMarker()) {
clauseBlock = clauseBlock.getNextBlock();
continue;
}
switch (clauseBlock.type) {
case 'controls_if_elseif': {
const inputIf = this.getInput('IF' + i);
const inputDo = this.getInput('DO' + i);
clauseBlock.valueConnection_ =
inputIf && inputIf.connection.targetConnection;
clauseBlock.statementConnection_ =
inputDo && inputDo.connection.targetConnection;
i++;
break;
}
case 'controls_if_else': {
const inputDo = this.getInput('ELSE');
clauseBlock.statementConnection_ =
inputDo && inputDo.connection.targetConnection;
break;
}
default:
throw TypeError('Unknown block type: ' + clauseBlock.type);
}
clauseBlock = clauseBlock.getNextBlock();
}
},
/**
* Reconstructs the block with all child blocks attached.
* @this {Block}
*/
rebuildShape_: function() {
const valueConnections = [null];
const statementConnections = [null];
let elseStatementConnection = null;
if (this.getInput('ELSE')) {
elseStatementConnection =
this.getInput('ELSE').connection.targetConnection;
}
for (let i = 1; this.getInput('IF' + i); i++) {
const inputIf = this.getInput('IF' + i);
const inputDo = this.getInput('DO' + i);
valueConnections.push(inputIf.connection.targetConnection);
statementConnections.push(inputDo.connection.targetConnection);
}
this.updateShape_();
this.reconnectChildBlocks_(
valueConnections, statementConnections, elseStatementConnection);
},
/**
* Modify this block to have the correct number of inputs.
* @this {Block}
* @private
*/
updateShape_: function() {
// Delete everything.
if (this.getInput('ELSE')) {
this.removeInput('ELSE');
}
for (let i = 1; this.getInput('IF' + i); i++) {
this.removeInput('IF' + i);
this.removeInput('DO' + i);
}
// Rebuild block.
for (let i = 1; i <= this.elseifCount_; i++) {
this.appendValueInput('IF' + i).setCheck('Boolean').appendField(
Msg['CONTROLS_IF_MSG_ELSEIF']);
this.appendStatementInput('DO' + i).appendField(
Msg['CONTROLS_IF_MSG_THEN']);
}
if (this.elseCount_) {
this.appendStatementInput('ELSE').appendField(
Msg['CONTROLS_IF_MSG_ELSE']);
}
},
/**
* Reconnects child blocks.
* @param {!Array<?RenderedConnection>} valueConnections List of
* value connections for 'if' input.
* @param {!Array<?RenderedConnection>} statementConnections List of
* statement connections for 'do' input.
* @param {?RenderedConnection} elseStatementConnection Statement
* connection for else input.
* @this {Block}
*/
reconnectChildBlocks_: function(
valueConnections, statementConnections, elseStatementConnection) {
for (let i = 1; i <= this.elseifCount_; i++) {
Mutator.reconnect(valueConnections[i], this, 'IF' + i);
Mutator.reconnect(statementConnections[i], this, 'DO' + i);
}
Mutator.reconnect(elseStatementConnection, this, 'ELSE');
},
};
Extensions.registerMutator(
'controls_if_mutator', CONTROLS_IF_MUTATOR_MIXIN, null,
['controls_if_elseif', 'controls_if_else']);
/**
* "controls_if" extension function. Adds mutator, shape updating methods, and
* dynamic tooltip to "controls_if" blocks.
* @this {Block}
*/
const CONTROLS_IF_TOOLTIP_EXTENSION = function() {
this.setTooltip(function() {
if (!this.elseifCount_ && !this.elseCount_) {
return Msg['CONTROLS_IF_TOOLTIP_1'];
} else if (!this.elseifCount_ && this.elseCount_) {
return Msg['CONTROLS_IF_TOOLTIP_2'];
} else if (this.elseifCount_ && !this.elseCount_) {
return Msg['CONTROLS_IF_TOOLTIP_3'];
} else if (this.elseifCount_ && this.elseCount_) {
return Msg['CONTROLS_IF_TOOLTIP_4'];
}
return '';
}.bind(this));
};
Extensions.register('controls_if_tooltip', CONTROLS_IF_TOOLTIP_EXTENSION);
/**
* Adds dynamic type validation for the left and right sides of a logic_compare
* block.
* @mixin
* @augments Block
* @readonly
*/
const LOGIC_COMPARE_ONCHANGE_MIXIN = {
/**
* Called whenever anything on the workspace changes.
* Prevent mismatched types from being compared.
* @param {!AbstractEvent} e Change event.
* @this {Block}
*/
onchange: function(e) {
if (!this.prevBlocks_) {
this.prevBlocks_ = [null, null];
}
const blockA = this.getInputTargetBlock('A');
const blockB = this.getInputTargetBlock('B');
// Disconnect blocks that existed prior to this change if they don't match.
if (blockA && blockB &&
!this.workspace.connectionChecker.doTypeChecks(
blockA.outputConnection, blockB.outputConnection)) {
// Mismatch between two inputs. Revert the block connections,
// bumping away the newly connected block(s).
Events.setGroup(e.group);
const prevA = this.prevBlocks_[0];
if (prevA !== blockA) {
blockA.unplug();
if (prevA && !prevA.isDisposed() && !prevA.isShadow()) {
// The shadow block is automatically replaced during unplug().
this.getInput('A').connection.connect(prevA.outputConnection);
}
}
const prevB = this.prevBlocks_[1];
if (prevB !== blockB) {
blockB.unplug();
if (prevB && !prevB.isDisposed() && !prevB.isShadow()) {
// The shadow block is automatically replaced during unplug().
this.getInput('B').connection.connect(prevB.outputConnection);
}
}
this.bumpNeighbours();
Events.setGroup(false);
}
this.prevBlocks_[0] = this.getInputTargetBlock('A');
this.prevBlocks_[1] = this.getInputTargetBlock('B');
},
};
/**
* "logic_compare" extension function. Adds type left and right side type
* checking to "logic_compare" blocks.
* @this {Block}
* @readonly
*/
const LOGIC_COMPARE_EXTENSION = function() {
// Add onchange handler to ensure types are compatible.
this.mixin(LOGIC_COMPARE_ONCHANGE_MIXIN);
};
Extensions.register('logic_compare', LOGIC_COMPARE_EXTENSION);
/**
* Adds type coordination between inputs and output.
* @mixin
* @augments Block
* @readonly
*/
const LOGIC_TERNARY_ONCHANGE_MIXIN = {
prevParentConnection_: null,
/**
* Called whenever anything on the workspace changes.
* Prevent mismatched types.
* @param {!AbstractEvent} e Change event.
* @this {Block}
*/
onchange: function(e) {
const blockA = this.getInputTargetBlock('THEN');
const blockB = this.getInputTargetBlock('ELSE');
const parentConnection = this.outputConnection.targetConnection;
// Disconnect blocks that existed prior to this change if they don't match.
if ((blockA || blockB) && parentConnection) {
for (let i = 0; i < 2; i++) {
const block = (i === 1) ? blockA : blockB;
if (block &&
!block.workspace.connectionChecker.doTypeChecks(
block.outputConnection, parentConnection)) {
// Ensure that any disconnections are grouped with the causing event.
Events.setGroup(e.group);
if (parentConnection === this.prevParentConnection_) {
this.unplug();
parentConnection.getSourceBlock().bumpNeighbours();
} else {
block.unplug();
block.bumpNeighbours();
}
Events.setGroup(false);
}
}
}
this.prevParentConnection_ = parentConnection;
},
};
Extensions.registerMixin('logic_ternary', LOGIC_TERNARY_ONCHANGE_MIXIN);
// Register provided blocks.
defineBlocks(blocks);