diff options
Diffstat (limited to 'src/blockly/blocks')
-rw-r--r-- | src/blockly/blocks/colour.js | 133 | ||||
-rw-r--r-- | src/blockly/blocks/lists.js | 836 | ||||
-rw-r--r-- | src/blockly/blocks/logic.js | 488 | ||||
-rw-r--r-- | src/blockly/blocks/loops.js | 283 | ||||
-rw-r--r-- | src/blockly/blocks/math.js | 553 | ||||
-rw-r--r-- | src/blockly/blocks/procedures.js | 875 | ||||
-rw-r--r-- | src/blockly/blocks/text.js | 685 | ||||
-rw-r--r-- | src/blockly/blocks/variables.js | 100 |
8 files changed, 3953 insertions, 0 deletions
diff --git a/src/blockly/blocks/colour.js b/src/blockly/blocks/colour.js new file mode 100644 index 0000000..ceb9efc --- /dev/null +++ b/src/blockly/blocks/colour.js @@ -0,0 +1,133 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Colour blocks for Blockly. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Blocks.colour'); + +goog.require('Blockly.Blocks'); + + +/** + * Common HSV hue for all blocks in this category. + */ +Blockly.Blocks.colour.HUE = 20; + +Blockly.Blocks['colour_picker'] = { + /** + * Block for colour picker. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": "%1", + "args0": [ + { + "type": "field_colour", + "name": "COLOUR", + "colour": "#ff0000" + } + ], + "output": "Colour", + "colour": Blockly.Blocks.colour.HUE, + "helpUrl": Blockly.Msg.COLOUR_PICKER_HELPURL + }); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + // Colour block is trivial. Use tooltip of parent block if it exists. + this.setTooltip(function() { + var parent = thisBlock.getParent(); + return (parent && parent.getInputsInline() && parent.tooltip) || + Blockly.Msg.COLOUR_PICKER_TOOLTIP; + }); + } +}; + +Blockly.Blocks['colour_random'] = { + /** + * Block for random colour. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.COLOUR_RANDOM_TITLE, + "output": "Colour", + "colour": Blockly.Blocks.colour.HUE, + "tooltip": Blockly.Msg.COLOUR_RANDOM_TOOLTIP, + "helpUrl": Blockly.Msg.COLOUR_RANDOM_HELPURL + }); + } +}; + +Blockly.Blocks['colour_rgb'] = { + /** + * Block for composing a colour from RGB components. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.COLOUR_RGB_HELPURL); + this.setColour(Blockly.Blocks.colour.HUE); + this.appendValueInput('RED') + .setCheck('Number') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(Blockly.Msg.COLOUR_RGB_TITLE) + .appendField(Blockly.Msg.COLOUR_RGB_RED); + this.appendValueInput('GREEN') + .setCheck('Number') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(Blockly.Msg.COLOUR_RGB_GREEN); + this.appendValueInput('BLUE') + .setCheck('Number') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(Blockly.Msg.COLOUR_RGB_BLUE); + this.setOutput(true, 'Colour'); + this.setTooltip(Blockly.Msg.COLOUR_RGB_TOOLTIP); + } +}; + +Blockly.Blocks['colour_blend'] = { + /** + * Block for blending two colours together. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.COLOUR_BLEND_HELPURL); + this.setColour(Blockly.Blocks.colour.HUE); + this.appendValueInput('COLOUR1') + .setCheck('Colour') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(Blockly.Msg.COLOUR_BLEND_TITLE) + .appendField(Blockly.Msg.COLOUR_BLEND_COLOUR1); + this.appendValueInput('COLOUR2') + .setCheck('Colour') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(Blockly.Msg.COLOUR_BLEND_COLOUR2); + this.appendValueInput('RATIO') + .setCheck('Number') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(Blockly.Msg.COLOUR_BLEND_RATIO); + this.setOutput(true, 'Colour'); + this.setTooltip(Blockly.Msg.COLOUR_BLEND_TOOLTIP); + } +}; diff --git a/src/blockly/blocks/lists.js b/src/blockly/blocks/lists.js new file mode 100644 index 0000000..55dfac0 --- /dev/null +++ b/src/blockly/blocks/lists.js @@ -0,0 +1,836 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview List blocks for Blockly. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Blocks.lists'); + +goog.require('Blockly.Blocks'); + + +/** + * Common HSV hue for all blocks in this category. + */ +Blockly.Blocks.lists.HUE = 260; + +Blockly.Blocks['lists_create_empty'] = { + /** + * Block for creating an empty list. + * The 'list_create_with' block is preferred as it is more flexible. + * <block type="lists_create_with"> + * <mutation items="0"></mutation> + * </block> + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.LISTS_CREATE_EMPTY_TITLE, + "output": "Array", + "colour": Blockly.Blocks.lists.HUE, + "tooltip": Blockly.Msg.LISTS_CREATE_EMPTY_TOOLTIP, + "helpUrl": Blockly.Msg.LISTS_CREATE_EMPTY_HELPURL + }); + } +}; + +Blockly.Blocks['lists_create_with'] = { + /** + * Block for creating a list with any number of elements of any type. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.LISTS_CREATE_WITH_HELPURL); + this.setColour(Blockly.Blocks.lists.HUE); + this.itemCount_ = 3; + this.updateShape_(); + this.setOutput(true, 'Array'); + this.setMutator(new Blockly.Mutator(['lists_create_with_item'])); + this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_TOOLTIP); + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + container.setAttribute('items', this.itemCount_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); + 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) { + var containerBlock = workspace.newBlock('lists_create_with_container'); + containerBlock.initSvg(); + var connection = containerBlock.getInput('STACK').connection; + for (var i = 0; i < this.itemCount_; i++) { + var itemBlock = workspace.newBlock('lists_create_with_item'); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + 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) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + // Count number of inputs. + var connections = []; + while (itemBlock) { + connections.push(itemBlock.valueConnection_); + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + // Disconnect any children that don't belong. + for (var i = 0; i < this.itemCount_; i++) { + var connection = this.getInput('ADD' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) == -1) { + connection.disconnect(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (var i = 0; i < this.itemCount_; i++) { + Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); + } + }, + /** + * Store pointers to any connected child blocks. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + saveConnections: function(containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + var i = 0; + while (itemBlock) { + var input = this.getInput('ADD' + i); + itemBlock.valueConnection_ = input && input.connection.targetConnection; + i++; + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + }, + /** + * Modify this block to have the correct number of inputs. + * @private + * @this Blockly.Block + */ + updateShape_: function() { + if (this.itemCount_ && this.getInput('EMPTY')) { + this.removeInput('EMPTY'); + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { + this.appendDummyInput('EMPTY') + .appendField(Blockly.Msg.LISTS_CREATE_EMPTY_TITLE); + } + // Add new inputs. + for (var i = 0; i < this.itemCount_; i++) { + if (!this.getInput('ADD' + i)) { + var input = this.appendValueInput('ADD' + i); + if (i == 0) { + input.appendField(Blockly.Msg.LISTS_CREATE_WITH_INPUT_WITH); + } + } + } + // Remove deleted inputs. + while (this.getInput('ADD' + i)) { + this.removeInput('ADD' + i); + i++; + } + } +}; + +Blockly.Blocks['lists_create_with_container'] = { + /** + * Mutator block for list container. + * @this Blockly.Block + */ + init: function() { + this.setColour(Blockly.Blocks.lists.HUE); + this.appendDummyInput() + .appendField(Blockly.Msg.LISTS_CREATE_WITH_CONTAINER_TITLE_ADD); + this.appendStatementInput('STACK'); + this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_CONTAINER_TOOLTIP); + this.contextMenu = false; + } +}; + +Blockly.Blocks['lists_create_with_item'] = { + /** + * Mutator bolck for adding items. + * @this Blockly.Block + */ + init: function() { + this.setColour(Blockly.Blocks.lists.HUE); + this.appendDummyInput() + .appendField(Blockly.Msg.LISTS_CREATE_WITH_ITEM_TITLE); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_ITEM_TOOLTIP); + this.contextMenu = false; + } +}; + +Blockly.Blocks['lists_repeat'] = { + /** + * Block for creating a list with one element repeated. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.LISTS_REPEAT_TITLE, + "args0": [ + { + "type": "input_value", + "name": "ITEM" + }, + { + "type": "input_value", + "name": "NUM", + "check": "Number" + } + ], + "output": "Array", + "colour": Blockly.Blocks.lists.HUE, + "tooltip": Blockly.Msg.LISTS_REPEAT_TOOLTIP, + "helpUrl": Blockly.Msg.LISTS_REPEAT_HELPURL + }); + } +}; + +Blockly.Blocks['lists_length'] = { + /** + * Block for list length. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.LISTS_LENGTH_TITLE, + "args0": [ + { + "type": "input_value", + "name": "VALUE", + "check": ['String', 'Array'] + } + ], + "output": 'Number', + "colour": Blockly.Blocks.lists.HUE, + "tooltip": Blockly.Msg.LISTS_LENGTH_TOOLTIP, + "helpUrl": Blockly.Msg.LISTS_LENGTH_HELPURL + }); + } +}; + +Blockly.Blocks['lists_isEmpty'] = { + /** + * Block for is the list empty? + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.LISTS_ISEMPTY_TITLE, + "args0": [ + { + "type": "input_value", + "name": "VALUE", + "check": ['String', 'Array'] + } + ], + "output": 'Boolean', + "colour": Blockly.Blocks.lists.HUE, + "tooltip": Blockly.Msg.LISTS_ISEMPTY_TOOLTIP, + "helpUrl": Blockly.Msg.LISTS_ISEMPTY_HELPURL + }); + } +}; + +Blockly.Blocks['lists_indexOf'] = { + /** + * Block for finding an item in the list. + * @this Blockly.Block + */ + init: function() { + var OPERATORS = + [[Blockly.Msg.LISTS_INDEX_OF_FIRST, 'FIRST'], + [Blockly.Msg.LISTS_INDEX_OF_LAST, 'LAST']]; + this.setHelpUrl(Blockly.Msg.LISTS_INDEX_OF_HELPURL); + this.setColour(Blockly.Blocks.lists.HUE); + this.setOutput(true, 'Number'); + this.appendValueInput('VALUE') + .setCheck('Array') + .appendField(Blockly.Msg.LISTS_INDEX_OF_INPUT_IN_LIST); + this.appendValueInput('FIND') + .appendField(new Blockly.FieldDropdown(OPERATORS), 'END'); + this.setInputsInline(true); + var tooltip = Blockly.Msg.LISTS_INDEX_OF_TOOLTIP + .replace('%1', Blockly.Blocks.ONE_BASED_INDEXING ? '0' : '-1'); + this.setTooltip(tooltip); + } +}; + +Blockly.Blocks['lists_getIndex'] = { + /** + * Block for getting element at index. + * @this Blockly.Block + */ + init: function() { + var MODE = + [[Blockly.Msg.LISTS_GET_INDEX_GET, 'GET'], + [Blockly.Msg.LISTS_GET_INDEX_GET_REMOVE, 'GET_REMOVE'], + [Blockly.Msg.LISTS_GET_INDEX_REMOVE, 'REMOVE']]; + this.WHERE_OPTIONS = + [[Blockly.Msg.LISTS_GET_INDEX_FROM_START, 'FROM_START'], + [Blockly.Msg.LISTS_GET_INDEX_FROM_END, 'FROM_END'], + [Blockly.Msg.LISTS_GET_INDEX_FIRST, 'FIRST'], + [Blockly.Msg.LISTS_GET_INDEX_LAST, 'LAST'], + [Blockly.Msg.LISTS_GET_INDEX_RANDOM, 'RANDOM']]; + this.setHelpUrl(Blockly.Msg.LISTS_GET_INDEX_HELPURL); + this.setColour(Blockly.Blocks.lists.HUE); + var modeMenu = new Blockly.FieldDropdown(MODE, function(value) { + var isStatement = (value == 'REMOVE'); + this.sourceBlock_.updateStatement_(isStatement); + }); + this.appendValueInput('VALUE') + .setCheck('Array') + .appendField(Blockly.Msg.LISTS_GET_INDEX_INPUT_IN_LIST); + this.appendDummyInput() + .appendField(modeMenu, 'MODE') + .appendField('', 'SPACE'); + this.appendDummyInput('AT'); + if (Blockly.Msg.LISTS_GET_INDEX_TAIL) { + this.appendDummyInput('TAIL') + .appendField(Blockly.Msg.LISTS_GET_INDEX_TAIL); + } + this.setInputsInline(true); + this.setOutput(true); + this.updateAt_(true); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var mode = thisBlock.getFieldValue('MODE'); + var where = thisBlock.getFieldValue('WHERE'); + var tooltip = ''; + switch (mode + ' ' + where) { + case 'GET FROM_START': + case 'GET FROM_END': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FROM; + break; + case 'GET FIRST': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FIRST; + break; + case 'GET LAST': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_LAST; + break; + case 'GET RANDOM': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_RANDOM; + break; + case 'GET_REMOVE FROM_START': + case 'GET_REMOVE FROM_END': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM; + break; + case 'GET_REMOVE FIRST': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST; + break; + case 'GET_REMOVE LAST': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST; + break; + case 'GET_REMOVE RANDOM': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM; + break; + case 'REMOVE FROM_START': + case 'REMOVE FROM_END': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM; + break; + case 'REMOVE FIRST': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST; + break; + case 'REMOVE LAST': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST; + break; + case 'REMOVE RANDOM': + tooltip = Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM; + break; + } + if (where == 'FROM_START' || where == 'FROM_END') { + tooltip += ' ' + Blockly.Msg.LISTS_INDEX_FROM_START_TOOLTIP + .replace('%1', Blockly.Blocks.ONE_BASED_INDEXING ? '#1' : '#0'); + } + return tooltip; + }); + }, + /** + * Create XML to represent whether the block is a statement or a value. + * Also represent whether there is an 'AT' input. + * @return {Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + var isStatement = !this.outputConnection; + container.setAttribute('statement', isStatement); + var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE; + container.setAttribute('at', isAt); + return container; + }, + /** + * Parse XML to restore the 'AT' input. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + // Note: Until January 2013 this block did not have mutations, + // so 'statement' defaults to false and 'at' defaults to true. + var isStatement = (xmlElement.getAttribute('statement') == 'true'); + this.updateStatement_(isStatement); + var isAt = (xmlElement.getAttribute('at') != 'false'); + this.updateAt_(isAt); + }, + /** + * Switch between a value block and a statement block. + * @param {boolean} newStatement True if the block should be a statement. + * False if the block should be a value. + * @private + * @this Blockly.Block + */ + updateStatement_: function(newStatement) { + var oldStatement = !this.outputConnection; + if (newStatement != oldStatement) { + this.unplug(true, true); + if (newStatement) { + this.setOutput(false); + this.setPreviousStatement(true); + this.setNextStatement(true); + } else { + this.setPreviousStatement(false); + this.setNextStatement(false); + this.setOutput(true); + } + } + }, + /** + * Create or delete an input for the numeric index. + * @param {boolean} isAt True if the input should exist. + * @private + * @this Blockly.Block + */ + updateAt_: function(isAt) { + // Destroy old 'AT' and 'ORDINAL' inputs. + this.removeInput('AT'); + this.removeInput('ORDINAL', true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT').setCheck('Number'); + if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) { + this.appendDummyInput('ORDINAL') + .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX); + } + } else { + this.appendDummyInput('AT'); + } + var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) { + var newAt = (value == 'FROM_START') || (value == 'FROM_END'); + // The 'isAt' variable is available due to this function being a closure. + if (newAt != isAt) { + var block = this.sourceBlock_; + block.updateAt_(newAt); + // This menu has been destroyed and replaced. Update the replacement. + block.setFieldValue(value, 'WHERE'); + return null; + } + return undefined; + }); + this.getInput('AT').appendField(menu, 'WHERE'); + if (Blockly.Msg.LISTS_GET_INDEX_TAIL) { + this.moveInputBefore('TAIL', null); + } + } +}; + +Blockly.Blocks['lists_setIndex'] = { + /** + * Block for setting the element at index. + * @this Blockly.Block + */ + init: function() { + var MODE = + [[Blockly.Msg.LISTS_SET_INDEX_SET, 'SET'], + [Blockly.Msg.LISTS_SET_INDEX_INSERT, 'INSERT']]; + this.WHERE_OPTIONS = + [[Blockly.Msg.LISTS_GET_INDEX_FROM_START, 'FROM_START'], + [Blockly.Msg.LISTS_GET_INDEX_FROM_END, 'FROM_END'], + [Blockly.Msg.LISTS_GET_INDEX_FIRST, 'FIRST'], + [Blockly.Msg.LISTS_GET_INDEX_LAST, 'LAST'], + [Blockly.Msg.LISTS_GET_INDEX_RANDOM, 'RANDOM']]; + this.setHelpUrl(Blockly.Msg.LISTS_SET_INDEX_HELPURL); + this.setColour(Blockly.Blocks.lists.HUE); + this.appendValueInput('LIST') + .setCheck('Array') + .appendField(Blockly.Msg.LISTS_SET_INDEX_INPUT_IN_LIST); + this.appendDummyInput() + .appendField(new Blockly.FieldDropdown(MODE), 'MODE') + .appendField('', 'SPACE'); + this.appendDummyInput('AT'); + this.appendValueInput('TO') + .appendField(Blockly.Msg.LISTS_SET_INDEX_INPUT_TO); + this.setInputsInline(true); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.LISTS_SET_INDEX_TOOLTIP); + this.updateAt_(true); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var mode = thisBlock.getFieldValue('MODE'); + var where = thisBlock.getFieldValue('WHERE'); + var tooltip = ''; + switch (mode + ' ' + where) { + case 'SET FROM_START': + case 'SET FROM_END': + tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FROM; + break; + case 'SET FIRST': + tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FIRST; + break; + case 'SET LAST': + tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_LAST; + break; + case 'SET RANDOM': + tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_RANDOM; + break; + case 'INSERT FROM_START': + case 'INSERT FROM_END': + tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FROM; + break; + case 'INSERT FIRST': + tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST; + break; + case 'INSERT LAST': + tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_LAST; + break; + case 'INSERT RANDOM': + tooltip = Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM; + break; + } + if (where == 'FROM_START' || where == 'FROM_END') { + tooltip += ' ' + Blockly.Msg.LISTS_INDEX_FROM_START_TOOLTIP + .replace('%1', Blockly.Blocks.ONE_BASED_INDEXING ? '#1' : '#0'); + } + return tooltip; + }); + }, + /** + * Create XML to represent whether there is an 'AT' input. + * @return {Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE; + container.setAttribute('at', isAt); + return container; + }, + /** + * Parse XML to restore the 'AT' input. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + // Note: Until January 2013 this block did not have mutations, + // so 'at' defaults to true. + var isAt = (xmlElement.getAttribute('at') != 'false'); + this.updateAt_(isAt); + }, + /** + * Create or delete an input for the numeric index. + * @param {boolean} isAt True if the input should exist. + * @private + * @this Blockly.Block + */ + updateAt_: function(isAt) { + // Destroy old 'AT' and 'ORDINAL' input. + this.removeInput('AT'); + this.removeInput('ORDINAL', true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT').setCheck('Number'); + if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) { + this.appendDummyInput('ORDINAL') + .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX); + } + } else { + this.appendDummyInput('AT'); + } + var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) { + var newAt = (value == 'FROM_START') || (value == 'FROM_END'); + // The 'isAt' variable is available due to this function being a closure. + if (newAt != isAt) { + var block = this.sourceBlock_; + block.updateAt_(newAt); + // This menu has been destroyed and replaced. Update the replacement. + block.setFieldValue(value, 'WHERE'); + return null; + } + return undefined; + }); + this.moveInputBefore('AT', 'TO'); + if (this.getInput('ORDINAL')) { + this.moveInputBefore('ORDINAL', 'TO'); + } + + this.getInput('AT').appendField(menu, 'WHERE'); + } +}; + +Blockly.Blocks['lists_getSublist'] = { + /** + * Block for getting sublist. + * @this Blockly.Block + */ + init: function() { + this['WHERE_OPTIONS_1'] = + [[Blockly.Msg.LISTS_GET_SUBLIST_START_FROM_START, 'FROM_START'], + [Blockly.Msg.LISTS_GET_SUBLIST_START_FROM_END, 'FROM_END'], + [Blockly.Msg.LISTS_GET_SUBLIST_START_FIRST, 'FIRST']]; + this['WHERE_OPTIONS_2'] = + [[Blockly.Msg.LISTS_GET_SUBLIST_END_FROM_START, 'FROM_START'], + [Blockly.Msg.LISTS_GET_SUBLIST_END_FROM_END, 'FROM_END'], + [Blockly.Msg.LISTS_GET_SUBLIST_END_LAST, 'LAST']]; + this.setHelpUrl(Blockly.Msg.LISTS_GET_SUBLIST_HELPURL); + this.setColour(Blockly.Blocks.lists.HUE); + this.appendValueInput('LIST') + .setCheck('Array') + .appendField(Blockly.Msg.LISTS_GET_SUBLIST_INPUT_IN_LIST); + this.appendDummyInput('AT1'); + this.appendDummyInput('AT2'); + if (Blockly.Msg.LISTS_GET_SUBLIST_TAIL) { + this.appendDummyInput('TAIL') + .appendField(Blockly.Msg.LISTS_GET_SUBLIST_TAIL); + } + this.setInputsInline(true); + this.setOutput(true, 'Array'); + this.updateAt_(1, true); + this.updateAt_(2, true); + this.setTooltip(Blockly.Msg.LISTS_GET_SUBLIST_TOOLTIP); + }, + /** + * Create XML to represent whether there are 'AT' inputs. + * @return {Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + var isAt1 = this.getInput('AT1').type == Blockly.INPUT_VALUE; + container.setAttribute('at1', isAt1); + var isAt2 = this.getInput('AT2').type == Blockly.INPUT_VALUE; + container.setAttribute('at2', isAt2); + return container; + }, + /** + * Parse XML to restore the 'AT' inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + var isAt1 = (xmlElement.getAttribute('at1') == 'true'); + var isAt2 = (xmlElement.getAttribute('at2') == 'true'); + this.updateAt_(1, isAt1); + this.updateAt_(2, isAt2); + }, + /** + * Create or delete an input for a numeric index. + * This block has two such inputs, independant of each other. + * @param {number} n Specify first or second input (1 or 2). + * @param {boolean} isAt True if the input should exist. + * @private + * @this Blockly.Block + */ + updateAt_: function(n, isAt) { + // Create or delete an input for the numeric index. + // Destroy old 'AT' and 'ORDINAL' inputs. + this.removeInput('AT' + n); + this.removeInput('ORDINAL' + n, true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT' + n).setCheck('Number'); + if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) { + this.appendDummyInput('ORDINAL' + n) + .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX); + } + } else { + this.appendDummyInput('AT' + n); + } + var menu = new Blockly.FieldDropdown(this['WHERE_OPTIONS_' + n], + function(value) { + var newAt = (value == 'FROM_START') || (value == 'FROM_END'); + // The 'isAt' variable is available due to this function being a + // closure. + if (newAt != isAt) { + var block = this.sourceBlock_; + block.updateAt_(n, newAt); + // This menu has been destroyed and replaced. + // Update the replacement. + block.setFieldValue(value, 'WHERE' + n); + return null; + } + return undefined; + }); + this.getInput('AT' + n) + .appendField(menu, 'WHERE' + n); + if (n == 1) { + this.moveInputBefore('AT1', 'AT2'); + if (this.getInput('ORDINAL1')) { + this.moveInputBefore('ORDINAL1', 'AT2'); + } + } + if (Blockly.Msg.LISTS_GET_SUBLIST_TAIL) { + this.moveInputBefore('TAIL', null); + } + } +}; + +Blockly.Blocks['lists_sort'] = { + /** + * Block for sorting a list. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.LISTS_SORT_TITLE, + "args0": [ + { + "type": "field_dropdown", + "name": "TYPE", + "options": [ + [Blockly.Msg.LISTS_SORT_TYPE_NUMERIC, "NUMERIC"], + [Blockly.Msg.LISTS_SORT_TYPE_TEXT, "TEXT"], + [Blockly.Msg.LISTS_SORT_TYPE_IGNORECASE, "IGNORE_CASE"] + ] + }, + { + "type": "field_dropdown", + "name": "DIRECTION", + "options": [ + [Blockly.Msg.LISTS_SORT_ORDER_ASCENDING, "1"], + [Blockly.Msg.LISTS_SORT_ORDER_DESCENDING, "-1"] + ] + }, + { + "type": "input_value", + "name": "LIST", + "check": "Array" + } + ], + "output": "Array", + "colour": Blockly.Blocks.lists.HUE, + "tooltip": Blockly.Msg.LISTS_SORT_TOOLTIP, + "helpUrl": Blockly.Msg.LISTS_SORT_HELPURL + }); + } +}; + +Blockly.Blocks['lists_split'] = { + /** + * Block for splitting text into a list, or joining a list into text. + * @this Blockly.Block + */ + init: function() { + // Assign 'this' to a variable for use in the closures below. + var thisBlock = this; + var dropdown = new Blockly.FieldDropdown( + [[Blockly.Msg.LISTS_SPLIT_LIST_FROM_TEXT, 'SPLIT'], + [Blockly.Msg.LISTS_SPLIT_TEXT_FROM_LIST, 'JOIN']], + function(newMode) { + thisBlock.updateType_(newMode); + }); + this.setHelpUrl(Blockly.Msg.LISTS_SPLIT_HELPURL); + this.setColour(Blockly.Blocks.lists.HUE); + this.appendValueInput('INPUT') + .setCheck('String') + .appendField(dropdown, 'MODE'); + this.appendValueInput('DELIM') + .setCheck('String') + .appendField(Blockly.Msg.LISTS_SPLIT_WITH_DELIMITER); + this.setInputsInline(true); + this.setOutput(true, 'Array'); + this.setTooltip(function() { + var mode = thisBlock.getFieldValue('MODE'); + if (mode == 'SPLIT') { + return Blockly.Msg.LISTS_SPLIT_TOOLTIP_SPLIT; + } else if (mode == 'JOIN') { + return Blockly.Msg.LISTS_SPLIT_TOOLTIP_JOIN; + } + throw 'Unknown mode: ' + mode; + }); + }, + /** + * Modify this block to have the correct input and output types. + * @param {string} newMode Either 'SPLIT' or 'JOIN'. + * @private + * @this Blockly.Block + */ + updateType_: function(newMode) { + if (newMode == 'SPLIT') { + this.outputConnection.setCheck('Array'); + this.getInput('INPUT').setCheck('String'); + } else { + this.outputConnection.setCheck('String'); + this.getInput('INPUT').setCheck('Array'); + } + }, + /** + * Create XML to represent the input and output types. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + container.setAttribute('mode', this.getFieldValue('MODE')); + return container; + }, + /** + * Parse XML to restore the input and output types. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + this.updateType_(xmlElement.getAttribute('mode')); + } +}; diff --git a/src/blockly/blocks/logic.js b/src/blockly/blocks/logic.js new file mode 100644 index 0000000..57af601 --- /dev/null +++ b/src/blockly/blocks/logic.js @@ -0,0 +1,488 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Logic blocks for Blockly. + * @author q.neutron@gmail.com (Quynh Neutron) + */ +'use strict'; + +goog.provide('Blockly.Blocks.logic'); + +goog.require('Blockly.Blocks'); + + +/** + * Common HSV hue for all blocks in this category. + */ +Blockly.Blocks.logic.HUE = 210; + +Blockly.Blocks['controls_if'] = { + /** + * Block for if/elseif/else condition. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.CONTROLS_IF_HELPURL); + this.setColour(Blockly.Blocks.logic.HUE); + this.appendValueInput('IF0') + .setCheck('Boolean') + .appendField(Blockly.Msg.CONTROLS_IF_MSG_IF); + this.appendStatementInput('DO0') + .appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setMutator(new Blockly.Mutator(['controls_if_elseif', + 'controls_if_else'])); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + if (!thisBlock.elseifCount_ && !thisBlock.elseCount_) { + return Blockly.Msg.CONTROLS_IF_TOOLTIP_1; + } else if (!thisBlock.elseifCount_ && thisBlock.elseCount_) { + return Blockly.Msg.CONTROLS_IF_TOOLTIP_2; + } else if (thisBlock.elseifCount_ && !thisBlock.elseCount_) { + return Blockly.Msg.CONTROLS_IF_TOOLTIP_3; + } else if (thisBlock.elseifCount_ && thisBlock.elseCount_) { + return Blockly.Msg.CONTROLS_IF_TOOLTIP_4; + } + return ''; + }); + this.elseifCount_ = 0; + this.elseCount_ = 0; + }, + /** + * Create XML to represent the number of else-if and else inputs. + * @return {Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + if (!this.elseifCount_ && !this.elseCount_) { + return null; + } + var container = document.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. + * @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.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) { + var containerBlock = workspace.newBlock('controls_if_if'); + containerBlock.initSvg(); + var connection = containerBlock.nextConnection; + for (var i = 1; i <= this.elseifCount_; i++) { + var elseifBlock = workspace.newBlock('controls_if_elseif'); + elseifBlock.initSvg(); + connection.connect(elseifBlock.previousConnection); + connection = elseifBlock.nextConnection; + } + if (this.elseCount_) { + var 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) { + var clauseBlock = containerBlock.nextConnection.targetBlock(); + // Count number of inputs. + this.elseifCount_ = 0; + this.elseCount_ = 0; + var valueConnections = [null]; + var statementConnections = [null]; + var elseStatementConnection = null; + while (clauseBlock) { + 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 'Unknown block type.'; + } + clauseBlock = clauseBlock.nextConnection && + clauseBlock.nextConnection.targetBlock(); + } + this.updateShape_(); + // Reconnect any child blocks. + for (var 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'); + }, + /** + * Store pointers to any connected child blocks. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + saveConnections: function(containerBlock) { + var clauseBlock = containerBlock.nextConnection.targetBlock(); + var i = 1; + while (clauseBlock) { + switch (clauseBlock.type) { + case 'controls_if_elseif': + var inputIf = this.getInput('IF' + i); + var inputDo = this.getInput('DO' + i); + clauseBlock.valueConnection_ = + inputIf && inputIf.connection.targetConnection; + clauseBlock.statementConnection_ = + inputDo && inputDo.connection.targetConnection; + i++; + break; + case 'controls_if_else': + var inputDo = this.getInput('ELSE'); + clauseBlock.statementConnection_ = + inputDo && inputDo.connection.targetConnection; + break; + default: + throw 'Unknown block type.'; + } + clauseBlock = clauseBlock.nextConnection && + clauseBlock.nextConnection.targetBlock(); + } + }, + /** + * Modify this block to have the correct number of inputs. + * @private + * @this Blockly.Block + */ + updateShape_: function() { + // Delete everything. + if (this.getInput('ELSE')) { + this.removeInput('ELSE'); + } + var i = 1; + while (this.getInput('IF' + i)) { + this.removeInput('IF' + i); + this.removeInput('DO' + i); + i++; + } + // Rebuild block. + for (var 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); + } + } +}; + +Blockly.Blocks['controls_if_if'] = { + /** + * Mutator block for if container. + * @this Blockly.Block + */ + init: function() { + this.setColour(Blockly.Blocks.logic.HUE); + this.appendDummyInput() + .appendField(Blockly.Msg.CONTROLS_IF_IF_TITLE_IF); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.CONTROLS_IF_IF_TOOLTIP); + this.contextMenu = false; + } +}; + +Blockly.Blocks['controls_if_elseif'] = { + /** + * Mutator bolck for else-if condition. + * @this Blockly.Block + */ + init: function() { + this.setColour(Blockly.Blocks.logic.HUE); + this.appendDummyInput() + .appendField(Blockly.Msg.CONTROLS_IF_ELSEIF_TITLE_ELSEIF); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.CONTROLS_IF_ELSEIF_TOOLTIP); + this.contextMenu = false; + } +}; + +Blockly.Blocks['controls_if_else'] = { + /** + * Mutator block for else condition. + * @this Blockly.Block + */ + init: function() { + this.setColour(Blockly.Blocks.logic.HUE); + this.appendDummyInput() + .appendField(Blockly.Msg.CONTROLS_IF_ELSE_TITLE_ELSE); + this.setPreviousStatement(true); + this.setTooltip(Blockly.Msg.CONTROLS_IF_ELSE_TOOLTIP); + this.contextMenu = false; + } +}; + +Blockly.Blocks['logic_compare'] = { + /** + * Block for comparison operator. + * @this Blockly.Block + */ + init: function() { + var rtlOperators = [ + ['=', 'EQ'], + ['\u2260', 'NEQ'], + ['\u200F<\u200F', 'LT'], + ['\u200F\u2264\u200F', 'LTE'], + ['\u200F>\u200F', 'GT'], + ['\u200F\u2265\u200F', 'GTE'] + ]; + var ltrOperators = [ + ['=', 'EQ'], + ['\u2260', 'NEQ'], + ['<', 'LT'], + ['\u2264', 'LTE'], + ['>', 'GT'], + ['\u2265', 'GTE'] + ]; + var OPERATORS = this.RTL ? rtlOperators : ltrOperators; + this.setHelpUrl(Blockly.Msg.LOGIC_COMPARE_HELPURL); + this.setColour(Blockly.Blocks.logic.HUE); + this.setOutput(true, 'Boolean'); + this.appendValueInput('A'); + this.appendValueInput('B') + .appendField(new Blockly.FieldDropdown(OPERATORS), 'OP'); + this.setInputsInline(true); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var op = thisBlock.getFieldValue('OP'); + var TOOLTIPS = { + 'EQ': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_EQ, + 'NEQ': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_NEQ, + 'LT': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_LT, + 'LTE': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_LTE, + 'GT': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_GT, + 'GTE': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_GTE + }; + return TOOLTIPS[op]; + }); + this.prevBlocks_ = [null, null]; + }, + /** + * 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) { + var blockA = this.getInputTargetBlock('A'); + var blockB = this.getInputTargetBlock('B'); + // Disconnect blocks that existed prior to this change if they don't match. + if (blockA && blockB && + !blockA.outputConnection.checkType_(blockB.outputConnection)) { + // Mismatch between two inputs. Disconnect previous and bump it away. + // Ensure that any disconnections are grouped with the causing event. + Blockly.Events.setGroup(e.group); + for (var i = 0; i < this.prevBlocks_.length; i++) { + var block = this.prevBlocks_[i]; + if (block === blockA || block === blockB) { + block.unplug(); + block.bumpNeighbours_(); + } + } + Blockly.Events.setGroup(false); + } + this.prevBlocks_[0] = blockA; + this.prevBlocks_[1] = blockB; + } +}; + +Blockly.Blocks['logic_operation'] = { + /** + * Block for logical operations: 'and', 'or'. + * @this Blockly.Block + */ + init: function() { + var OPERATORS = + [[Blockly.Msg.LOGIC_OPERATION_AND, 'AND'], + [Blockly.Msg.LOGIC_OPERATION_OR, 'OR']]; + this.setHelpUrl(Blockly.Msg.LOGIC_OPERATION_HELPURL); + this.setColour(Blockly.Blocks.logic.HUE); + this.setOutput(true, 'Boolean'); + this.appendValueInput('A') + .setCheck('Boolean'); + this.appendValueInput('B') + .setCheck('Boolean') + .appendField(new Blockly.FieldDropdown(OPERATORS), 'OP'); + this.setInputsInline(true); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var op = thisBlock.getFieldValue('OP'); + var TOOLTIPS = { + 'AND': Blockly.Msg.LOGIC_OPERATION_TOOLTIP_AND, + 'OR': Blockly.Msg.LOGIC_OPERATION_TOOLTIP_OR + }; + return TOOLTIPS[op]; + }); + } +}; + +Blockly.Blocks['logic_negate'] = { + /** + * Block for negation. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.LOGIC_NEGATE_TITLE, + "args0": [ + { + "type": "input_value", + "name": "BOOL", + "check": "Boolean" + } + ], + "output": "Boolean", + "colour": Blockly.Blocks.logic.HUE, + "tooltip": Blockly.Msg.LOGIC_NEGATE_TOOLTIP, + "helpUrl": Blockly.Msg.LOGIC_NEGATE_HELPURL + }); + } +}; + +Blockly.Blocks['logic_boolean'] = { + /** + * Block for boolean data type: true and false. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": "%1", + "args0": [ + { + "type": "field_dropdown", + "name": "BOOL", + "options": [ + [Blockly.Msg.LOGIC_BOOLEAN_TRUE, "TRUE"], + [Blockly.Msg.LOGIC_BOOLEAN_FALSE, "FALSE"] + ] + } + ], + "output": "Boolean", + "colour": Blockly.Blocks.logic.HUE, + "tooltip": Blockly.Msg.LOGIC_BOOLEAN_TOOLTIP, + "helpUrl": Blockly.Msg.LOGIC_BOOLEAN_HELPURL + }); + } +}; + +Blockly.Blocks['logic_null'] = { + /** + * Block for null data type. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.LOGIC_NULL, + "output": null, + "colour": Blockly.Blocks.logic.HUE, + "tooltip": Blockly.Msg.LOGIC_NULL_TOOLTIP, + "helpUrl": Blockly.Msg.LOGIC_NULL_HELPURL + }); + } +}; + +Blockly.Blocks['logic_ternary'] = { + /** + * Block for ternary operator. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.LOGIC_TERNARY_HELPURL); + this.setColour(Blockly.Blocks.logic.HUE); + this.appendValueInput('IF') + .setCheck('Boolean') + .appendField(Blockly.Msg.LOGIC_TERNARY_CONDITION); + this.appendValueInput('THEN') + .appendField(Blockly.Msg.LOGIC_TERNARY_IF_TRUE); + this.appendValueInput('ELSE') + .appendField(Blockly.Msg.LOGIC_TERNARY_IF_FALSE); + this.setOutput(true); + this.setTooltip(Blockly.Msg.LOGIC_TERNARY_TOOLTIP); + this.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) { + var blockA = this.getInputTargetBlock('THEN'); + var blockB = this.getInputTargetBlock('ELSE'); + var parentConnection = this.outputConnection.targetConnection; + // Disconnect blocks that existed prior to this change if they don't match. + if ((blockA || blockB) && parentConnection) { + for (var i = 0; i < 2; i++) { + var block = (i == 1) ? blockA : blockB; + if (block && !block.outputConnection.checkType_(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; + } +}; diff --git a/src/blockly/blocks/loops.js b/src/blockly/blocks/loops.js new file mode 100644 index 0000000..b9bae1e --- /dev/null +++ b/src/blockly/blocks/loops.js @@ -0,0 +1,283 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Loop blocks for Blockly. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Blocks.loops'); + +goog.require('Blockly.Blocks'); + + +/** + * Common HSV hue for all blocks in this category. + */ +Blockly.Blocks.loops.HUE = 120; + +Blockly.Blocks['controls_repeat_ext'] = { + /** + * Block for repeat n times (external number). + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.CONTROLS_REPEAT_TITLE, + "args0": [ + { + "type": "input_value", + "name": "TIMES", + "check": "Number" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": Blockly.Blocks.loops.HUE, + "tooltip": Blockly.Msg.CONTROLS_REPEAT_TOOLTIP, + "helpUrl": Blockly.Msg.CONTROLS_REPEAT_HELPURL + }); + this.appendStatementInput('DO') + .appendField(Blockly.Msg.CONTROLS_REPEAT_INPUT_DO); + } +}; + +Blockly.Blocks['controls_repeat'] = { + /** + * Block for repeat n times (internal number). + * The 'controls_repeat_ext' block is preferred as it is more flexible. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.CONTROLS_REPEAT_TITLE, + "args0": [ + { + "type": "field_number", + "name": "TIMES", + "value": 10, + "min": 0, + "precision": 1 + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": Blockly.Blocks.loops.HUE, + "tooltip": Blockly.Msg.CONTROLS_REPEAT_TOOLTIP, + "helpUrl": Blockly.Msg.CONTROLS_REPEAT_HELPURL + }); + this.appendStatementInput('DO') + .appendField(Blockly.Msg.CONTROLS_REPEAT_INPUT_DO); + } +}; + +Blockly.Blocks['controls_whileUntil'] = { + /** + * Block for 'do while/until' loop. + * @this Blockly.Block + */ + init: function() { + var OPERATORS = + [[Blockly.Msg.CONTROLS_WHILEUNTIL_OPERATOR_WHILE, 'WHILE'], + [Blockly.Msg.CONTROLS_WHILEUNTIL_OPERATOR_UNTIL, 'UNTIL']]; + this.setHelpUrl(Blockly.Msg.CONTROLS_WHILEUNTIL_HELPURL); + this.setColour(Blockly.Blocks.loops.HUE); + this.appendValueInput('BOOL') + .setCheck('Boolean') + .appendField(new Blockly.FieldDropdown(OPERATORS), 'MODE'); + this.appendStatementInput('DO') + .appendField(Blockly.Msg.CONTROLS_WHILEUNTIL_INPUT_DO); + this.setPreviousStatement(true); + this.setNextStatement(true); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var op = thisBlock.getFieldValue('MODE'); + var TOOLTIPS = { + 'WHILE': Blockly.Msg.CONTROLS_WHILEUNTIL_TOOLTIP_WHILE, + 'UNTIL': Blockly.Msg.CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL + }; + return TOOLTIPS[op]; + }); + } +}; + +Blockly.Blocks['controls_for'] = { + /** + * Block for 'for' loop. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.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" + } + ], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": Blockly.Blocks.loops.HUE, + "helpUrl": Blockly.Msg.CONTROLS_FOR_HELPURL + }); + this.appendStatementInput('DO') + .appendField(Blockly.Msg.CONTROLS_FOR_INPUT_DO); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + return Blockly.Msg.CONTROLS_FOR_TOOLTIP.replace('%1', + thisBlock.getFieldValue('VAR')); + }); + }, + /** + * Add menu option to create getter block for loop variable. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu: function(options) { + if (!this.isCollapsed()) { + var option = {enabled: true}; + var name = this.getFieldValue('VAR'); + option.text = Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', name); + var xmlField = goog.dom.createDom('field', null, name); + xmlField.setAttribute('name', 'VAR'); + var xmlBlock = goog.dom.createDom('block', null, xmlField); + xmlBlock.setAttribute('type', 'variables_get'); + option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); + options.push(option); + } + } +}; + +Blockly.Blocks['controls_forEach'] = { + /** + * Block for 'for each' loop. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.CONTROLS_FOREACH_TITLE, + "args0": [ + { + "type": "field_variable", + "name": "VAR", + "variable": null + }, + { + "type": "input_value", + "name": "LIST", + "check": "Array" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": Blockly.Blocks.loops.HUE, + "helpUrl": Blockly.Msg.CONTROLS_FOREACH_HELPURL + }); + this.appendStatementInput('DO') + .appendField(Blockly.Msg.CONTROLS_FOREACH_INPUT_DO); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + return Blockly.Msg.CONTROLS_FOREACH_TOOLTIP.replace('%1', + thisBlock.getFieldValue('VAR')); + }); + }, + customContextMenu: Blockly.Blocks['controls_for'].customContextMenu +}; + +Blockly.Blocks['controls_flow_statements'] = { + /** + * Block for flow statements: continue, break. + * @this Blockly.Block + */ + init: function() { + var OPERATORS = + [[Blockly.Msg.CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK, 'BREAK'], + [Blockly.Msg.CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE, 'CONTINUE']]; + this.setHelpUrl(Blockly.Msg.CONTROLS_FLOW_STATEMENTS_HELPURL); + this.setColour(Blockly.Blocks.loops.HUE); + this.appendDummyInput() + .appendField(new Blockly.FieldDropdown(OPERATORS), 'FLOW'); + this.setPreviousStatement(true); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var op = thisBlock.getFieldValue('FLOW'); + var TOOLTIPS = { + 'BREAK': Blockly.Msg.CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK, + 'CONTINUE': Blockly.Msg.CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE + }; + return TOOLTIPS[op]; + }); + }, + /** + * Called whenever anything on the workspace changes. + * Add warning if this flow block is not nested inside a loop. + * @param {!Blockly.Events.Abstract} e Change event. + * @this Blockly.Block + */ + onchange: function(e) { + var legal = false; + // Is the block nested in a loop? + var block = this; + do { + if (this.LOOP_TYPES.indexOf(block.type) != -1) { + legal = true; + break; + } + block = block.getSurroundParent(); + } while (block); + if (legal) { + this.setWarningText(null); + } else { + this.setWarningText(Blockly.Msg.CONTROLS_FLOW_STATEMENTS_WARNING); + } + }, + /** + * List of block types that are loops and thus do not need warnings. + * To add a new loop type add this to your code: + * Blockly.Blocks['controls_flow_statements'].LOOP_TYPES.push('custom_loop'); + */ + LOOP_TYPES: ['controls_repeat', 'controls_repeat_ext', 'controls_forEach', + 'controls_for', 'controls_whileUntil'] +}; diff --git a/src/blockly/blocks/math.js b/src/blockly/blocks/math.js new file mode 100644 index 0000000..053b545 --- /dev/null +++ b/src/blockly/blocks/math.js @@ -0,0 +1,553 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Math blocks for Blockly. + * @author q.neutron@gmail.com (Quynh Neutron) + */ +'use strict'; + +goog.provide('Blockly.Blocks.math'); + +goog.require('Blockly.Blocks'); + + +/** + * Common HSV hue for all blocks in this category. + */ +Blockly.Blocks.math.HUE = 230; + +Blockly.Blocks['math_number'] = { + /** + * Block for numeric value. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.MATH_NUMBER_HELPURL); + this.setColour(Blockly.Blocks.math.HUE); + this.appendDummyInput() + .appendField(new Blockly.FieldNumber('0'), 'NUM'); + this.setOutput(true, 'Number'); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + // Number block is trivial. Use tooltip of parent block if it exists. + this.setTooltip(function() { + var parent = thisBlock.getParent(); + return (parent && parent.getInputsInline() && parent.tooltip) || + Blockly.Msg.MATH_NUMBER_TOOLTIP; + }); + } +}; + +Blockly.Blocks['math_arithmetic'] = { + /** + * Block for basic arithmetic operator. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": "%1 %2 %3", + "args0": [ + { + "type": "input_value", + "name": "A", + "check": "Number" + }, + { + "type": "field_dropdown", + "name": "OP", + "options": + [[Blockly.Msg.MATH_ADDITION_SYMBOL, 'ADD'], + [Blockly.Msg.MATH_SUBTRACTION_SYMBOL, 'MINUS'], + [Blockly.Msg.MATH_MULTIPLICATION_SYMBOL, 'MULTIPLY'], + [Blockly.Msg.MATH_DIVISION_SYMBOL, 'DIVIDE'], + [Blockly.Msg.MATH_POWER_SYMBOL, 'POWER']] + }, + { + "type": "input_value", + "name": "B", + "check": "Number" + } + ], + "inputsInline": true, + "output": "Number", + "colour": Blockly.Blocks.math.HUE, + "helpUrl": Blockly.Msg.MATH_ARITHMETIC_HELPURL + }); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var mode = thisBlock.getFieldValue('OP'); + var TOOLTIPS = { + 'ADD': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_ADD, + 'MINUS': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_MINUS, + 'MULTIPLY': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_MULTIPLY, + 'DIVIDE': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_DIVIDE, + 'POWER': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_POWER + }; + return TOOLTIPS[mode]; + }); + } +}; + +Blockly.Blocks['math_single'] = { + /** + * Block for advanced math operators with single operand. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": "%1 %2", + "args0": [ + { + "type": "field_dropdown", + "name": "OP", + "options": [ + [Blockly.Msg.MATH_SINGLE_OP_ROOT, 'ROOT'], + [Blockly.Msg.MATH_SINGLE_OP_ABSOLUTE, 'ABS'], + ['-', 'NEG'], + ['ln', 'LN'], + ['log10', 'LOG10'], + ['e^', 'EXP'], + ['10^', 'POW10'] + ] + }, + { + "type": "input_value", + "name": "NUM", + "check": "Number" + } + ], + "output": "Number", + "colour": Blockly.Blocks.math.HUE, + "helpUrl": Blockly.Msg.MATH_SINGLE_HELPURL + }); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var mode = thisBlock.getFieldValue('OP'); + var TOOLTIPS = { + 'ROOT': Blockly.Msg.MATH_SINGLE_TOOLTIP_ROOT, + 'ABS': Blockly.Msg.MATH_SINGLE_TOOLTIP_ABS, + 'NEG': Blockly.Msg.MATH_SINGLE_TOOLTIP_NEG, + 'LN': Blockly.Msg.MATH_SINGLE_TOOLTIP_LN, + 'LOG10': Blockly.Msg.MATH_SINGLE_TOOLTIP_LOG10, + 'EXP': Blockly.Msg.MATH_SINGLE_TOOLTIP_EXP, + 'POW10': Blockly.Msg.MATH_SINGLE_TOOLTIP_POW10 + }; + return TOOLTIPS[mode]; + }); + } +}; + +Blockly.Blocks['math_trig'] = { + /** + * Block for trigonometry operators. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": "%1 %2", + "args0": [ + { + "type": "field_dropdown", + "name": "OP", + "options": [ + [Blockly.Msg.MATH_TRIG_SIN, 'SIN'], + [Blockly.Msg.MATH_TRIG_COS, 'COS'], + [Blockly.Msg.MATH_TRIG_TAN, 'TAN'], + [Blockly.Msg.MATH_TRIG_ASIN, 'ASIN'], + [Blockly.Msg.MATH_TRIG_ACOS, 'ACOS'], + [Blockly.Msg.MATH_TRIG_ATAN, 'ATAN'] + ] + }, + { + "type": "input_value", + "name": "NUM", + "check": "Number" + } + ], + "output": "Number", + "colour": Blockly.Blocks.math.HUE, + "helpUrl": Blockly.Msg.MATH_TRIG_HELPURL + }); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var mode = thisBlock.getFieldValue('OP'); + var TOOLTIPS = { + 'SIN': Blockly.Msg.MATH_TRIG_TOOLTIP_SIN, + 'COS': Blockly.Msg.MATH_TRIG_TOOLTIP_COS, + 'TAN': Blockly.Msg.MATH_TRIG_TOOLTIP_TAN, + 'ASIN': Blockly.Msg.MATH_TRIG_TOOLTIP_ASIN, + 'ACOS': Blockly.Msg.MATH_TRIG_TOOLTIP_ACOS, + 'ATAN': Blockly.Msg.MATH_TRIG_TOOLTIP_ATAN + }; + return TOOLTIPS[mode]; + }); + } +}; + +Blockly.Blocks['math_constant'] = { + /** + * Block for constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": "%1", + "args0": [ + { + "type": "field_dropdown", + "name": "CONSTANT", + "options": [ + ['\u03c0', 'PI'], + ['e', 'E'], + ['\u03c6', 'GOLDEN_RATIO'], + ['sqrt(2)', 'SQRT2'], + ['sqrt(\u00bd)', 'SQRT1_2'], + ['\u221e', 'INFINITY'] + ] + } + ], + "output": "Number", + "colour": Blockly.Blocks.math.HUE, + "tooltip": Blockly.Msg.MATH_CONSTANT_TOOLTIP, + "helpUrl": Blockly.Msg.MATH_CONSTANT_HELPURL + }); + } +}; + +Blockly.Blocks['math_number_property'] = { + /** + * Block for checking if a number is even, odd, prime, whole, positive, + * negative or if it is divisible by certain number. + * @this Blockly.Block + */ + init: function() { + var PROPERTIES = + [[Blockly.Msg.MATH_IS_EVEN, 'EVEN'], + [Blockly.Msg.MATH_IS_ODD, 'ODD'], + [Blockly.Msg.MATH_IS_PRIME, 'PRIME'], + [Blockly.Msg.MATH_IS_WHOLE, 'WHOLE'], + [Blockly.Msg.MATH_IS_POSITIVE, 'POSITIVE'], + [Blockly.Msg.MATH_IS_NEGATIVE, 'NEGATIVE'], + [Blockly.Msg.MATH_IS_DIVISIBLE_BY, 'DIVISIBLE_BY']]; + this.setColour(Blockly.Blocks.math.HUE); + this.appendValueInput('NUMBER_TO_CHECK') + .setCheck('Number'); + var dropdown = new Blockly.FieldDropdown(PROPERTIES, function(option) { + var divisorInput = (option == 'DIVISIBLE_BY'); + this.sourceBlock_.updateShape_(divisorInput); + }); + this.appendDummyInput() + .appendField(dropdown, 'PROPERTY'); + this.setInputsInline(true); + this.setOutput(true, 'Boolean'); + this.setTooltip(Blockly.Msg.MATH_IS_TOOLTIP); + }, + /** + * Create XML to represent whether the 'divisorInput' should be present. + * @return {Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + var divisorInput = (this.getFieldValue('PROPERTY') == 'DIVISIBLE_BY'); + container.setAttribute('divisor_input', divisorInput); + return container; + }, + /** + * Parse XML to restore the 'divisorInput'. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + var divisorInput = (xmlElement.getAttribute('divisor_input') == 'true'); + this.updateShape_(divisorInput); + }, + /** + * Modify this block to have (or not have) an input for 'is divisible by'. + * @param {boolean} divisorInput True if this block has a divisor input. + * @private + * @this Blockly.Block + */ + updateShape_: function(divisorInput) { + // Add or remove a Value Input. + var inputExists = this.getInput('DIVISOR'); + if (divisorInput) { + if (!inputExists) { + this.appendValueInput('DIVISOR') + .setCheck('Number'); + } + } else if (inputExists) { + this.removeInput('DIVISOR'); + } + } +}; + +Blockly.Blocks['math_change'] = { + /** + * Block for adding to a variable in place. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.MATH_CHANGE_TITLE, + "args0": [ + { + "type": "field_variable", + "name": "VAR", + "variable": Blockly.Msg.MATH_CHANGE_TITLE_ITEM + }, + { + "type": "input_value", + "name": "DELTA", + "check": "Number" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": Blockly.Blocks.variables.HUE, + "helpUrl": Blockly.Msg.MATH_CHANGE_HELPURL + }); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + return Blockly.Msg.MATH_CHANGE_TOOLTIP.replace('%1', + thisBlock.getFieldValue('VAR')); + }); + } +}; + +Blockly.Blocks['math_round'] = { + /** + * Block for rounding functions. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": "%1 %2", + "args0": [ + { + "type": "field_dropdown", + "name": "OP", + "options": [ + [Blockly.Msg.MATH_ROUND_OPERATOR_ROUND, 'ROUND'], + [Blockly.Msg.MATH_ROUND_OPERATOR_ROUNDUP, 'ROUNDUP'], + [Blockly.Msg.MATH_ROUND_OPERATOR_ROUNDDOWN, 'ROUNDDOWN'] + ] + }, + { + "type": "input_value", + "name": "NUM", + "check": "Number" + } + ], + "output": "Number", + "colour": Blockly.Blocks.math.HUE, + "tooltip": Blockly.Msg.MATH_ROUND_TOOLTIP, + "helpUrl": Blockly.Msg.MATH_ROUND_HELPURL + }); + } +}; + +Blockly.Blocks['math_on_list'] = { + /** + * Block for evaluating a list of numbers to return sum, average, min, max, + * etc. Some functions also work on text (min, max, mode, median). + * @this Blockly.Block + */ + init: function() { + var OPERATORS = + [[Blockly.Msg.MATH_ONLIST_OPERATOR_SUM, 'SUM'], + [Blockly.Msg.MATH_ONLIST_OPERATOR_MIN, 'MIN'], + [Blockly.Msg.MATH_ONLIST_OPERATOR_MAX, 'MAX'], + [Blockly.Msg.MATH_ONLIST_OPERATOR_AVERAGE, 'AVERAGE'], + [Blockly.Msg.MATH_ONLIST_OPERATOR_MEDIAN, 'MEDIAN'], + [Blockly.Msg.MATH_ONLIST_OPERATOR_MODE, 'MODE'], + [Blockly.Msg.MATH_ONLIST_OPERATOR_STD_DEV, 'STD_DEV'], + [Blockly.Msg.MATH_ONLIST_OPERATOR_RANDOM, 'RANDOM']]; + // Assign 'this' to a variable for use in the closures below. + var thisBlock = this; + this.setHelpUrl(Blockly.Msg.MATH_ONLIST_HELPURL); + this.setColour(Blockly.Blocks.math.HUE); + this.setOutput(true, 'Number'); + var dropdown = new Blockly.FieldDropdown(OPERATORS, function(newOp) { + thisBlock.updateType_(newOp); + }); + this.appendValueInput('LIST') + .setCheck('Array') + .appendField(dropdown, 'OP'); + this.setTooltip(function() { + var mode = thisBlock.getFieldValue('OP'); + var TOOLTIPS = { + 'SUM': Blockly.Msg.MATH_ONLIST_TOOLTIP_SUM, + 'MIN': Blockly.Msg.MATH_ONLIST_TOOLTIP_MIN, + 'MAX': Blockly.Msg.MATH_ONLIST_TOOLTIP_MAX, + 'AVERAGE': Blockly.Msg.MATH_ONLIST_TOOLTIP_AVERAGE, + 'MEDIAN': Blockly.Msg.MATH_ONLIST_TOOLTIP_MEDIAN, + 'MODE': Blockly.Msg.MATH_ONLIST_TOOLTIP_MODE, + 'STD_DEV': Blockly.Msg.MATH_ONLIST_TOOLTIP_STD_DEV, + 'RANDOM': Blockly.Msg.MATH_ONLIST_TOOLTIP_RANDOM + }; + return TOOLTIPS[mode]; + }); + }, + /** + * Modify this block to have the correct output type. + * @param {string} newOp Either 'MODE' or some op than returns a number. + * @private + * @this Blockly.Block + */ + updateType_: function(newOp) { + if (newOp == 'MODE') { + this.outputConnection.setCheck('Array'); + } else { + this.outputConnection.setCheck('Number'); + } + }, + /** + * Create XML to represent the output type. + * @return {Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + container.setAttribute('op', this.getFieldValue('OP')); + return container; + }, + /** + * Parse XML to restore the output type. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + this.updateType_(xmlElement.getAttribute('op')); + } +}; + +Blockly.Blocks['math_modulo'] = { + /** + * Block for remainder of a division. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.MATH_MODULO_TITLE, + "args0": [ + { + "type": "input_value", + "name": "DIVIDEND", + "check": "Number" + }, + { + "type": "input_value", + "name": "DIVISOR", + "check": "Number" + } + ], + "inputsInline": true, + "output": "Number", + "colour": Blockly.Blocks.math.HUE, + "tooltip": Blockly.Msg.MATH_MODULO_TOOLTIP, + "helpUrl": Blockly.Msg.MATH_MODULO_HELPURL + }); + } +}; + +Blockly.Blocks['math_constrain'] = { + /** + * Block for constraining a number between two limits. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.MATH_CONSTRAIN_TITLE, + "args0": [ + { + "type": "input_value", + "name": "VALUE", + "check": "Number" + }, + { + "type": "input_value", + "name": "LOW", + "check": "Number" + }, + { + "type": "input_value", + "name": "HIGH", + "check": "Number" + } + ], + "inputsInline": true, + "output": "Number", + "colour": Blockly.Blocks.math.HUE, + "tooltip": Blockly.Msg.MATH_CONSTRAIN_TOOLTIP, + "helpUrl": Blockly.Msg.MATH_CONSTRAIN_HELPURL + }); + } +}; + +Blockly.Blocks['math_random_int'] = { + /** + * Block for random integer between [X] and [Y]. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.MATH_RANDOM_INT_TITLE, + "args0": [ + { + "type": "input_value", + "name": "FROM", + "check": "Number" + }, + { + "type": "input_value", + "name": "TO", + "check": "Number" + } + ], + "inputsInline": true, + "output": "Number", + "colour": Blockly.Blocks.math.HUE, + "tooltip": Blockly.Msg.MATH_RANDOM_INT_TOOLTIP, + "helpUrl": Blockly.Msg.MATH_RANDOM_INT_HELPURL + }); + } +}; + +Blockly.Blocks['math_random_float'] = { + /** + * Block for random fraction between 0 and 1. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.MATH_RANDOM_FLOAT_TITLE_RANDOM, + "output": "Number", + "colour": Blockly.Blocks.math.HUE, + "tooltip": Blockly.Msg.MATH_RANDOM_FLOAT_TOOLTIP, + "helpUrl": Blockly.Msg.MATH_RANDOM_FLOAT_HELPURL + }); + } +}; diff --git a/src/blockly/blocks/procedures.js b/src/blockly/blocks/procedures.js new file mode 100644 index 0000000..19a5f6e --- /dev/null +++ b/src/blockly/blocks/procedures.js @@ -0,0 +1,875 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Procedure blocks for Blockly. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Blocks.procedures'); + +goog.require('Blockly.Blocks'); + + +/** + * Common HSV hue for all blocks in this category. + */ +Blockly.Blocks.procedures.HUE = 290; + +Blockly.Blocks['procedures_defnoreturn'] = { + /** + * Block for defining a procedure with no return value. + * @this Blockly.Block + */ + init: function() { + var nameField = new Blockly.FieldTextInput( + Blockly.Msg.PROCEDURES_DEFNORETURN_PROCEDURE, + Blockly.Procedures.rename); + nameField.setSpellcheck(false); + this.appendDummyInput() + .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_TITLE) + .appendField(nameField, 'NAME') + .appendField('', 'PARAMS'); + this.setMutator(new Blockly.Mutator(['procedures_mutatorarg'])); + if (Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT) { + this.setCommentText(Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT); + } + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_DEFNORETURN_TOOLTIP); + this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFNORETURN_HELPURL); + this.arguments_ = []; + this.setStatements_(true); + this.statementConnection_ = null; + }, + /** + * Add or remove the statement block from this function definition. + * @param {boolean} hasStatements True if a statement block is needed. + * @this Blockly.Block + */ + setStatements_: function(hasStatements) { + if (this.hasStatements_ === hasStatements) { + return; + } + if (hasStatements) { + this.appendStatementInput('STACK') + .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_DO); + if (this.getInput('RETURN')) { + this.moveInputBefore('STACK', 'RETURN'); + } + } else { + this.removeInput('STACK', true); + } + this.hasStatements_ = hasStatements; + }, + /** + * Update the display of parameters for this procedure definition block. + * Display a warning if there are duplicately named parameters. + * @private + * @this Blockly.Block + */ + updateParams_: function() { + // Check for duplicated arguments. + var badArg = false; + var hash = {}; + for (var i = 0; i < this.arguments_.length; i++) { + if (hash['arg_' + this.arguments_[i].toLowerCase()]) { + badArg = true; + break; + } + hash['arg_' + this.arguments_[i].toLowerCase()] = true; + } + if (badArg) { + this.setWarningText(Blockly.Msg.PROCEDURES_DEF_DUPLICATE_WARNING); + } else { + this.setWarningText(null); + } + // Merge the arguments into a human-readable list. + var paramString = ''; + if (this.arguments_.length) { + paramString = Blockly.Msg.PROCEDURES_BEFORE_PARAMS + + ' ' + this.arguments_.join(', '); + } + // The params field is deterministic based on the mutation, + // no need to fire a change event. + Blockly.Events.disable(); + try { + this.setFieldValue(paramString, 'PARAMS'); + } finally { + Blockly.Events.enable(); + } + }, + /** + * Create XML to represent the argument inputs. + * @param {=boolean} opt_paramIds If true include the IDs of the parameter + * quarks. Used by Blockly.Procedures.mutateCallers for reconnection. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function(opt_paramIds) { + var container = document.createElement('mutation'); + if (opt_paramIds) { + container.setAttribute('name', this.getFieldValue('NAME')); + } + for (var i = 0; i < this.arguments_.length; i++) { + var parameter = document.createElement('arg'); + parameter.setAttribute('name', this.arguments_[i]); + if (opt_paramIds && this.paramIds_) { + parameter.setAttribute('paramId', this.paramIds_[i]); + } + container.appendChild(parameter); + } + + // Save whether the statement input is visible. + if (!this.hasStatements_) { + container.setAttribute('statements', 'false'); + } + return container; + }, + /** + * Parse XML to restore the argument inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + this.arguments_ = []; + for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() == 'arg') { + this.arguments_.push(childNode.getAttribute('name')); + } + } + this.updateParams_(); + Blockly.Procedures.mutateCallers(this); + + // Show or hide the statement input. + this.setStatements_(xmlElement.getAttribute('statements') !== 'false'); + }, + /** + * 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) { + var containerBlock = workspace.newBlock('procedures_mutatorcontainer'); + containerBlock.initSvg(); + + // Check/uncheck the allow statement box. + if (this.getInput('RETURN')) { + containerBlock.setFieldValue(this.hasStatements_ ? 'TRUE' : 'FALSE', + 'STATEMENTS'); + } else { + containerBlock.getInput('STATEMENT_INPUT').setVisible(false); + } + + // Parameter list. + var connection = containerBlock.getInput('STACK').connection; + for (var i = 0; i < this.arguments_.length; i++) { + var paramBlock = workspace.newBlock('procedures_mutatorarg'); + paramBlock.initSvg(); + paramBlock.setFieldValue(this.arguments_[i], 'NAME'); + // Store the old location. + paramBlock.oldLocation = i; + connection.connect(paramBlock.previousConnection); + connection = paramBlock.nextConnection; + } + // Initialize procedure's callers with blank IDs. + Blockly.Procedures.mutateCallers(this); + 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) { + // Parameter list. + this.arguments_ = []; + this.paramIds_ = []; + var paramBlock = containerBlock.getInputTargetBlock('STACK'); + while (paramBlock) { + this.arguments_.push(paramBlock.getFieldValue('NAME')); + this.paramIds_.push(paramBlock.id); + paramBlock = paramBlock.nextConnection && + paramBlock.nextConnection.targetBlock(); + } + this.updateParams_(); + Blockly.Procedures.mutateCallers(this); + + // Show/hide the statement input. + var hasStatements = containerBlock.getFieldValue('STATEMENTS'); + if (hasStatements !== null) { + hasStatements = hasStatements == 'TRUE'; + if (this.hasStatements_ != hasStatements) { + if (hasStatements) { + this.setStatements_(true); + // Restore the stack, if one was saved. + Blockly.Mutator.reconnect(this.statementConnection_, this, 'STACK'); + this.statementConnection_ = null; + } else { + // Save the stack, then disconnect it. + var stackConnection = this.getInput('STACK').connection; + this.statementConnection_ = stackConnection.targetConnection; + if (this.statementConnection_) { + var stackBlock = stackConnection.targetBlock(); + stackBlock.unplug(); + stackBlock.bumpNeighbours_(); + } + this.setStatements_(false); + } + } + } + }, + /** + * Return the signature of this procedure definition. + * @return {!Array} 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 Blockly.Block + */ + getProcedureDef: function() { + return [this.getFieldValue('NAME'), this.arguments_, false]; + }, + /** + * Return all variables referenced by this block. + * @return {!Array.<string>} List of variable names. + * @this Blockly.Block + */ + getVars: function() { + return this.arguments_; + }, + /** + * Notification that a variable is renaming. + * If the name matches one of this block's variables, rename it. + * @param {string} oldName Previous name of variable. + * @param {string} newName Renamed variable. + * @this Blockly.Block + */ + renameVar: function(oldName, newName) { + var change = false; + for (var i = 0; i < this.arguments_.length; i++) { + if (Blockly.Names.equals(oldName, this.arguments_[i])) { + this.arguments_[i] = newName; + change = true; + } + } + if (change) { + this.updateParams_(); + // Update the mutator's variables if the mutator is open. + if (this.mutator.isVisible()) { + var blocks = this.mutator.workspace_.getAllBlocks(); + for (var i = 0, block; block = blocks[i]; i++) { + if (block.type == 'procedures_mutatorarg' && + Blockly.Names.equals(oldName, block.getFieldValue('NAME'))) { + block.setFieldValue(newName, 'NAME'); + } + } + } + } + }, + /** + * Add custom menu options to this block's context menu. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu: function(options) { + // Add option to create caller. + var option = {enabled: true}; + var name = this.getFieldValue('NAME'); + option.text = Blockly.Msg.PROCEDURES_CREATE_DO.replace('%1', name); + var xmlMutation = goog.dom.createDom('mutation'); + xmlMutation.setAttribute('name', name); + for (var i = 0; i < this.arguments_.length; i++) { + var xmlArg = goog.dom.createDom('arg'); + xmlArg.setAttribute('name', this.arguments_[i]); + xmlMutation.appendChild(xmlArg); + } + var xmlBlock = goog.dom.createDom('block', null, xmlMutation); + xmlBlock.setAttribute('type', this.callType_); + option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); + options.push(option); + + // Add options to create getters for each parameter. + if (!this.isCollapsed()) { + for (var i = 0; i < this.arguments_.length; i++) { + var option = {enabled: true}; + var name = this.arguments_[i]; + option.text = Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', name); + var xmlField = goog.dom.createDom('field', null, name); + xmlField.setAttribute('name', 'VAR'); + var xmlBlock = goog.dom.createDom('block', null, xmlField); + xmlBlock.setAttribute('type', 'variables_get'); + option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); + options.push(option); + } + } + }, + callType_: 'procedures_callnoreturn' +}; + +Blockly.Blocks['procedures_defreturn'] = { + /** + * Block for defining a procedure with a return value. + * @this Blockly.Block + */ + init: function() { + var nameField = new Blockly.FieldTextInput( + Blockly.Msg.PROCEDURES_DEFRETURN_PROCEDURE, + Blockly.Procedures.rename); + nameField.setSpellcheck(false); + this.appendDummyInput() + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_TITLE) + .appendField(nameField, 'NAME') + .appendField('', 'PARAMS'); + this.appendValueInput('RETURN') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + this.setMutator(new Blockly.Mutator(['procedures_mutatorarg'])); + if (Blockly.Msg.PROCEDURES_DEFRETURN_COMMENT) { + this.setCommentText(Blockly.Msg.PROCEDURES_DEFRETURN_COMMENT); + } + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_DEFRETURN_TOOLTIP); + this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFRETURN_HELPURL); + this.arguments_ = []; + this.setStatements_(true); + this.statementConnection_ = null; + }, + setStatements_: Blockly.Blocks['procedures_defnoreturn'].setStatements_, + updateParams_: Blockly.Blocks['procedures_defnoreturn'].updateParams_, + mutationToDom: Blockly.Blocks['procedures_defnoreturn'].mutationToDom, + domToMutation: Blockly.Blocks['procedures_defnoreturn'].domToMutation, + decompose: Blockly.Blocks['procedures_defnoreturn'].decompose, + compose: Blockly.Blocks['procedures_defnoreturn'].compose, + /** + * Return the signature of this procedure definition. + * @return {!Array} Tuple containing three elements: + * - the name of the defined procedure, + * - a list of all its arguments, + * - that it DOES have a return value. + * @this Blockly.Block + */ + getProcedureDef: function() { + return [this.getFieldValue('NAME'), this.arguments_, true]; + }, + getVars: Blockly.Blocks['procedures_defnoreturn'].getVars, + renameVar: Blockly.Blocks['procedures_defnoreturn'].renameVar, + customContextMenu: Blockly.Blocks['procedures_defnoreturn'].customContextMenu, + callType_: 'procedures_callreturn' +}; + +Blockly.Blocks['procedures_mutatorcontainer'] = { + /** + * Mutator block for procedure container. + * @this Blockly.Block + */ + init: function() { + this.appendDummyInput() + .appendField(Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TITLE); + this.appendStatementInput('STACK'); + this.appendDummyInput('STATEMENT_INPUT') + .appendField(Blockly.Msg.PROCEDURES_ALLOW_STATEMENTS) + .appendField(new Blockly.FieldCheckbox('TRUE'), 'STATEMENTS'); + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TOOLTIP); + this.contextMenu = false; + } +}; + +Blockly.Blocks['procedures_mutatorarg'] = { + /** + * Mutator block for procedure argument. + * @this Blockly.Block + */ + init: function() { + var field = new Blockly.FieldTextInput('x', this.validator_); + this.appendDummyInput() + .appendField(Blockly.Msg.PROCEDURES_MUTATORARG_TITLE) + .appendField(field, 'NAME'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORARG_TOOLTIP); + this.contextMenu = false; + + // Create the default variable when we drag the block in from the flyout. + // Have to do this after installing the field on the block. + field.onFinishEditing_ = this.createNewVar_; + field.onFinishEditing_('x'); + }, + /** + * Obtain a valid name for the procedure. + * Merge runs of whitespace. Strip leading and trailing whitespace. + * Beyond this, all names are legal. + * @param {string} newVar User-supplied name. + * @return {?string} Valid name, or null if a name was not specified. + * @private + * @this Blockly.Block + */ + validator_: function(newVar) { + newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); + return newVar || null; + }, + /** + * Called when focusing away from the text field. + * Creates a new variable with this name. + * @param {string} newText The new variable name. + * @private + * @this Blockly.FieldTextInput + */ + createNewVar_: function(newText) { + var source = this.sourceBlock_; + if (source && source.workspace && source.workspace.options + && source.workspace.options.parentWorkspace) { + source.workspace.options.parentWorkspace.createVariable(newText); + } + } +}; + +Blockly.Blocks['procedures_callnoreturn'] = { + /** + * Block for calling a procedure with no return value. + * @this Blockly.Block + */ + init: function() { + this.appendDummyInput('TOPROW') + .appendField(this.id, 'NAME'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setColour(Blockly.Blocks.procedures.HUE); + // Tooltip is set in renameProcedure. + this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLNORETURN_HELPURL); + this.arguments_ = []; + this.quarkConnections_ = {}; + this.quarkIds_ = null; + }, + /** + * Returns the name of the procedure this block calls. + * @return {string} Procedure name. + * @this Blockly.Block + */ + getProcedureCall: function() { + // The NAME field is guaranteed to exist, null will never be returned. + return /** @type {string} */ (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 Blockly.Block + */ + renameProcedure: function(oldName, newName) { + if (Blockly.Names.equals(oldName, this.getProcedureCall())) { + this.setFieldValue(newName, 'NAME'); + this.setTooltip( + (this.outputConnection ? Blockly.Msg.PROCEDURES_CALLRETURN_TOOLTIP : + Blockly.Msg.PROCEDURES_CALLNORETURN_TOOLTIP) + .replace('%1', newName)); + } + }, + /** + * Notification that the procedure's parameters have changed. + * @param {!Array.<string>} paramNames New param names, e.g. ['x', 'y', 'z']. + * @param {!Array.<string>} 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 Blockly.Block + */ + setProcedureParameters_: function(paramNames, paramIds) { + // Data structures: + // this.arguments = ['x', 'y'] + // Existing param names. + // this.quarkConnections_ {piua: null, f8b_: Blockly.Connection} + // Look-up of paramIds to connections plugged into the call block. + // this.quarkIds_ = ['piua', 'f8b_'] + // 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. + var defBlock = Blockly.Procedures.getDefinition(this.getProcedureCall(), + this.workspace); + var mutatorOpen = defBlock && defBlock.mutator && + defBlock.mutator.isVisible(); + if (!mutatorOpen) { + this.quarkConnections_ = {}; + this.quarkIds_ = null; + } + if (!paramIds) { + // Reset the quarks (a mutator is about to open). + return; + } + if (goog.array.equals(this.arguments_, paramNames)) { + // No change. + this.quarkIds_ = paramIds; + return; + } + if (paramIds.length != paramNames.length) { + throw 'Error: paramNames and paramIds must be the same length.'; + } + this.setCollapsed(false); + if (!this.quarkIds_) { + // Initialize tracking for this block. + this.quarkConnections_ = {}; + if (paramNames.join('\n') == this.arguments_.join('\n')) { + // No change to the parameters, allow quarkConnections_ to be + // populated with the existing connections. + this.quarkIds_ = paramIds; + } else { + this.quarkIds_ = []; + } + } + // Switch off rendering while the block is rebuilt. + var savedRendered = this.rendered; + this.rendered = false; + // Update the quarkConnections_ with existing connections. + for (var i = 0; i < this.arguments_.length; i++) { + var input = this.getInput('ARG' + i); + if (input) { + var connection = input.connection.targetConnection; + this.quarkConnections_[this.quarkIds_[i]] = connection; + if (mutatorOpen && connection && + paramIds.indexOf(this.quarkIds_[i]) == -1) { + // This connection should no longer be attached to this block. + connection.disconnect(); + connection.getSourceBlock().bumpNeighbours_(); + } + } + } + // Rebuild the block's arguments. + this.arguments_ = [].concat(paramNames); + this.updateShape_(); + this.quarkIds_ = paramIds; + // Reconnect any child blocks. + if (this.quarkIds_) { + for (var i = 0; i < this.arguments_.length; i++) { + var quarkId = this.quarkIds_[i]; + if (quarkId in this.quarkConnections_) { + var connection = this.quarkConnections_[quarkId]; + if (!Blockly.Mutator.reconnect(connection, this, 'ARG' + i)) { + // Block no longer exists or has been attached elsewhere. + delete this.quarkConnections_[quarkId]; + } + } + } + } + // Restore rendering and show the changes. + this.rendered = savedRendered; + if (this.rendered) { + this.render(); + } + }, + /** + * Modify this block to have the correct number of arguments. + * @private + * @this Blockly.Block + */ + updateShape_: function() { + for (var i = 0; i < this.arguments_.length; i++) { + var field = this.getField('ARGNAME' + i); + if (field) { + // Ensure argument name is up to date. + // The argument name field is deterministic based on the mutation, + // no need to fire a change event. + Blockly.Events.disable(); + try { + field.setValue(this.arguments_[i]); + } finally { + Blockly.Events.enable(); + } + } else { + // Add new input. + field = new Blockly.FieldLabel(this.arguments_[i]); + var input = this.appendValueInput('ARG' + i) + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(field, 'ARGNAME' + i); + input.init(); + } + } + // Remove deleted inputs. + while (this.getInput('ARG' + i)) { + this.removeInput('ARG' + i); + i++; + } + // Add 'with:' if there are parameters, remove otherwise. + var topRow = this.getInput('TOPROW'); + if (topRow) { + if (this.arguments_.length) { + if (!this.getField('WITH')) { + topRow.appendField(Blockly.Msg.PROCEDURES_CALL_BEFORE_PARAMS, 'WITH'); + topRow.init(); + } + } else { + if (this.getField('WITH')) { + topRow.removeField('WITH'); + } + } + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + container.setAttribute('name', this.getProcedureCall()); + for (var i = 0; i < this.arguments_.length; i++) { + var parameter = document.createElement('arg'); + parameter.setAttribute('name', this.arguments_[i]); + container.appendChild(parameter); + } + return container; + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + var name = xmlElement.getAttribute('name'); + this.renameProcedure(this.getProcedureCall(), name); + var args = []; + var paramIds = []; + for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() == 'arg') { + args.push(childNode.getAttribute('name')); + paramIds.push(childNode.getAttribute('paramId')); + } + } + this.setProcedureParameters_(args, paramIds); + }, + /** + * Notification that a variable is renaming. + * If the name matches one of this block's variables, rename it. + * @param {string} oldName Previous name of variable. + * @param {string} newName Renamed variable. + * @this Blockly.Block + */ + renameVar: function(oldName, newName) { + for (var i = 0; i < this.arguments_.length; i++) { + if (Blockly.Names.equals(oldName, this.arguments_[i])) { + this.arguments_[i] = newName; + this.getField('ARGNAME' + i).setValue(newName); + } + } + }, + /** + * Procedure calls cannot exist without the corresponding procedure + * definition. Enforce this link whenever an event is fired. + * @this Blockly.Block + */ + onchange: function(event) { + if (!this.workspace || this.workspace.isFlyout) { + // Block is deleted or is in a flyout. + return; + } + if (event.type == Blockly.Events.CREATE && + event.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. + var name = this.getProcedureCall(); + var def = Blockly.Procedures.getDefinition(name, this.workspace); + if (def && (def.type != this.defType_ || + JSON.stringify(def.arguments_) != JSON.stringify(this.arguments_))) { + // The signatures don't match. + def = null; + } + if (!def) { + Blockly.Events.setGroup(event.group); + /** + * Create matching definition block. + * <xml> + * <block type="procedures_defreturn" x="10" y="20"> + * <mutation name="test"> + * <arg name="x"></arg> + * </mutation> + * <field name="NAME">test</field> + * </block> + * </xml> + */ + var xml = goog.dom.createDom('xml'); + var block = goog.dom.createDom('block'); + block.setAttribute('type', this.defType_); + var xy = this.getRelativeToSurfaceXY(); + var x = xy.x + Blockly.SNAP_RADIUS * (this.RTL ? -1 : 1); + var y = xy.y + Blockly.SNAP_RADIUS * 2; + block.setAttribute('x', x); + block.setAttribute('y', y); + var mutation = this.mutationToDom(); + block.appendChild(mutation); + var field = goog.dom.createDom('field'); + field.setAttribute('name', 'NAME'); + field.appendChild(document.createTextNode(this.getProcedureCall())); + block.appendChild(field); + xml.appendChild(block); + Blockly.Xml.domToWorkspace(xml, this.workspace); + Blockly.Events.setGroup(false); + } + } else if (event.type == Blockly.Events.DELETE) { + // Look for the case where a procedure definition has been deleted, + // leaving this block (a procedure call) orphaned. In this case, delete + // the orphan. + var name = this.getProcedureCall(); + var def = Blockly.Procedures.getDefinition(name, this.workspace); + if (!def) { + Blockly.Events.setGroup(event.group); + this.dispose(true, false); + Blockly.Events.setGroup(false); + } + } + }, + /** + * Add menu option to find the definition block for this call. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu: function(options) { + var option = {enabled: true}; + option.text = Blockly.Msg.PROCEDURES_HIGHLIGHT_DEF; + var name = this.getProcedureCall(); + var workspace = this.workspace; + option.callback = function() { + var def = Blockly.Procedures.getDefinition(name, workspace); + def && def.select(); + }; + options.push(option); + }, + defType_: 'procedures_defnoreturn' +}; + +Blockly.Blocks['procedures_callreturn'] = { + /** + * Block for calling a procedure with a return value. + * @this Blockly.Block + */ + init: function() { + this.appendDummyInput('TOPROW') + .appendField('', 'NAME'); + this.setOutput(true); + this.setColour(Blockly.Blocks.procedures.HUE); + // Tooltip is set in domToMutation. + this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLRETURN_HELPURL); + this.arguments_ = []; + this.quarkConnections_ = {}; + this.quarkIds_ = null; + }, + getProcedureCall: Blockly.Blocks['procedures_callnoreturn'].getProcedureCall, + renameProcedure: Blockly.Blocks['procedures_callnoreturn'].renameProcedure, + setProcedureParameters_: + Blockly.Blocks['procedures_callnoreturn'].setProcedureParameters_, + updateShape_: Blockly.Blocks['procedures_callnoreturn'].updateShape_, + mutationToDom: Blockly.Blocks['procedures_callnoreturn'].mutationToDom, + domToMutation: Blockly.Blocks['procedures_callnoreturn'].domToMutation, + renameVar: Blockly.Blocks['procedures_callnoreturn'].renameVar, + onchange: Blockly.Blocks['procedures_callnoreturn'].onchange, + customContextMenu: + Blockly.Blocks['procedures_callnoreturn'].customContextMenu, + defType_: 'procedures_defreturn' +}; + +Blockly.Blocks['procedures_ifreturn'] = { + /** + * Block for conditionally returning a value from a procedure. + * @this Blockly.Block + */ + init: function() { + this.appendValueInput('CONDITION') + .setCheck('Boolean') + .appendField(Blockly.Msg.CONTROLS_IF_MSG_IF); + this.appendValueInput('VALUE') + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + this.setInputsInline(true); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_IFRETURN_TOOLTIP); + this.setHelpUrl(Blockly.Msg.PROCEDURES_IFRETURN_HELPURL); + this.hasReturnValue_ = true; + }, + /** + * Create XML to represent whether this block has a return value. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + container.setAttribute('value', Number(this.hasReturnValue_)); + return container; + }, + /** + * Parse XML to restore whether this block has a return value. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + var value = xmlElement.getAttribute('value'); + this.hasReturnValue_ = (value == 1); + if (!this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendDummyInput('VALUE') + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + } + }, + /** + * Called whenever anything on the workspace changes. + * Add warning if this flow block is not nested inside a loop. + * @param {!Blockly.Events.Abstract} e Change event. + * @this Blockly.Block + */ + onchange: function(e) { + var legal = false; + // Is the block nested in a procedure? + var block = this; + do { + if (this.FUNCTION_TYPES.indexOf(block.type) != -1) { + legal = true; + break; + } + 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(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + this.hasReturnValue_ = false; + } else if (block.type == 'procedures_defreturn' && + !this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendValueInput('VALUE') + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + this.hasReturnValue_ = true; + } + this.setWarningText(null); + } else { + this.setWarningText(Blockly.Msg.PROCEDURES_IFRETURN_WARNING); + } + }, + /** + * List of block types that are functions and thus do not need warnings. + * To add a new function type add this to your code: + * Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func'); + */ + FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn'] +}; diff --git a/src/blockly/blocks/text.js b/src/blockly/blocks/text.js new file mode 100644 index 0000000..c26bb57 --- /dev/null +++ b/src/blockly/blocks/text.js @@ -0,0 +1,685 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Text blocks for Blockly. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Blocks.texts'); + +goog.require('Blockly.Blocks'); + + +/** + * Common HSV hue for all blocks in this category. + */ +Blockly.Blocks.texts.HUE = 160; + +Blockly.Blocks['text'] = { + /** + * Block for text value. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.TEXT_TEXT_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + this.appendDummyInput() + .appendField(this.newQuote_(true)) + .appendField(new Blockly.FieldTextInput(''), 'TEXT') + .appendField(this.newQuote_(false)); + this.setOutput(true, 'String'); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + // Text block is trivial. Use tooltip of parent block if it exists. + this.setTooltip(function() { + var parent = thisBlock.getParent(); + return (parent && parent.getInputsInline() && parent.tooltip) || + Blockly.Msg.TEXT_TEXT_TOOLTIP; + }); + }, + /** + * Create an image of an open or closed quote. + * @param {boolean} open True if open quote, false if closed. + * @return {!Blockly.FieldImage} The field image of the quote. + * @this Blockly.Block + * @private + */ + newQuote_: function(open) { + if (open == this.RTL) { + var file = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAqUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhggONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvBO3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5AoslLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg=='; + } else { + var file = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAn0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMfz9AylsaRRgGzvZAAAAAElFTkSuQmCC'; + } + return new Blockly.FieldImage(file, 12, 12, '"'); + } +}; + +Blockly.Blocks['text_join'] = { + /** + * Block for creating a string made up of any number of elements of any type. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.TEXT_JOIN_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + this.itemCount_ = 2; + this.updateShape_(); + this.setOutput(true, 'String'); + this.setMutator(new Blockly.Mutator(['text_create_join_item'])); + this.setTooltip(Blockly.Msg.TEXT_JOIN_TOOLTIP); + }, + /** + * Create XML to represent number of text inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + container.setAttribute('items', this.itemCount_); + return container; + }, + /** + * Parse XML to restore the text inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); + 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) { + var containerBlock = workspace.newBlock('text_create_join_container'); + containerBlock.initSvg(); + var connection = containerBlock.getInput('STACK').connection; + for (var i = 0; i < this.itemCount_; i++) { + var itemBlock = workspace.newBlock('text_create_join_item'); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + 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) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + // Count number of inputs. + var connections = []; + while (itemBlock) { + connections.push(itemBlock.valueConnection_); + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + // Disconnect any children that don't belong. + for (var i = 0; i < this.itemCount_; i++) { + var connection = this.getInput('ADD' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) == -1) { + connection.disconnect(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (var i = 0; i < this.itemCount_; i++) { + Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); + } + }, + /** + * Store pointers to any connected child blocks. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + saveConnections: function(containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + var i = 0; + while (itemBlock) { + var input = this.getInput('ADD' + i); + itemBlock.valueConnection_ = input && input.connection.targetConnection; + i++; + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + }, + /** + * Modify this block to have the correct number of inputs. + * @private + * @this Blockly.Block + */ + updateShape_: function() { + if (this.itemCount_ && this.getInput('EMPTY')) { + this.removeInput('EMPTY'); + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { + this.appendDummyInput('EMPTY') + .appendField(this.newQuote_(true)) + .appendField(this.newQuote_(false)); + } + // Add new inputs. + for (var i = 0; i < this.itemCount_; i++) { + if (!this.getInput('ADD' + i)) { + var input = this.appendValueInput('ADD' + i); + if (i == 0) { + input.appendField(Blockly.Msg.TEXT_JOIN_TITLE_CREATEWITH); + } + } + } + // Remove deleted inputs. + while (this.getInput('ADD' + i)) { + this.removeInput('ADD' + i); + i++; + } + }, + newQuote_: Blockly.Blocks['text'].newQuote_ +}; + +Blockly.Blocks['text_create_join_container'] = { + /** + * Mutator block for container. + * @this Blockly.Block + */ + init: function() { + this.setColour(Blockly.Blocks.texts.HUE); + this.appendDummyInput() + .appendField(Blockly.Msg.TEXT_CREATE_JOIN_TITLE_JOIN); + this.appendStatementInput('STACK'); + this.setTooltip(Blockly.Msg.TEXT_CREATE_JOIN_TOOLTIP); + this.contextMenu = false; + } +}; + +Blockly.Blocks['text_create_join_item'] = { + /** + * Mutator block for add items. + * @this Blockly.Block + */ + init: function() { + this.setColour(Blockly.Blocks.texts.HUE); + this.appendDummyInput() + .appendField(Blockly.Msg.TEXT_CREATE_JOIN_ITEM_TITLE_ITEM); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.TEXT_CREATE_JOIN_ITEM_TOOLTIP); + this.contextMenu = false; + } +}; + +Blockly.Blocks['text_append'] = { + /** + * Block for appending to a variable in place. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.TEXT_APPEND_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + this.appendValueInput('TEXT') + .appendField(Blockly.Msg.TEXT_APPEND_TO) + .appendField(new Blockly.FieldVariable( + Blockly.Msg.TEXT_APPEND_VARIABLE), 'VAR') + .appendField(Blockly.Msg.TEXT_APPEND_APPENDTEXT); + this.setPreviousStatement(true); + this.setNextStatement(true); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + return Blockly.Msg.TEXT_APPEND_TOOLTIP.replace('%1', + thisBlock.getFieldValue('VAR')); + }); + } +}; + +Blockly.Blocks['text_length'] = { + /** + * Block for string length. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.TEXT_LENGTH_TITLE, + "args0": [ + { + "type": "input_value", + "name": "VALUE", + "check": ['String', 'Array'] + } + ], + "output": 'Number', + "colour": Blockly.Blocks.texts.HUE, + "tooltip": Blockly.Msg.TEXT_LENGTH_TOOLTIP, + "helpUrl": Blockly.Msg.TEXT_LENGTH_HELPURL + }); + } +}; + +Blockly.Blocks['text_isEmpty'] = { + /** + * Block for is the string null? + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.TEXT_ISEMPTY_TITLE, + "args0": [ + { + "type": "input_value", + "name": "VALUE", + "check": ['String', 'Array'] + } + ], + "output": 'Boolean', + "colour": Blockly.Blocks.texts.HUE, + "tooltip": Blockly.Msg.TEXT_ISEMPTY_TOOLTIP, + "helpUrl": Blockly.Msg.TEXT_ISEMPTY_HELPURL + }); + } +}; + +Blockly.Blocks['text_indexOf'] = { + /** + * Block for finding a substring in the text. + * @this Blockly.Block + */ + init: function() { + var OPERATORS = + [[Blockly.Msg.TEXT_INDEXOF_OPERATOR_FIRST, 'FIRST'], + [Blockly.Msg.TEXT_INDEXOF_OPERATOR_LAST, 'LAST']]; + this.setHelpUrl(Blockly.Msg.TEXT_INDEXOF_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + this.setOutput(true, 'Number'); + this.appendValueInput('VALUE') + .setCheck('String') + .appendField(Blockly.Msg.TEXT_INDEXOF_INPUT_INTEXT); + this.appendValueInput('FIND') + .setCheck('String') + .appendField(new Blockly.FieldDropdown(OPERATORS), 'END'); + if (Blockly.Msg.TEXT_INDEXOF_TAIL) { + this.appendDummyInput().appendField(Blockly.Msg.TEXT_INDEXOF_TAIL); + } + this.setInputsInline(true); + var tooltip = Blockly.Msg.TEXT_INDEXOF_TOOLTIP + .replace('%1', Blockly.Blocks.ONE_BASED_INDEXING ? '0' : '-1'); + this.setTooltip(tooltip); + } +}; + +Blockly.Blocks['text_charAt'] = { + /** + * Block for getting a character from the string. + * @this Blockly.Block + */ + init: function() { + this.WHERE_OPTIONS = + [[Blockly.Msg.TEXT_CHARAT_FROM_START, 'FROM_START'], + [Blockly.Msg.TEXT_CHARAT_FROM_END, 'FROM_END'], + [Blockly.Msg.TEXT_CHARAT_FIRST, 'FIRST'], + [Blockly.Msg.TEXT_CHARAT_LAST, 'LAST'], + [Blockly.Msg.TEXT_CHARAT_RANDOM, 'RANDOM']]; + this.setHelpUrl(Blockly.Msg.TEXT_CHARAT_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + this.setOutput(true, 'String'); + this.appendValueInput('VALUE') + .setCheck('String') + .appendField(Blockly.Msg.TEXT_CHARAT_INPUT_INTEXT); + this.appendDummyInput('AT'); + this.setInputsInline(true); + this.updateAt_(true); + // Assign 'this' to a variable for use in the tooltip closure below. + var thisBlock = this; + this.setTooltip(function() { + var where = thisBlock.getFieldValue('WHERE'); + var tooltip = Blockly.Msg.TEXT_CHARAT_TOOLTIP; + if (where == 'FROM_START' || where == 'FROM_END') { + tooltip += ' ' + Blockly.Msg.LISTS_INDEX_FROM_END_TOOLTIP + .replace('%1', Blockly.Blocks.ONE_BASED_INDEXING ? '#1' : '#0'); + } + return tooltip; + }); + }, + /** + * Create XML to represent whether there is an 'AT' input. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE; + container.setAttribute('at', isAt); + return container; + }, + /** + * Parse XML to restore the 'AT' input. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + // Note: Until January 2013 this block did not have mutations, + // so 'at' defaults to true. + var isAt = (xmlElement.getAttribute('at') != 'false'); + this.updateAt_(isAt); + }, + /** + * Create or delete an input for the numeric index. + * @param {boolean} isAt True if the input should exist. + * @private + * @this Blockly.Block + */ + updateAt_: function(isAt) { + // Destroy old 'AT' and 'ORDINAL' inputs. + this.removeInput('AT'); + this.removeInput('ORDINAL', true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT').setCheck('Number'); + if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) { + this.appendDummyInput('ORDINAL') + .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX); + } + } else { + this.appendDummyInput('AT'); + } + if (Blockly.Msg.TEXT_CHARAT_TAIL) { + this.removeInput('TAIL', true); + this.appendDummyInput('TAIL') + .appendField(Blockly.Msg.TEXT_CHARAT_TAIL); + } + var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) { + var newAt = (value == 'FROM_START') || (value == 'FROM_END'); + // The 'isAt' variable is available due to this function being a closure. + if (newAt != isAt) { + var block = this.sourceBlock_; + block.updateAt_(newAt); + // This menu has been destroyed and replaced. Update the replacement. + block.setFieldValue(value, 'WHERE'); + return null; + } + return undefined; + }); + this.getInput('AT').appendField(menu, 'WHERE'); + } +}; + +Blockly.Blocks['text_getSubstring'] = { + /** + * Block for getting substring. + * @this Blockly.Block + */ + init: function() { + this['WHERE_OPTIONS_1'] = + [[Blockly.Msg.TEXT_GET_SUBSTRING_START_FROM_START, 'FROM_START'], + [Blockly.Msg.TEXT_GET_SUBSTRING_START_FROM_END, 'FROM_END'], + [Blockly.Msg.TEXT_GET_SUBSTRING_START_FIRST, 'FIRST']]; + this['WHERE_OPTIONS_2'] = + [[Blockly.Msg.TEXT_GET_SUBSTRING_END_FROM_START, 'FROM_START'], + [Blockly.Msg.TEXT_GET_SUBSTRING_END_FROM_END, 'FROM_END'], + [Blockly.Msg.TEXT_GET_SUBSTRING_END_LAST, 'LAST']]; + this.setHelpUrl(Blockly.Msg.TEXT_GET_SUBSTRING_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + this.appendValueInput('STRING') + .setCheck('String') + .appendField(Blockly.Msg.TEXT_GET_SUBSTRING_INPUT_IN_TEXT); + this.appendDummyInput('AT1'); + this.appendDummyInput('AT2'); + if (Blockly.Msg.TEXT_GET_SUBSTRING_TAIL) { + this.appendDummyInput('TAIL') + .appendField(Blockly.Msg.TEXT_GET_SUBSTRING_TAIL); + } + this.setInputsInline(true); + this.setOutput(true, 'String'); + this.updateAt_(1, true); + this.updateAt_(2, true); + this.setTooltip(Blockly.Msg.TEXT_GET_SUBSTRING_TOOLTIP); + }, + /** + * Create XML to represent whether there are 'AT' inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + var isAt1 = this.getInput('AT1').type == Blockly.INPUT_VALUE; + container.setAttribute('at1', isAt1); + var isAt2 = this.getInput('AT2').type == Blockly.INPUT_VALUE; + container.setAttribute('at2', isAt2); + return container; + }, + /** + * Parse XML to restore the 'AT' inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + var isAt1 = (xmlElement.getAttribute('at1') == 'true'); + var isAt2 = (xmlElement.getAttribute('at2') == 'true'); + this.updateAt_(1, isAt1); + this.updateAt_(2, isAt2); + }, + /** + * Create or delete an input for a numeric index. + * This block has two such inputs, independant of each other. + * @param {number} n Specify first or second input (1 or 2). + * @param {boolean} isAt True if the input should exist. + * @private + * @this Blockly.Block + */ + updateAt_: function(n, isAt) { + // Create or delete an input for the numeric index. + // Destroy old 'AT' and 'ORDINAL' inputs. + this.removeInput('AT' + n); + this.removeInput('ORDINAL' + n, true); + // Create either a value 'AT' input or a dummy input. + if (isAt) { + this.appendValueInput('AT' + n).setCheck('Number'); + if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) { + this.appendDummyInput('ORDINAL' + n) + .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX); + } + } else { + this.appendDummyInput('AT' + n); + } + // Move tail, if present, to end of block. + if (n == 2 && Blockly.Msg.TEXT_GET_SUBSTRING_TAIL) { + this.removeInput('TAIL', true); + this.appendDummyInput('TAIL') + .appendField(Blockly.Msg.TEXT_GET_SUBSTRING_TAIL); + } + var menu = new Blockly.FieldDropdown(this['WHERE_OPTIONS_' + n], + function(value) { + var newAt = (value == 'FROM_START') || (value == 'FROM_END'); + // The 'isAt' variable is available due to this function being a + // closure. + if (newAt != isAt) { + var block = this.sourceBlock_; + block.updateAt_(n, newAt); + // This menu has been destroyed and replaced. + // Update the replacement. + block.setFieldValue(value, 'WHERE' + n); + return null; + } + return undefined; + }); + + this.getInput('AT' + n) + .appendField(menu, 'WHERE' + n); + if (n == 1) { + this.moveInputBefore('AT1', 'AT2'); + } + } +}; + +Blockly.Blocks['text_changeCase'] = { + /** + * Block for changing capitalization. + * @this Blockly.Block + */ + init: function() { + var OPERATORS = + [[Blockly.Msg.TEXT_CHANGECASE_OPERATOR_UPPERCASE, 'UPPERCASE'], + [Blockly.Msg.TEXT_CHANGECASE_OPERATOR_LOWERCASE, 'LOWERCASE'], + [Blockly.Msg.TEXT_CHANGECASE_OPERATOR_TITLECASE, 'TITLECASE']]; + this.setHelpUrl(Blockly.Msg.TEXT_CHANGECASE_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + this.appendValueInput('TEXT') + .setCheck('String') + .appendField(new Blockly.FieldDropdown(OPERATORS), 'CASE'); + this.setOutput(true, 'String'); + this.setTooltip(Blockly.Msg.TEXT_CHANGECASE_TOOLTIP); + } +}; + +Blockly.Blocks['text_trim'] = { + /** + * Block for trimming spaces. + * @this Blockly.Block + */ + init: function() { + var OPERATORS = + [[Blockly.Msg.TEXT_TRIM_OPERATOR_BOTH, 'BOTH'], + [Blockly.Msg.TEXT_TRIM_OPERATOR_LEFT, 'LEFT'], + [Blockly.Msg.TEXT_TRIM_OPERATOR_RIGHT, 'RIGHT']]; + this.setHelpUrl(Blockly.Msg.TEXT_TRIM_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + this.appendValueInput('TEXT') + .setCheck('String') + .appendField(new Blockly.FieldDropdown(OPERATORS), 'MODE'); + this.setOutput(true, 'String'); + this.setTooltip(Blockly.Msg.TEXT_TRIM_TOOLTIP); + } +}; + +Blockly.Blocks['text_print'] = { + /** + * Block for print statement. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.TEXT_PRINT_TITLE, + "args0": [ + { + "type": "input_value", + "name": "TEXT" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": Blockly.Blocks.texts.HUE, + "tooltip": Blockly.Msg.TEXT_PRINT_TOOLTIP, + "helpUrl": Blockly.Msg.TEXT_PRINT_HELPURL + }); + } +}; + +Blockly.Blocks['text_prompt_ext'] = { + /** + * Block for prompt function (external message). + * @this Blockly.Block + */ + init: function() { + var TYPES = + [[Blockly.Msg.TEXT_PROMPT_TYPE_TEXT, 'TEXT'], + [Blockly.Msg.TEXT_PROMPT_TYPE_NUMBER, 'NUMBER']]; + this.setHelpUrl(Blockly.Msg.TEXT_PROMPT_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + // Assign 'this' to a variable for use in the closures below. + var thisBlock = this; + var dropdown = new Blockly.FieldDropdown(TYPES, function(newOp) { + thisBlock.updateType_(newOp); + }); + this.appendValueInput('TEXT') + .appendField(dropdown, 'TYPE'); + this.setOutput(true, 'String'); + this.setTooltip(function() { + return (thisBlock.getFieldValue('TYPE') == 'TEXT') ? + Blockly.Msg.TEXT_PROMPT_TOOLTIP_TEXT : + Blockly.Msg.TEXT_PROMPT_TOOLTIP_NUMBER; + }); + }, + /** + * Modify this block to have the correct output type. + * @param {string} newOp Either 'TEXT' or 'NUMBER'. + * @private + * @this Blockly.Block + */ + updateType_: function(newOp) { + this.outputConnection.setCheck(newOp == 'NUMBER' ? 'Number' : 'String'); + }, + /** + * Create XML to represent the output type. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + container.setAttribute('type', this.getFieldValue('TYPE')); + return container; + }, + /** + * Parse XML to restore the output type. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + this.updateType_(xmlElement.getAttribute('type')); + } +}; + +Blockly.Blocks['text_prompt'] = { + /** + * Block for prompt function (internal message). + * The 'text_prompt_ext' block is preferred as it is more flexible. + * @this Blockly.Block + */ + init: function() { + var TYPES = + [[Blockly.Msg.TEXT_PROMPT_TYPE_TEXT, 'TEXT'], + [Blockly.Msg.TEXT_PROMPT_TYPE_NUMBER, 'NUMBER']]; + // Assign 'this' to a variable for use in the closures below. + var thisBlock = this; + this.setHelpUrl(Blockly.Msg.TEXT_PROMPT_HELPURL); + this.setColour(Blockly.Blocks.texts.HUE); + var dropdown = new Blockly.FieldDropdown(TYPES, function(newOp) { + thisBlock.updateType_(newOp); + }); + this.appendDummyInput() + .appendField(dropdown, 'TYPE') + .appendField(this.newQuote_(true)) + .appendField(new Blockly.FieldTextInput(''), 'TEXT') + .appendField(this.newQuote_(false)); + this.setOutput(true, 'String'); + this.setTooltip(function() { + return (thisBlock.getFieldValue('TYPE') == 'TEXT') ? + Blockly.Msg.TEXT_PROMPT_TOOLTIP_TEXT : + Blockly.Msg.TEXT_PROMPT_TOOLTIP_NUMBER; + }); + }, + newQuote_: Blockly.Blocks['text'].newQuote_, + updateType_: Blockly.Blocks['text_prompt_ext'].updateType_, + mutationToDom: Blockly.Blocks['text_prompt_ext'].mutationToDom, + domToMutation: Blockly.Blocks['text_prompt_ext'].domToMutation +}; diff --git a/src/blockly/blocks/variables.js b/src/blockly/blocks/variables.js new file mode 100644 index 0000000..3b7d99f --- /dev/null +++ b/src/blockly/blocks/variables.js @@ -0,0 +1,100 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Variable blocks for Blockly. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Blocks.variables'); + +goog.require('Blockly.Blocks'); + + +/** + * Common HSV hue for all blocks in this category. + */ +Blockly.Blocks.variables.HUE = 330; + +Blockly.Blocks['variables_get'] = { + /** + * Block for variable getter. + * @this Blockly.Block + */ + init: function() { + this.setHelpUrl(Blockly.Msg.VARIABLES_GET_HELPURL); + this.setColour(Blockly.Blocks.variables.HUE); + this.appendDummyInput() + .appendField(new Blockly.FieldVariable( + Blockly.Msg.VARIABLES_DEFAULT_NAME), 'VAR'); + this.setOutput(true); + this.setTooltip(Blockly.Msg.VARIABLES_GET_TOOLTIP); + this.contextMenuMsg_ = Blockly.Msg.VARIABLES_GET_CREATE_SET; + }, + contextMenuType_: 'variables_set', + /** + * Add menu option to create getter/setter block for this setter/getter. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu: function(options) { + var option = {enabled: true}; + var name = this.getFieldValue('VAR'); + option.text = this.contextMenuMsg_.replace('%1', name); + var xmlField = goog.dom.createDom('field', null, name); + xmlField.setAttribute('name', 'VAR'); + var xmlBlock = goog.dom.createDom('block', null, xmlField); + xmlBlock.setAttribute('type', this.contextMenuType_); + option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); + options.push(option); + } +}; + +Blockly.Blocks['variables_set'] = { + /** + * Block for variable setter. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": Blockly.Msg.VARIABLES_SET, + "args0": [ + { + "type": "field_variable", + "name": "VAR", + "variable": Blockly.Msg.VARIABLES_DEFAULT_NAME + }, + { + "type": "input_value", + "name": "VALUE" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": Blockly.Blocks.variables.HUE, + "tooltip": Blockly.Msg.VARIABLES_SET_TOOLTIP, + "helpUrl": Blockly.Msg.VARIABLES_SET_HELPURL + }); + this.contextMenuMsg_ = Blockly.Msg.VARIABLES_SET_CREATE_GET; + }, + contextMenuType_: 'variables_get', + customContextMenu: Blockly.Blocks['variables_get'].customContextMenu +}; |