/** * @license * Copyright 2012 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Logic blocks for Blockly. * * This file is scraped to extract a .json file of block definitions. The array * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes * only, no outside references, no functions, no trailing commas, etc. The one * exception is end-of-line comments, which the scraper will remove. */ 'use strict'; goog.provide('Blockly.Blocks.logic'); // Deprecated goog.provide('Blockly.Constants.Logic'); goog.require('Blockly'); goog.require('Blockly.FieldDropdown'); goog.require('Blockly.FieldLabel'); goog.require('Blockly.Mutator'); /** * Unused constant for the common HSV hue for all blocks in this category. * @deprecated Use Blockly.Msg['LOGIC_HUE']. (2018 April 5) */ Blockly.Constants.Logic.HUE = 210; Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT // 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"], }, ]); // END JSON EXTRACT (Do not delete this comment.) Blockly.defineBlocksWithJsonArray([ // Mutator blocks. Do not extract. // 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}", }, ]); /** * Tooltip text, keyed by block OP value. Used by logic_compare and * logic_operation blocks. * @see {Blockly.Extensions#buildTooltipForDropdown} * @package * @readonly */ Blockly.Constants.Logic.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}', }; Blockly.Extensions.register('logic_op_tooltip', Blockly.Extensions.buildTooltipForDropdown( 'OP', Blockly.Constants.Logic.TOOLTIPS_BY_OP)); /** * Mutator methods added to controls_if blocks. * @mixin * @augments Blockly.Block * @package * @readonly */ Blockly.Constants.Logic.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 {Blockly.Block} */ mutationToDom: function() { if (!this.elseifCount_ && !this.elseCount_) { return null; } const container = Blockly.utils.xml.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 {Blockly.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 {!Blockly.Workspace} workspace Mutator's workspace. * @return {!Blockly.Block} Root block in mutator. * @this {Blockly.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 {!Blockly.Block} containerBlock Root block in mutator. * @this {Blockly.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 && !clauseBlock.isInsertionMarker()) { 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.nextConnection && clauseBlock.nextConnection.targetBlock(); } this.updateShape_(); // Reconnect any child blocks. this.reconnectChildBlocks_(valueConnections, statementConnections, elseStatementConnection); }, /** * Store pointers to any connected child blocks. * @param {!Blockly.Block} containerBlock Root block in mutator. * @this {Blockly.Block} */ saveConnections: function(containerBlock) { let clauseBlock = containerBlock.nextConnection.targetBlock(); let i = 1; while (clauseBlock) { 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.nextConnection && clauseBlock.nextConnection.targetBlock(); } }, /** * Reconstructs the block with all child blocks attached. * @this {Blockly.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 {Blockly.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(Blockly.Msg['CONTROLS_IF_MSG_ELSEIF']); this.appendStatementInput('DO' + i) .appendField(Blockly.Msg['CONTROLS_IF_MSG_THEN']); } if (this.elseCount_) { this.appendStatementInput('ELSE') .appendField(Blockly.Msg['CONTROLS_IF_MSG_ELSE']); } }, /** * Reconnects child blocks. * @param {!Array} valueConnections List of * value connections for 'if' input. * @param {!Array} statementConnections List of * statement connections for 'do' input. * @param {?Blockly.RenderedConnection} elseStatementConnection Statement * connection for else input. * @this {Blockly.Block} */ reconnectChildBlocks_: function(valueConnections, statementConnections, elseStatementConnection) { for (let i = 1; i <= this.elseifCount_; i++) { Blockly.Mutator.reconnect(valueConnections[i], this, 'IF' + i); Blockly.Mutator.reconnect(statementConnections[i], this, 'DO' + i); } Blockly.Mutator.reconnect(elseStatementConnection, this, 'ELSE'); }, }; Blockly.Extensions.registerMutator('controls_if_mutator', Blockly.Constants.Logic.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 {Blockly.Block} * @package */ Blockly.Constants.Logic.CONTROLS_IF_TOOLTIP_EXTENSION = function() { this.setTooltip(function() { if (!this.elseifCount_ && !this.elseCount_) { return Blockly.Msg['CONTROLS_IF_TOOLTIP_1']; } else if (!this.elseifCount_ && this.elseCount_) { return Blockly.Msg['CONTROLS_IF_TOOLTIP_2']; } else if (this.elseifCount_ && !this.elseCount_) { return Blockly.Msg['CONTROLS_IF_TOOLTIP_3']; } else if (this.elseifCount_ && this.elseCount_) { return Blockly.Msg['CONTROLS_IF_TOOLTIP_4']; } return ''; }.bind(this)); }; Blockly.Extensions.register('controls_if_tooltip', Blockly.Constants.Logic.CONTROLS_IF_TOOLTIP_EXTENSION); /** * Adds dynamic type validation for the left and right sides of a logic_compare * block. * @mixin * @augments Blockly.Block * @package * @readonly */ Blockly.Constants.Logic.LOGIC_COMPARE_ONCHANGE_MIXIN = { /** * Called whenever anything on the workspace changes. * Prevent mismatched types from being compared. * @param {!Blockly.Events.Abstract} e Change event. * @this {Blockly.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). Blockly.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(); Blockly.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 {Blockly.Block} * @package * @readonly */ Blockly.Constants.Logic.LOGIC_COMPARE_EXTENSION = function() { // Add onchange handler to ensure types are compatible. this.mixin(Blockly.Constants.Logic.LOGIC_COMPARE_ONCHANGE_MIXIN); }; Blockly.Extensions.register('logic_compare', Blockly.Constants.Logic.LOGIC_COMPARE_EXTENSION); /** * Adds type coordination between inputs and output. * @mixin * @augments Blockly.Block * @package * @readonly */ Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN = { prevParentConnection_: null, /** * Called whenever anything on the workspace changes. * Prevent mismatched types. * @param {!Blockly.Events.Abstract} e Change event. * @this {Blockly.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. Blockly.Events.setGroup(e.group); if (parentConnection === this.prevParentConnection_) { this.unplug(); parentConnection.getSourceBlock().bumpNeighbours(); } else { block.unplug(); block.bumpNeighbours(); } Blockly.Events.setGroup(false); } } } this.prevParentConnection_ = parentConnection; }, }; Blockly.Extensions.registerMixin('logic_ternary', Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN);