From 7b0275cd70cb715d0d633e4a5468173fcdd668a8 Mon Sep 17 00:00:00 2001 From: Andrew n marshall Date: Mon, 23 Jan 2017 10:23:55 -0800 Subject: [PATCH] Porting math.js blocks to JSON (#846) Moving all `math.js` definitions into a single JSON array, complete with i18n syntax for all messages, dropdowns, and tooltips. Adding Blockly.Extensions.buildTooltipForDropdown(..) to facilitate the creation and error-checking of tooltips that update based on the value of a dropdown. Now warn on raw string in JSON 'extensions'. --- blocks/colour.js | 8 +- blocks/math.js | 811 ++++++++++++++++++++--------------------- core/block.js | 5 + core/extensions.js | 76 ++++ core/field_dropdown.js | 18 +- core/utils.js | 61 +++- 6 files changed, 539 insertions(+), 440 deletions(-) diff --git a/blocks/colour.js b/blocks/colour.js index c26d64546..ca52a7b50 100644 --- a/blocks/colour.js +++ b/blocks/colour.js @@ -21,9 +21,9 @@ /** * @fileoverview Colour blocks for Blockly. * - * This file is scraped to extract .json file definitions. The array passed to - * defineBlocksWithJsonArray(..) must be strict JSON: double quotes only, no - * outside references, no functions, no trailing commasa, etc. The one + * This file is scraped to extract a .json file of block definitions. The array + * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes + * only, no outside references, no functions, no trailing commas, etc. The one * exception is end-of-line comments, which the scraper will remove. * @author fraser@google.com (Neil Fraser) */ @@ -128,4 +128,4 @@ Blockly.defineBlocksWithJsonArray([ "helpUrl": "%{BKY_COLOUR_BLEND_HELPURL}", "tooltip": "%{BKY_COLOUR_BLEND_TOOLTIP}" } -]); // End of defineBlocksWithJsonArray(...) DO NOT DELETE +]); // End of defineBlocksWithJsonArray(...) (Do not delete this comment.) diff --git a/blocks/math.js b/blocks/math.js index 8eb285c1b..935c2de47 100644 --- a/blocks/math.js +++ b/blocks/math.js @@ -20,6 +20,11 @@ /** * @fileoverview Math blocks for Blockly. + * + * This file is scraped to extract a .json file of block definitions. The array + * passed to defineBlocksWithJsonArray(..) must be strict JSON: double quotes + * only, no outside references, no functions, no trailing commas, etc. The one + * exception is end-of-line comments, which the scraper will remove. * @author q.neutron@gmail.com (Quynh Neutron) */ 'use strict'; @@ -31,6 +36,7 @@ goog.require('Blockly.Blocks'); /** * Common HSV hue for all blocks in this category. + * Should be the same as Blockly.Msg.MATH_HUE */ Blockly.Blocks.math.HUE = 230; @@ -49,216 +55,371 @@ Blockly.defineBlocksWithJsonArray([ "helpUrl": "%{BKY_MATH_NUMBER_HELPURL}", "tooltip": "%{BKY_MATH_NUMBER_TOOLTIP}", "extensions": ["parent_tooltip_when_inline"] - } -]); - -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); }, + + // Block for basic arithmetic operator. + { + "type": "math_arithmetic", + "message0": "%1 %2 %3", + "args0": [ + { + "type": "input_value", + "name": "A", + "check": "Number" + }, + { + "type": "field_dropdown", + "name": "OP", + "options": [ + ["%{BKY_MATH_ADDITION_SYMBOL}", "ADD"], + ["%{BKY_MATH_SUBTRACTION_SYMBOL}", "MINUS"], + ["%{BKY_MATH_MULTIPLICATION_SYMBOL}", "MULTIPLY"], + ["%{BKY_MATH_DIVISION_SYMBOL}", "DIVIDE"], + ["%{BKY_MATH_POWER_SYMBOL}", "POWER"] + ] + }, + { + "type": "input_value", + "name": "B", + "check": "Number" + } + ], + "inputsInline": true, + "output": "Number", + "colour": "%{BKY_MATH_HUE}", + "helpUrl": "%{BKY_MATH_ARITHMETIC_HELPURL}", + "extensions": ["math_op_tooltip"] + }, + + // Block for advanced math operators with single operand. + { + "type": "math_single", + "message0": "%1 %2", + "args0": [ + { + "type": "field_dropdown", + "name": "OP", + "options": [ + ["%{BKY_MATH_SINGLE_OP_ROOT}", 'ROOT'], + ["%{BKY_MATH_SINGLE_OP_ABSOLUTE}", 'ABS'], + ['-', 'NEG'], + ['ln', 'LN'], + ['log10', 'LOG10'], + ['e^', 'EXP'], + ['10^', 'POW10'] + ] + }, + { + "type": "input_value", + "name": "NUM", + "check": "Number" + } + ], + "output": "Number", + "colour": "%{BKY_MATH_HUE}", + "helpUrl": "%{BKY_MATH_SINGLE_HELPURL}", + "extensions": ["math_op_tooltip"] + }, + + // Block for trigonometry operators. + { + "type": "math_trig", + "message0": "%1 %2", + "args0": [ + { + "type": "field_dropdown", + "name": "OP", + "options": [ + ["%{BKY_MATH_TRIG_SIN}", "SIN"], + ["%{BKY_MATH_TRIG_COS}", "COS"], + ["%{BKY_MATH_TRIG_TAN}", "TAN"], + ["%{BKY_MATH_TRIG_ASIN}", "ASIN"], + ["%{BKY_MATH_TRIG_ACOS}", "ACOS"], + ["%{BKY_MATH_TRIG_ATAN}", "ATAN"] + ] + }, + { + "type": "input_value", + "name": "NUM", + "check": "Number" + } + ], + "output": "Number", + "colour": "%{BKY_MATH_HUE}", + "helpUrl": "%{BKY_MATH_TRIG_HELPURL}", + "extensions": ["math_op_tooltip"] + }, + + // Block for constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. + { + "type": "math_constant", + "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": "%{BKY_MATH_HUE}", + "tooltip": "%{BKY_MATH_CONSTANT_TOOLTIP}", + "helpUrl": "%{BKY_MATH_CONSTANT_HELPURL}" + }, + + // Block for checking if a number is even, odd, prime, whole, positive, + // negative or if it is divisible by certain number. + { + "type": "math_number_property", + "message0": "%1 %2", + "args0": [ + { + "type": "input_value", + "name": "NUMBER_TO_CHECK", + "check": "Number" + }, + { + "type": "field_dropdown", + "name": "PROPERTY", + "options": [ + ["%{BKY_MATH_IS_EVEN}", "EVEN"], + ["%{BKY_MATH_IS_ODD}", "ODD"], + ["%{BKY_MATH_IS_PRIME}", "PRIME"], + ["%{BKY_MATH_IS_WHOLE}", "WHOLE"], + ["%{BKY_MATH_IS_POSITIVE}", "POSITIVE"], + ["%{BKY_MATH_IS_NEGATIVE}", "NEGATIVE"], + ["%{BKY_MATH_IS_DIVISIBLE_BY}", "DIVISIBLE_BY"] + ] + } + ], + "inputsInline": true, + "output": "Boolean", + "colour": "%{BKY_MATH_HUE}", + "tooltip": "%{BKY_MATH_IS_TOOLTIP}", + "extensions": ["math_is_divisibleby_mutator"] + }, + + // Block for adding to a variable in place. + { + "type": "math_change", + "message0": "%{BKY_MATH_CHANGE_TITLE}", + "args0": [ + { + "type": "field_variable", + "name": "VAR", + "variable": "%{BKY_MATH_CHANGE_TITLE_ITEM}" + }, + { + "type": "input_value", + "name": "DELTA", + "check": "Number" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": "%{BKY_MATH_HUE}", + "helpUrl": "%{BKY_MATH_CHANGE_HELPURL}", + "extensions": ["math_change_tooltip"] + }, + + // Block for rounding functions. + { + "type": "math_round", + "message0": "%1 %2", + "args0": [ + { + "type": "field_dropdown", + "name": "OP", + "options": [ + ["%{BKY_MATH_ROUND_OPERATOR_ROUND}", "ROUND"], + ["%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}", "ROUNDUP"], + ["%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}", "ROUNDDOWN"] + ] + }, + { + "type": "input_value", + "name": "NUM", + "check": "Number" + } + ], + "output": "Number", + "colour": "%{BKY_MATH_HUE}", + "helpUrl": "%{BKY_MATH_ROUND_HELPURL}", + "tooltip": "%{BKY_MATH_ROUND_TOOLTIP}" + }, + + // Block for evaluating a list of numbers to return sum, average, min, max, + // etc. Some functions also work on text (min, max, mode, median). + { + "type": "math_on_list", + "message0": "%1 %2", + "args0": [ + { + "type": "field_dropdown", + "name": "OP", + "options": [ + ["%{BKY_MATH_ONLIST_OPERATOR_SUM}", "SUM"], + ["%{BKY_MATH_ONLIST_OPERATOR_MIN}", "MIN"], + ["%{BKY_MATH_ONLIST_OPERATOR_MAX}", "MAX"], + ["%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}", "AVERAGE"], + ["%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}", "MEDIAN"], + ["%{BKY_MATH_ONLIST_OPERATOR_MODE}", "MODE"], + ["%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}", "STD_DEV"], + ["%{BKY_MATH_ONLIST_OPERATOR_RANDOM}", "RANDOM"] + ] + }, + { + "type": "input_value", + "name": "LIST", + "check": "Array" + } + ], + "output": "Number", + "colour": "%{BKY_MATH_HUE}", + "helpUrl": "%{BKY_MATH_ONLIST_HELPURL}", + "extensions": ["math_op_tooltip", "math_modes_of_list_mutator"] + }, + + // Block for remainder of a division. + { + "type": "math_modulo", + "message0": "%{BKY_MATH_MODULO_TITLE}", + "args0": [ + { + "type": "input_value", + "name": "DIVIDEND", + "check": "Number" + }, + { + "type": "input_value", + "name": "DIVISOR", + "check": "Number" + } + ], + "inputsInline": true, + "output": "Number", + "colour": "%{BKY_MATH_HUE}", + "tooltip": "%{BKY_MATH_MODULO_TOOLTIP}", + "helpUrl": "%{BKY_MATH_MODULO_HELPURL}" + }, + + // Block for constraining a number between two limits. + { + "type": "math_constrain", + "message0": "%{BKY_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": "%{BKY_MATH_HUE}", + "tooltip": "%{BKY_MATH_CONSTRAIN_TOOLTIP}", + "helpUrl": "%{BKY_MATH_CONSTRAIN_HELPURL}" + }, + + // Block for random integer between [X] and [Y]. + { + "type": "math_random_int", + "message0": "%{BKY_MATH_RANDOM_INT_TITLE}", + "args0": [ + { + "type": "input_value", + "name": "FROM", + "check": "Number" + }, + { + "type": "input_value", + "name": "TO", + "check": "Number" + } + ], + "inputsInline": true, + "output": "Number", + "colour": "%{BKY_MATH_HUE}", + "tooltip": "%{BKY_MATH_RANDOM_INT_TOOLTIP}", + "helpUrl": "%{BKY_MATH_RANDOM_INT_HELPURL}" + }, + + // Block for random integer between [X] and [Y]. + { + "type": "math_random_float", + "message0": "%{BKY_MATH_RANDOM_FLOAT_TITLE_RANDOM}", + "output": "Number", + "colour": "%{BKY_MATH_HUE}", + "tooltip": "%{BKY_MATH_RANDOM_FLOAT_TOOLTIP}", + "helpUrl": "%{BKY_MATH_RANDOM_FLOAT_HELPURL}" + } +]); // End of defineBlocksWithJsonArray(...) (Do not delete this comment.) + +/** + * Mapping of math block OP value to tooltip message for blocks + * math_arithmetic, math_simple, math_trig, and math_on_lists. + * + * Messages are not dereferenced here in order to capture possible language + * changes. + */ +Blockly.Blocks.math.TOOLTIPS_BY_OP_ = { + // math_arithmetic + 'ADD': '%{BKY_MATH_ARITHMETIC_TOOLTIP_ADD}', + 'MINUS': '%{BKY_MATH_ARITHMETIC_TOOLTIP_MINUS}', + 'MULTIPLY': '%{BKY_MATH_ARITHMETIC_TOOLTIP_MULTIPLY}', + 'DIVIDE': '%{BKY_MATH_ARITHMETIC_TOOLTIP_DIVIDE}', + 'POWER': '%{BKY_MATH_ARITHMETIC_TOOLTIP_POWER}', + + // math_simple + 'ROOT': '%{BKY_MATH_SINGLE_TOOLTIP_ROOT}', + 'ABS': '%{BKY_MATH_SINGLE_TOOLTIP_ABS}', + 'NEG': '%{BKY_MATH_SINGLE_TOOLTIP_NEG}', + 'LN': '%{BKY_MATH_SINGLE_TOOLTIP_LN}', + 'LOG10': '%{BKY_MATH_SINGLE_TOOLTIP_LOG10}', + 'EXP': '%{BKY_MATH_SINGLE_TOOLTIP_EXP}', + 'POW10': '%{BKY_MATH_SINGLE_TOOLTIP_POW10}', + + // math_trig + 'SIN': '%{BKY_MATH_TRIG_TOOLTIP_SIN}', + 'COS': '%{BKY_MATH_TRIG_TOOLTIP_COS}', + 'TAN': '%{BKY_MATH_TRIG_TOOLTIP_TAN}', + 'ASIN': '%{BKY_MATH_TRIG_TOOLTIP_ASIN}', + 'ACOS': '%{BKY_MATH_TRIG_TOOLTIP_ACOS}', + 'ATAN': '%{BKY_MATH_TRIG_TOOLTIP_ATAN}', + + // math_on_lists + 'SUM': '%{BKY_MATH_ONLIST_TOOLTIP_SUM}', + 'MIN': '%{BKY_MATH_ONLIST_TOOLTIP_MIN}', + 'MAX': '%{BKY_MATH_ONLIST_TOOLTIP_MAX}', + 'AVERAGE': '%{BKY_MATH_ONLIST_TOOLTIP_AVERAGE}', + 'MEDIAN': '%{BKY_MATH_ONLIST_TOOLTIP_MEDIAN}', + 'MODE': '%{BKY_MATH_ONLIST_TOOLTIP_MODE}', + 'STD_DEV': '%{BKY_MATH_ONLIST_TOOLTIP_STD_DEV}', + 'RANDOM': '%{BKY_MATH_ONLIST_TOOLTIP_RANDOM}', +}; +Blockly.Extensions.register("math_op_tooltip", + Blockly.Extensions.buildTooltipForDropdown( + 'OP', Blockly.Blocks.math.TOOLTIPS_BY_OP_)); + + +Blockly.Blocks.math.IS_DIVISIBLEBY_MUTATOR_MIXIN_ = { /** * Create XML to represent whether the 'divisorInput' should be present. * @return {Element} XML storage element. @@ -299,114 +460,34 @@ Blockly.Blocks['math_number_property'] = { } }; -Blockly.Blocks['math_change'] = { +Blockly.Extensions.register("math_is_divisibleby_mutator", /** - * Block for adding to a variable in place. - * @this Blockly.Block + * Update shape (add/remove divisor input) based on whether property is + * "divisble by". + * @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 + function() { + goog.mixin(this, Blockly.Blocks.math.IS_DIVISIBLEBY_MUTATOR_MIXIN_); + this.getField('PROPERTY').setValidator(function(option) { + var divisorInput = (option == 'DIVISIBLE_BY'); + this.sourceBlock_.updateShape_(divisorInput); }); - // Assign 'this' to a variable for use in the tooltip closure below. + }); + +Blockly.Extensions.register("math_change_tooltip", + /** + * Update tooltip with named variable. + * @this {Blockly.Block} + */ + function() { 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]; - }); - }, +Blockly.Blocks.math.LIST_MODES_MUTATOR_MIXIN_ = { /** * Modify this block to have the correct output type. * @param {string} newOp Either 'MODE' or some op than returns a number. @@ -439,111 +520,15 @@ Blockly.Blocks['math_on_list'] = { this.updateType_(xmlElement.getAttribute('op')); } }; - -Blockly.Blocks['math_modulo'] = { +Blockly.Extensions.register("math_modes_of_list_mutator", /** - * Block for remainder of a division. - * @this Blockly.Block + * Update output type based on whether the operator is "mode". + * @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 + function() { + var thisBlock = this; + goog.mixin(this, Blockly.Blocks.math.LIST_MODES_MUTATOR_MIXIN_); + this.getField('OP').setValidator(function(newOp) { + thisBlock.updateType_(newOp); }); - } -}; - -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/core/block.js b/core/block.js index bad026075..f8c65c24c 100644 --- a/core/block.js +++ b/core/block.js @@ -1024,6 +1024,11 @@ Blockly.Block.prototype.jsonInit = function(json) { var localizedValue = Blockly.utils.replaceMessageReferences(rawValue); this.setHelpUrl(localizedValue); } + if (goog.isString(json['extensions'])) { + console.warn('JSON attribute \'extensions\' should be an array of ' + + 'strings. Found raw string in JSON for \'' + json['type'] + '\' block.'); + json['extensions'] = [json['extensions']]; // Correct and continue. + } if (Array.isArray(json['extensions'])) { var extensionNames = json['extensions']; for (var i = 0; i < extensionNames.length; ++i) { diff --git a/core/extensions.js b/core/extensions.js index 12337b018..10eaac032 100644 --- a/core/extensions.js +++ b/core/extensions.js @@ -74,6 +74,82 @@ Blockly.Extensions.apply = function(name, block) { extensionFn.apply(block); }; +/** + * Builds an extension function that will map a dropdown value to a tooltip string. + * Tooltip strings will be passed through Blockly.utils.checkMessageReferences(..) + * immediately and Blockly.utils.replaceMessageReferences(..) at display time. + * @param {string} dropdownName The name of the field whose value is the key + * to the lookup table. + * @param {!Object} lookupTable The table of field values to + * tooltip text. + * @return {Function} The extension function. + */ +Blockly.Extensions.buildTooltipForDropdown = function(dropdownName, lookupTable) { + // List of block types already validated, to minimize duplicate warnings. + var blockTypesChecked = []; + + // Validate message strings early. + for (var key in lookupTable) { + Blockly.utils.checkMessageReferences(lookupTable[key]); + } + + /** + * The actual extension. + * @this {Blockly.Block} + */ + return function() { + var thisBlock = this; + + if (this.type && !blockTypesChecked.includes(this.type)) { + Blockly.Extensions.checkDropdownOptionsInTable_( + this, dropdownName, lookupTable); + blockTypesChecked.push(this.type); + } + + this.setTooltip(function() { + var value = thisBlock.getFieldValue(dropdownName); + var tooltip = lookupTable[value]; + if (tooltip == null) { + if (!blockTypesChecked.includes(thisBlock.type)) { + // Warn for missing values on generated tooltips + var warning = 'No tooltip mapping for value ' + value + + ' of field ' + dropdownName; + if (thisBlock.type != null) { + warning += (' of block type ' + thisBlock.type); + } + console.warn(warning + '.'); + } + } else { + tooltip = Blockly.utils.replaceMessageReferences(tooltip); + } + return tooltip; + }); + }; +}; + +/** + * Checks all options keys are present in the provided string lookup table. + * Emits console warnings when they are not. + * @param {!Blockly.Block} block The block containing the dropdown + * @param {string} dropdownName The name of the dropdown + * @param {!Object} lookupTable The string lookup table + */ +Blockly.Extensions.checkDropdownOptionsInTable_ = + function(block, dropdownName, lookupTable) { + // Validate all dropdown options have values. + var dropdown = block.getField(dropdownName); + if (!dropdown.isOptionListDynamic()) { + var options = dropdown.getOptions(); + for (var i = 0; i < options.length; ++i) { + var optionKey = options[i][1]; // label, then value + if (lookupTable[optionKey] == null) { + console.warn('No tooltip mapping for value ' + optionKey + + ' of field ' + dropdownName + ' of block type ' + block.type); + } + } + } + }; + /** * Configures the tooltip to mimic the parent block when connected. Otherwise, * uses the tooltip text at the time this extension is initialized. This takes diff --git a/core/field_dropdown.js b/core/field_dropdown.js index fd081e7ef..47ee8ba8a 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -52,7 +52,7 @@ goog.require('goog.userAgent'); Blockly.FieldDropdown = function(menuGenerator, opt_validator) { this.menuGenerator_ = menuGenerator; this.trimOptions_(); - var firstTuple = this.getOptions_()[0]; + var firstTuple = this.getOptions()[0]; // Call parent's constructor. Blockly.FieldDropdown.superClass_.constructor.call(this, firstTuple[1], @@ -138,7 +138,7 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() { var menu = new goog.ui.Menu(); menu.setRightToLeft(this.sourceBlock_.RTL); - var options = this.getOptions_(); + var options = this.getOptions(); for (var i = 0; i < options.length; i++) { var content = options[i][0]; // Human-readable text or image. var value = options[i][1]; // Language-neutral value. @@ -230,7 +230,7 @@ Blockly.FieldDropdown.prototype.onItemSelected = function(menu, menuItem) { if (value !== null) { this.setValue(value); } -} +}; /** * Factor out common words in statically defined options. @@ -288,13 +288,19 @@ Blockly.FieldDropdown.prototype.trimOptions_ = function() { this.menuGenerator_ = newOptions; }; +/** + * @return {boolean} True if the option list is generated by a function. Otherwise false. + */ +Blockly.FieldDropdown.prototype.isOptionListDynamic = function() { + return goog.isFunction(this.menuGenerator_); +}; + /** * Return a list of the options for this dropdown. * @return {!Array.} Array of option tuples: * (human-readable text or image, language-neutral name). - * @private */ -Blockly.FieldDropdown.prototype.getOptions_ = function() { +Blockly.FieldDropdown.prototype.getOptions = function() { if (goog.isFunction(this.menuGenerator_)) { return this.menuGenerator_.call(this); } @@ -323,7 +329,7 @@ Blockly.FieldDropdown.prototype.setValue = function(newValue) { } this.value_ = newValue; // Look up and display the human-readable text. - var options = this.getOptions_(); + var options = this.getOptions(); for (var i = 0; i < options.length; i++) { // Options are tuples of human-readable text and language-neutral values. if (options[i][1] == newValue) { diff --git a/core/utils.js b/core/utils.js index 9ec03b84b..2bf60d014 100644 --- a/core/utils.js +++ b/core/utils.js @@ -84,15 +84,15 @@ Blockly.utils.removeClass = function(element, className) { /** * Checks if an element has the specified CSS class. * Similar to Closure's goog.dom.classes.has, except it handles SVG elements. - * @param {!Element} element DOM element to check. - * @param {string} className Name of class to check. - * @return {boolean} True if class exists, false otherwise. - * @private - */ - Blockly.utils.hasClass = function(element, className) { - var classes = element.getAttribute('class'); - return (' ' + classes + ' ').indexOf(' ' + className + ' ') != -1; - }; + * @param {!Element} element DOM element to check. + * @param {string} className Name of class to check. + * @return {boolean} True if class exists, false otherwise. + * @private + */ +Blockly.utils.hasClass = function(element, className) { + var classes = element.getAttribute('class'); + return (' ' + classes + ' ').indexOf(' ' + className + ' ') != -1; +}; /** * Don't do anything for this event, just halt propagation. @@ -144,7 +144,7 @@ Blockly.utils.getRelativeXY = function(element) { } } - // Then check for style = transform: translate(...) or translate3d(...) + // Then check for style = transform: translate(...) or translate3d(...) var style = element.getAttribute('style'); if (style && style.indexOf('translate') > -1) { var styleComponents = style.match(Blockly.utils.getRelativeXY.XY_2D_REGEX_); @@ -164,7 +164,7 @@ Blockly.utils.getRelativeXY = function(element) { /** * Return the coordinates of the top-left corner of this element relative to - * the div blockly was injected into. + * the div blockly was injected into. * @param {!Element} element SVG element to find the coordinates of. If this is * not a child of the div blockly was injected into, the behaviour is * undefined. @@ -185,7 +185,7 @@ Blockly.utils.getInjectionDivXY_ = function(element) { } element = element.parentNode; } - return new goog.math.Coordinate(x, y); + return new goog.math.Coordinate(x, y); }; /** @@ -195,9 +195,9 @@ Blockly.utils.getInjectionDivXY_ = function(element) { * @private */ Blockly.utils.getScale_ = function(element) { - var scale = 1; + var scale = 1; var transform = element.getAttribute('transform'); - if (transform) { + if (transform) { var transformComponents = transform.match(Blockly.utils.getScale_.REGEXP_); if (transformComponents && transformComponents[0]) { @@ -315,7 +315,7 @@ Blockly.utils.shortestStringLength = function(array) { if (!array.length) { return 0; } - return array.reduce(function (a, b) { + return array.reduce(function(a, b) { return a.length < b.length ? a : b; }).length; }; @@ -402,7 +402,7 @@ Blockly.utils.commonWordSuffix = function(array, opt_shortest) { */ Blockly.utils.tokenizeInterpolation = function(message) { return Blockly.utils.tokenizeInterpolation_(message, true); -} +}; /** * Replaces string table references in a message string. For example, @@ -416,7 +416,34 @@ Blockly.utils.replaceMessageReferences = function(message) { // When parseInterpolationTokens == false, interpolatedResult should be at // most length 1. return interpolatedResult.length ? interpolatedResult[0] : ""; -} +}; + +/** + * Validates that any %{BKY_...} references in the message refer to keys of + * the Blockly.Msg string table. + * @param {string} message Text which might contain string table references. + * @return {boolean} True if all message references have matching values. + * Otherwise, false. + */ +Blockly.utils.checkMessageReferences = function(message) { + var isValid = true; // True until a bad reference is found + + var regex = /%{BKY_([a-zA-Z][a-zA-Z0-9_]*)}/g; + var match = regex.exec(message); + while (match != null) { + var msgKey = match[1]; + if (Blockly.Msg[msgKey] == null) { + console.log('WARNING: No message string for %{BKY_' + msgKey + '}.'); + isValid = false; + } + + // Re-run on remainder of sting. + message = message.substring(match.index + msgKey.length + 1); + match = regex.exec(message); + } + + return isValid; +}; /** * Internal implemention of the message reference and interpolation token