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的额外输入:
DIVISIBLE_BY
在该 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 的样式是这样的:
我的目标是做成这样: