最新消息:

Blockly学习系列一:Blockly Mutators使用

Blockly 少儿编程 3923浏览 0评论

目录:

  1. mutationToDom
  2. domToMutation
  3. compose

  4. 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 的样式是这样的:

我的目标是做成这样:

                     

 

 

您必须 登录 才能发表评论!