码丁实验室,一站式儿童编程学习产品,寻地方代理合作共赢,微信联系:leon121393608。
目录:
- mutationToDom
- domToMutation
-
compose
-
decompose
https://developers.google.com/Blockly/guides/create-custom-blocks/mutators
Advanced blocks may use a mutator to be even more dynamic and configurable. Mutators allow blocks to change in custom ways, beyond dropdown and text input fields.
Saving mutation data is done by adding a mutationToDom function in the block’s definition.
The inverse function is domToMutation which is called whenever a block is being restored from XML.
摘要:mutator 这里翻译为 存储器
高级的blocks允许使用 mutator 来完成一些更加动态的灵活的配置。mutator 允许模块以定制的方式(custom way)来改变blocks,除了下拉菜单(dropdown)和文本输入(text input)的方式之外。最常见的例子是 弹出对话框, 允许 if 语句(statement)获得额外的 else if 和 else 语句。
1、mutationToDom 和 domToMutation
The XML format used to load, save, copy, and paste blocks automatically captures and restores all data stored in editable fields. 然而,如果blocks包含额外的附加的信息,当blocks被保存或者重新加载的时候将会丢失信息。每一个block的XML里的所有信息数据都可以保存在可选择的mutator元素里(Each block’s XML has an optional mutator element where arbitrary data may be stored)。
一个简单的例子 math.js里的 math_number_property block, 默认情况下他有一个输入:

如果下拉是“整除”改成,出现第二个输入:

这使用dropdown menu很容易的完成这个变化的block、问题是,当从XML创建该block(as occurs when displayed in the toolbox, cloned from the toolbox, copied and pasted, duplicated, or loaded from a saved file)时,init 函数将会创建block的默认样式。这将导致一个错误,如果XML指定一些其他 block 完成连接到一个不存在的输入。
简单的解决这个问题,涉及到 使用mutator来记录这个block的额外输入:
<block type="math_number_property"> <b><mutation divisor_input="true"></mutation></b> <field name="PROPERTY">DIVISIBLE_BY</field> </block>
在该 block 的定义中使用 mutationToDom 函数来保存 mutation 数据:
mutationToDom: function() { var container = document.createElement('mutation'); var divisorInput = (this.getFieldValue('PROPERTY') == 'DIVISIBLE_BY'); container.setAttribute('divisor_input', divisorInput); return container; }
每当一个 block 被写入 XML 时候这个函数 mutationToDom 就会被调用。如果这个函数不存在或者返回为空null,则没有 mutation 被记录。如果这个函数存在而且返回一个 ‘mutation’ XML 元素,则该元素(和任何属性或者子元素)将被存储在该block的XML表达式里。
与mutationToDom函数相对应的函数是 domToMutation,每当一个block 被从XML 恢复时调用:
domToMutation: function(xmlElement) { var hasDivisorInput = (xmlElement.getAttribute('divisor_input') == 'true'); this.updateShape_(hasDivisorInput); // Helper function for adding/removing 2nd input. }
如果该函数domToMutation存在,it is passed the block’s ‘mutation’ XML element.该函数可以解析元素和重新配置基于元素的属性和子元素的block。
math.js里的 math_number_property block源代码如下:
1 Blockly.Blocks['math_number_property'] = { 2 /** 3 * Block for checking if a number is even, odd, prime, whole, positive, 4 * negative or if it is divisible by certain number. 5 * @this Blockly.Block 6 */ 7 init: function() { 8 var PROPERTIES = 9 [[Blockly.Msg.MATH_IS_EVEN, 'EVEN'], 10 [Blockly.Msg.MATH_IS_ODD, 'ODD'], 11 [Blockly.Msg.MATH_IS_PRIME, 'PRIME'], 12 [Blockly.Msg.MATH_IS_WHOLE, 'WHOLE'], 13 [Blockly.Msg.MATH_IS_POSITIVE, 'POSITIVE'], 14 [Blockly.Msg.MATH_IS_NEGATIVE, 'NEGATIVE'], 15 [Blockly.Msg.MATH_IS_DIVISIBLE_BY, 'DIVISIBLE_BY']]; 16 this.setColour(Blockly.Blocks.math.HUE); 17 this.appendValueInput('NUMBER_TO_CHECK') 18 .setCheck('Number'); 19 var dropdown = new Blockly.FieldDropdown(PROPERTIES, function(option) { 20 var divisorInput = (option == 'DIVISIBLE_BY'); 21 this.sourceBlock_.updateShape_(divisorInput); 22 }); 23 this.appendDummyInput() 24 .appendField(dropdown, 'PROPERTY'); 25 this.setInputsInline(true); 26 this.setOutput(true, 'Boolean'); 27 this.setTooltip(Blockly.Msg.MATH_IS_TOOLTIP); 28 }, 29 /** 30 * Create XML to represent whether the 'divisorInput' should be present. 31 * @return {Element} XML storage element. 32 * @this Blockly.Block 33 */ 34 mutationToDom: function() { 35 var container = document.createElement('mutation'); 36 var divisorInput = (this.getFieldValue('PROPERTY') == 'DIVISIBLE_BY'); 37 container.setAttribute('divisor_input', divisorInput); 38 return container; 39 }, 40 /** 41 * Parse XML to restore the 'divisorInput'. 42 * @param {!Element} xmlElement XML storage element. 43 * @this Blockly.Block 44 */ 45 domToMutation: function(xmlElement) { 46 var divisorInput = (xmlElement.getAttribute('divisor_input') == 'true'); 47 this.updateShape_(divisorInput); 48 }, 49 /** 50 * Modify this block to have (or not have) an input for 'is divisible by'. 51 * @param {boolean} divisorInput True if this block has a divisor input. 52 * @private 53 * @this Blockly.Block 54 */ 55 updateShape_: function(divisorInput) { 56 // Add or remove a Value Input. 57 var inputExists = this.getInput('DIVISOR'); 58 if (divisorInput) { 59 if (!inputExists) { 60 this.appendValueInput('DIVISOR') 61 .setCheck('Number'); 62 } 63 } else if (inputExists) { 64 this.removeInput('DIVISOR'); 65 } 66 } 67 }; 68 69 Blockly.JavaScript['math_number_property'] = function(block) { 70 // Check if a number is even, odd, prime, whole, positive, or negative 71 // or if it is divisible by certain number. Returns true or false. 72 var number_to_check = Blockly.JavaScript.valueToCode(block, 'NUMBER_TO_CHECK', 73 Blockly.JavaScript.ORDER_MODULUS) || '0'; 74 var dropdown_property = block.getFieldValue('PROPERTY'); 75 var code; 76 if (dropdown_property == 'PRIME') { 77 // Prime is a special case as it is not a one-liner test. 78 var functionName = Blockly.JavaScript.provideFunction_( 79 'mathIsPrime', 80 ['function ' + Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_ + '(n) {', 81 ' // https://en.wikipedia.org/wiki/Primality_test#Naive_methods', 82 ' if (n == 2 || n == 3) {', 83 ' return true;', 84 ' }', 85 ' // False if n is NaN, negative, is 1, or not whole.', 86 ' // And false if n is divisible by 2 or 3.', 87 ' if (isNaN(n) || n <= 1 || n % 1 != 0 || n % 2 == 0 ||' + 88 ' n % 3 == 0) {', 89 ' return false;', 90 ' }', 91 ' // Check all the numbers of form 6k +/- 1, up to sqrt(n).', 92 ' for (var x = 6; x <= Math.sqrt(n) + 1; x += 6) {', 93 ' if (n % (x - 1) == 0 || n % (x + 1) == 0) {', 94 ' return false;', 95 ' }', 96 ' }', 97 ' return true;', 98 '}']); 99 code = functionName + '(' + number_to_check + ')'; 100 return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; 101 } 102 switch (dropdown_property) { 103 case 'EVEN': 104 code = number_to_check + ' % 2 == 0'; 105 break; 106 case 'ODD': 107 code = number_to_check + ' % 2 == 1'; 108 break; 109 case 'WHOLE': 110 code = number_to_check + ' % 1 == 0'; 111 break; 112 case 'POSITIVE': 113 code = number_to_check + ' > 0'; 114 break; 115 case 'NEGATIVE': 116 code = number_to_check + ' < 0'; 117 break; 118 case 'DIVISIBLE_BY': 119 var divisor = Blockly.JavaScript.valueToCode(block, 'DIVISOR', 120 Blockly.JavaScript.ORDER_MODULUS) || '0'; 121 code = number_to_check + ' % ' + divisor + ' == 0'; 122 break; 123 } 124 return [code, Blockly.JavaScript.ORDER_EQUALITY]; 125 };
2、compose 和 decompose
mutation 对话框 允许 用户 去扩展或重新配置 一个 block 到更小的 子block,从而改变原始 block 的形状。对话框按钮被增加到 一个block 的 init 功能里:
this.setMutator(new Blockly.Mutator(['controls_if_elseif', 'controls_if_else']));

setMutator 函数有一个参数,一个新的 Mutator。Mutator 构造函数有一个参数,子block(或叫从属的block)的列表在 toolbox 中显示。在这个时候不建议创造一个mutator的子block嵌套mutator。
当一个mutator对话框打开,block 的 decompose 函数被调用 来改变mutator 的worksapce:
decompose: function(workspace) {
var topBlock = Blockly.Block.obtain(workspace, 'controls_if_if');
topBlock.initSvg();
...
return topBlock;
}
At a minimum this function must create and initialize a top-level block for the mutator dialog, and return it. This function should also populate this top-level block with any sub-blocks which are appropriate.
至少这个功能必须创建并初始化用于增变对话框顶层块,并返回它。这个功能也应该与任何子块,适合填充此顶级块。

When a mutator dialog saves its content, the block's compose function is called to modify the original block according to the new settings.
当一个mutator对话框保存其内容,block的 compose 函数被调用,并按照新的设置修改原来的block。
compose: function(topBlock) {
...
}
This function is passed the top-level block from the mutator's workspace (the same block that was created and returned by the compose function). Typically this function would spider the sub-blocks attached to the top-level block, then update the original block accordingly.
这个函数是通过从 mutator 的工作空间(即创建,并由返回的相同块中的顶级块compose功能)。通常这个函数将蜘蛛附连到顶层块中的子块,则相应地更新原始块。

Ideally this function would ensure that any blocks already connected to the original block should remain connected to the correct inputs, even if the inputs are reordered.
理想情况下,此函数将确保已连接到原始块的任何块都应保持连接到正确的输入,即使输入重新排序。
1 Blockly.Blocks['controls_if'] = { 2 /** 3 * Block for if/elseif/else condition. 4 * @this Blockly.Block 5 */ 6 init: function() { 7 this.setHelpUrl(Blockly.Msg.CONTROLS_IF_HELPURL); 8 this.setColour(Blockly.Blocks.logic.HUE); 9 this.appendValueInput('IF0') 10 .setCheck('Boolean') 11 .appendField(Blockly.Msg.CONTROLS_IF_MSG_IF); 12 this.appendStatementInput('DO0') 13 .appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN); 14 this.setPreviousStatement(true); 15 this.setNextStatement(true); 16 this.setMutator(new Blockly.Mutator(['controls_if_elseif', 17 'controls_if_else'])); 18 // Assign 'this' to a variable for use in the tooltip closure below. 19 var thisBlock = this; 20 this.setTooltip(function() { 21 if (!thisBlock.elseifCount_ && !thisBlock.elseCount_) { 22 return Blockly.Msg.CONTROLS_IF_TOOLTIP_1; 23 } else if (!thisBlock.elseifCount_ && thisBlock.elseCount_) { 24 return Blockly.Msg.CONTROLS_IF_TOOLTIP_2; 25 } else if (thisBlock.elseifCount_ && !thisBlock.elseCount_) { 26 return Blockly.Msg.CONTROLS_IF_TOOLTIP_3; 27 } else if (thisBlock.elseifCount_ && thisBlock.elseCount_) { 28 return Blockly.Msg.CONTROLS_IF_TOOLTIP_4; 29 } 30 return ''; 31 }); 32 this.elseifCount_ = 0; 33 this.elseCount_ = 0; 34 }, 35 /** 36 * Create XML to represent the number of else-if and else inputs. 37 * @return {Element} XML storage element. 38 * @this Blockly.Block 39 */ 40 mutationToDom: function() { 41 if (!this.elseifCount_ && !this.elseCount_) { 42 return null; 43 } 44 var container = document.createElement('mutation'); 45 if (this.elseifCount_) { 46 container.setAttribute('elseif', this.elseifCount_); 47 } 48 if (this.elseCount_) { 49 container.setAttribute('else', 1); 50 } 51 return container; 52 }, 53 /** 54 * Parse XML to restore the else-if and else inputs. 55 * @param {!Element} xmlElement XML storage element. 56 * @this Blockly.Block 57 */ 58 domToMutation: function(xmlElement) { 59 this.elseifCount_ = parseInt(xmlElement.getAttribute('elseif'), 10) || 0; 60 this.elseCount_ = parseInt(xmlElement.getAttribute('else'), 10) || 0; 61 this.updateShape_(); 62 }, 63 /** 64 * Populate the mutator's dialog with this block's components. 65 * @param {!Blockly.Workspace} workspace Mutator's workspace. 66 * @return {!Blockly.Block} Root block in mutator. 67 * @this Blockly.Block 68 */ 69 decompose: function(workspace) { 70 var containerBlock = workspace.newBlock('controls_if_if'); 71 containerBlock.initSvg(); 72 var connection = containerBlock.nextConnection; 73 for (var i = 1; i <= this.elseifCount_; i++) { 74 var elseifBlock = workspace.newBlock('controls_if_elseif'); 75 elseifBlock.initSvg(); 76 connection.connect(elseifBlock.previousConnection); 77 connection = elseifBlock.nextConnection; 78 } 79 if (this.elseCount_) { 80 var elseBlock = workspace.newBlock('controls_if_else'); 81 elseBlock.initSvg(); 82 connection.connect(elseBlock.previousConnection); 83 } 84 return containerBlock; 85 }, 86 /** 87 * Reconfigure this block based on the mutator dialog's components. 88 * @param {!Blockly.Block} containerBlock Root block in mutator. 89 * @this Blockly.Block 90 */ 91 compose: function(containerBlock) { 92 var clauseBlock = containerBlock.nextConnection.targetBlock(); 93 // Count number of inputs. 94 this.elseifCount_ = 0; 95 this.elseCount_ = 0; 96 var valueConnections = [null]; 97 var statementConnections = [null]; 98 var elseStatementConnection = null; 99 while (clauseBlock) { 100 switch (clauseBlock.type) { 101 case 'controls_if_elseif': 102 this.elseifCount_++; 103 valueConnections.push(clauseBlock.valueConnection_); 104 statementConnections.push(clauseBlock.statementConnection_); 105 break; 106 case 'controls_if_else': 107 this.elseCount_++; 108 elseStatementConnection = clauseBlock.statementConnection_; 109 break; 110 default: 111 throw 'Unknown block type.'; 112 } 113 clauseBlock = clauseBlock.nextConnection && 114 clauseBlock.nextConnection.targetBlock(); 115 } 116 this.updateShape_(); 117 // Reconnect any child blocks. 118 for (var i = 1; i <= this.elseifCount_; i++) { 119 Blockly.Mutator.reconnect(valueConnections[i], this, 'IF' + i); 120 Blockly.Mutator.reconnect(statementConnections[i], this, 'DO' + i); 121 } 122 Blockly.Mutator.reconnect(elseStatementConnection, this, 'ELSE'); 123 }, 124 /** 125 * Store pointers to any connected child blocks. 126 * @param {!Blockly.Block} containerBlock Root block in mutator. 127 * @this Blockly.Block 128 */ 129 saveConnections: function(containerBlock) { 130 var clauseBlock = containerBlock.nextConnection.targetBlock(); 131 var i = 1; 132 while (clauseBlock) { 133 switch (clauseBlock.type) { 134 case 'controls_if_elseif': 135 var inputIf = this.getInput('IF' + i); 136 var inputDo = this.getInput('DO' + i); 137 clauseBlock.valueConnection_ = 138 inputIf && inputIf.connection.targetConnection; 139 clauseBlock.statementConnection_ = 140 inputDo && inputDo.connection.targetConnection; 141 i++; 142 break; 143 case 'controls_if_else': 144 var inputDo = this.getInput('ELSE'); 145 clauseBlock.statementConnection_ = 146 inputDo && inputDo.connection.targetConnection; 147 break; 148 default: 149 throw 'Unknown block type.'; 150 } 151 clauseBlock = clauseBlock.nextConnection && 152 clauseBlock.nextConnection.targetBlock(); 153 } 154 }, 155 /** 156 * Modify this block to have the correct number of inputs. 157 * @private 158 * @this Blockly.Block 159 */ 160 updateShape_: function() { 161 // Delete everything. 162 if (this.getInput('ELSE')) { 163 this.removeInput('ELSE'); 164 } 165 var i = 1; 166 while (this.getInput('IF' + i)) { 167 this.removeInput('IF' + i); 168 this.removeInput('DO' + i); 169 i++; 170 } 171 // Rebuild block. 172 for (var i = 1; i <= this.elseifCount_; i++) { 173 this.appendValueInput('IF' + i) 174 .setCheck('Boolean') 175 .appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSEIF); 176 this.appendStatementInput('DO' + i) 177 .appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN); 178 } 179 if (this.elseCount_) { 180 this.appendStatementInput('ELSE') 181 .appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSE); 182 } 183 } 184 }; 185 186 Blockly.JavaScript['controls_if'] = function(block) { 187 // If/elseif/else condition. 188 var n = 0; 189 var code = '', branchCode, conditionCode; 190 do { 191 conditionCode = Blockly.JavaScript.valueToCode(block, 'IF' + n, 192 Blockly.JavaScript.ORDER_NONE) || 'false'; 193 branchCode = Blockly.JavaScript.statementToCode(block, 'DO' + n); 194 code += (n > 0 ? ' else ' : '') + 195 'if (' + conditionCode + ') {\n' + branchCode + '}'; 196 197 ++n; 198 } while (block.getInput('IF' + n)); 199 200 if (block.getInput('ELSE')) { 201 branchCode = Blockly.JavaScript.statementToCode(block, 'ELSE'); 202 code += ' else {\n' + branchCode + '}'; 203 } 204 return code + '\n'; 205 };
以上是 Google 开发者官网的 blockly 文档,以下是自己的学习笔记记录:
Blockly.Blocks['js_function_expression'] = { /** * Block for redering a function expression. * @this Blockly.Block */ init: function() { this.setColour(290); this.appendDummyInput() .appendField("function"); this.appendValueInput('NAME'); this.appendValueInput('PARAM0') .appendField("("); this.appendDummyInput('END') .appendField(")"); this.appendStatementInput('STACK'); this.setInputsInline(true); this.setTooltip('Function expression.'); this.setOutput(true); } };
Blockly.JavaScript['js_function_expression'] = function(block) { var branch = Blockly.JavaScript.statementToCode(block, 'STACK'); var name = Blockly.JavaScript.valueToCode(block, 'NAME', Blockly.JavaScript.ORDER_ATOMIC); var args = []; for (var i = 0; i < block.paramCount; i++) { args[i] = Blockly.JavaScript.valueToCode(block, 'PARAM' + i, Blockly.JavaScript.ORDER_ATOMIC); } var code = 'yak.' + name + '=' + 'function ' + '(' + args.join(', ') + ') {\n' + branch + '}'; if (block.outputConnection) { return [code, Blockly.JavaScript.ORDER_ATOMIC]; } else { return code + ';\n'; } };
我的目前 Block 的样式是这样的:

我的目标是做成这样:



