ミューテータ

ミューテータは、ブロックに余分なシリアル化(保存および読み込みされる余分な状態)を追加するミックスインです。たとえば、組み込みの controls_if ブロックと list_create_with ブロックには、入力の数を保存できるようにするための追加のシリアル化が必要です。また、ユーザーがブロックの形状を変更できるように UI を追加することもあります。

リスト作成ブロックの 3 つのミューテーション(入力なし、3 つの入力、5 つの入力)。

if/do ブロックの 2 つのミューテーション: if-do と if-do-else-if-do-else。

ブロックの形状を変更しても、必ずしも追加のシリアル化が必要になるわけではありません。たとえば、math_number_property ブロックは形状を変更しますが、その変更はドロップダウン フィールドに基づいて行われます。このフィールドの値はすでにシリアル化されています。そのため、フィールド バリデータを使用するだけでよく、ミューテーターは必要ありません。

ドロップダウンが「偶数」に設定された `math_number_property` ブロック。値入力が 1 つあります。 `math_number_property` ブロック。ドロップダウンが `divisible by` に設定されています。2 つの値入力があります。

ミューテータが必要な場合と不要な場合について詳しくは、シリアル化のページをご覧ください。

また、ミューテーターは、オプションのメソッドを提供すると、ユーザーがブロックの形状を変更するための組み込み UI も提供します。

シリアル化フック

ミューテータには、連携するシリアル化フックが 2 組あります。フックの 1 つのペアは新しい JSON シリアル化システムで動作し、もう 1 つのペアは古い XML シリアル化システムで動作します。これらのペアを少なくとも 1 つ指定する必要があります。

saveExtraState と loadExtraState

saveExtraStateloadExtraState は、新しい JSON シリアル化システムで動作するシリアル化フックです。saveExtraState は、ブロックの追加の状態を表す JSON シリアル化可能な値を返します。loadExtraState は、同じ JSON シリアル化可能な値を受け取り、ブロックに適用します。

// These are the serialization hooks for the lists_create_with block.
saveExtraState: function() {
  return {
    'itemCount': this.itemCount_,
  };
},

loadExtraState: function(state) {
  this.itemCount_ = state['itemCount'];
  // This is a helper function which adds or removes inputs from the block.
  this.updateShape_();
},

結果の JSON は次のようになります。

{
  "type": "lists_create_with",
  "extraState": {
    "itemCount": 3 // or whatever the count is
  }
}

状態なし

シリアル化時にブロックがデフォルトの状態である場合、saveExtraState メソッドは null を返して、このことを示すことができます。saveExtraState メソッドが null を返した場合、JSON に extraState プロパティは追加されません。これにより、セーブファイルのサイズを小さく保つことができます。

完全なシリアル化とデータのバックアップ

saveExtraState は、省略可能な doFullSerialization パラメータも受け取ります。これは、別のシリアライザー(バッキング データモデルなど)によってシリアル化された状態を参照するブロックで使用されます。このパラメータは、ブロックが逆シリアル化されるときに参照される状態が使用できないことを示します。そのため、ブロックはすべてのバッキング状態を自身でシリアル化する必要があります。たとえば、個々のブロックがシリアル化された場合や、ブロックがコピー&ペーストされた場合などです。

一般的なユースケースは次のとおりです。

  • 個々のブロックが、バッキング データモデルが存在しないワークスペースに読み込まれると、新しいデータモデルを作成するのに十分な情報が独自の状態で保持されます。
  • ブロックをコピー&ペーストすると、既存のデータモデルを参照するのではなく、常に新しいデータモデルが作成されます。

これを使用するブロックには、@blockly/block-shareable-procedures ブロックなどがあります。通常、状態を保存するバッキング データモデルへの参照をシリアル化します。ただし、doFullSerialization パラメータが true の場合、すべての状態をシリアル化します。共有可能なプロシージャ ブロックは、コピー&ペーストされたときに既存のモデルを参照するのではなく、新しいバッキング データモデルを作成するためにこれを使用します。

mutationToDom と domToMutation

mutationToDomdomToMutation は、古い XML シリアル化システムで動作するシリアル化フックです。これらのフックは、やむを得ない場合(まだ移行されていない古いコードベースで作業している場合など)にのみ使用してください。それ以外の場合は、saveExtraStateloadExtraState を使用してください。

mutationToDom は、ブロックの追加の状態を表す XML ノードを返します。domToMutation は、同じ XML ノードを受け取り、ブロックに状態を適用します。

// These are the old XML serialization hooks for the lists_create_with block.
mutationToDom: function() {
  // You *must* create a <mutation></mutation> element.
  // This element can have children.
  var container = Blockly.utils.xml.createElement('mutation');
  container.setAttribute('items', this.itemCount_);
  return container;
},

domToMutation: function(xmlElement) {
  this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
  // This is a helper function which adds or removes inputs from the block.
  this.updateShape_();
},

結果の XML は次のようになります。

<block type="lists_create_with">
  <mutation items="3"></mutation>
</block>

mutationToDom 関数が null を返した場合、XML に追加の要素は追加されません。

UI フック

ミューテーターの一部として特定の関数を提供すると、Blockly はブロックにデフォルトの「ミューテーター」UI を追加します。

if-do ブロックと、開いているミューテーター バブル。これにより、ユーザーは if-do ブロックに else-if 句と else 句を追加できます。

追加のシリアル化を追加する場合は、この UI を使用する必要はありません。blocks-plus-minus プラグインが提供するようなカスタム UI を使用することも、UI をまったく使用しないこともできます。

compose と decompose

デフォルトの UI は compose 関数と decompose 関数に依存しています。

decompose は、ブロックを移動、追加、削除できる小さなサブブロックに「分解」します。この関数は、サブブロックが接続するミューテーター ワークスペースのメインブロックである「トップブロック」を返します。

compose はサブブロックの構成を解釈し、それらを使用してメインブロックを変更します。この関数は、decompose によって返された「トップブロック」をパラメータとして受け取る必要があります。

これらの関数は「変更」されるブロックに「ミックスイン」されるため、this を使用してそのブロックを参照できます。

// These are the decompose and compose functions for the lists_create_with block.
decompose: function(workspace) {
  // This is a special sub-block that only gets created in the mutator UI.
  // It acts as our "top block"
  var topBlock = workspace.newBlock('lists_create_with_container');
  topBlock.initSvg();

  // Then we add one sub-block for each item in the list.
  var connection = topBlock.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;
  }

  // And finally we have to return the top-block.
  return topBlock;
},

// The container block is the top-block returned by decompose.
compose: function(topBlock) {
  // First we get the first sub-block (which represents an input on our main block).
  var itemBlock = topBlock.getInputTargetBlock('STACK');

  // Then we collect up all of the connections of on our main block that are
  // referenced by our sub-blocks.
  // This relates to the saveConnections hook (explained below).
  var connections = [];
  while (itemBlock && !itemBlock.isInsertionMarker()) {  // Ignore insertion markers!
    connections.push(itemBlock.valueConnection_);
    itemBlock = itemBlock.nextConnection &&
        itemBlock.nextConnection.targetBlock();
  }

  // Then we disconnect any children where the sub-block associated with that
  // child has been deleted/removed from the stack.
  for (var i = 0; i < this.itemCount_; i++) {
    var connection = this.getInput('ADD' + i).connection.targetConnection;
    if (connection && connections.indexOf(connection) == -1) {
      connection.disconnect();
    }
  }

  // Then we update the shape of our block (removing or adding iputs as necessary).
  // `this` refers to the main block.
  this.itemCount_ = connections.length;
  this.updateShape_();

  // And finally we reconnect any child blocks.
  for (var i = 0; i < this.itemCount_; i++) {
    connections[i].reconnect(this, 'ADD' + i);
  }
},

saveConnections

必要に応じて、デフォルトの UI で動作する saveConnections 関数を定義することもできます。この関数を使用すると、メイン ワークスペースにあるメインブロックの子を、ミューテーター ワークスペースにあるサブブロックに関連付けることができます。このデータを使用して、サブブロックが再編成されたときに compose 関数がメインブロックの子を適切に再接続することを確認できます。

saveConnections は、decompose 関数から返された「トップブロック」をパラメータとして受け取る必要があります。saveConnections 関数が定義されている場合、Blockly は compose を呼び出す前にこの関数を呼び出します。

saveConnections: function(topBlock) {
  // First we get the first sub-block (which represents an input on our main block).
  var itemBlock = topBlock.getInputTargetBlock('STACK');

  // Then we go through and assign references to connections on our main block
  // (input.connection.targetConnection) to properties on our sub blocks
  // (itemBlock.valueConnection_).
  var i = 0;
  while (itemBlock) {
    // `this` refers to the main block (which is being "mutated").
    var input = this.getInput('ADD' + i);
    // This is the important line of this function!
    itemBlock.valueConnection_ = input && input.connection.targetConnection;
    i++;
    itemBlock = itemBlock.nextConnection &&
        itemBlock.nextConnection.targetBlock();
  }
},

登録中

ミューテータは特別な種類のミックスインであるため、ブロックタイプの JSON 定義で使用する前に登録する必要があります。

// Function signature.
Blockly.Extensions.registerMutator(name, mixinObj, opt_helperFn, opt_blockList);

// Example call.
Blockly.Extensions.registerMutator(
    'controls_if_mutator',
    { /* mutator methods */ },
    undefined,
    ['controls_if_elseif', 'controls_if_else']);
  • name: ミューテータに関連付ける文字列。JSON で使用できます。
  • mixinObj: さまざまなミューテーション メソッドを含むオブジェクト。例: saveExtraStateloadExtraState
  • opt_helperFn: ミックスインがミックスインされた後にブロックで実行される、オプションのヘルパー関数
  • opt_blockList: UI メソッドも定義されている場合、デフォルトのミューテーター UI のフライアウトに追加されるブロックタイプ(文字列)の配列(省略可)。

拡張機能とは異なり、各ブロックタイプには 1 つのミューテータのみを設定できます。

{
  //...
  "mutator": "controls_if_mutator"
}

ヘルパー関数

ミキシンとともに、ミューテータはヘルパー関数を登録できます。この関数は、指定された型の各ブロックが作成され、mixinObj が追加された後に実行されます。ミューテーションに追加のトリガーやエフェクトを追加するために使用できます。

たとえば、リストのようなブロックに、アイテムの初期数を設定するヘルパーを追加できます。

var helper = function() {
  this.itemCount_ = 5;
  this.updateShape();
}