拡張機能とミューテータ

拡張機能は、ブロックの作成時に特定のタイプの各ブロックで実行される関数です。多くの場合、ブロックにカスタムの設定や動作が追加されます。

ミューテータはカスタムのシリアル化と、場合によっては UI をブロックに追加する特別な拡張機能です。

拡張機能

拡張機能は、ブロックの作成時に特定のタイプの各ブロックで実行される関数です。カスタム構成(ブロックのツールチップの設定など)やカスタム動作(ブロックへのイベント リスナーの追加など)を追加できます。

// This extension sets the block's tooltip to be a function which displays
// the parent block's tooltip (if it exists).
Blockly.Extensions.register(
    'parent_tooltip_extension',
    function() { // this refers to the block that the extension is being run on
      var thisBlock = this;
      this.setTooltip(function() {
        var parent = thisBlock.getParent();
        return (parent && parent.getInputsInline() && parent.tooltip) ||
            Blockly.Msg.MATH_NUMBER_TOOLTIP;
      });
    });

拡張機能を「登録」して、文字列キーに関連付ける必要があります。この文字列キーを、ブロックタイプの JSON 定義extensions プロパティに割り当て、拡張機能をブロックに適用できます。

{
 //...,
 "extensions": ["parent_tooltip_extension",]
}

複数の広告表示オプションをまとめて追加することもできます。1 つの拡張機能のみを適用する場合でも、extensions プロパティは配列にする必要があります。

{
  //...,
  "extensions": ["parent_tooltip_extension", "break_warning_extension"],
}

ミックスイン

Blockly は、いくつかのプロパティまたはヘルパー関数をブロックに追加したいけれどもすぐには実行しない場合に便利なメソッドも提供します。これは、すべての追加プロパティ/メソッドを含む mixin オブジェクトを登録することで機能します。ミックスイン オブジェクトは、指定されたブロックタイプのインスタンスが作成されるたびにミックスインを適用する関数にラップされます。

Blockly.Extensions.registerMixin('my_mixin', {
  someProperty: 'a cool value',

  someMethod: function() {
    // Do something cool!
  }
))`

ミックスインに関連付けられた文字列キーは、他の拡張機能と同様に JSON で参照できます。

{
 //...,
 "extensions": ["my_mixin"],
}

ミューテータ

ミューテータは特別なタイプの拡張機能で、ブロックにシリアル化(保存と読み込まれる追加の状態)を追加するものです。たとえば、組み込みの controls_if ブロックと list_create_with ブロックでは、入力の数を保存できるように追加のシリアル化が必要です。

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

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

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

シリアル化フック

ミューテータには、連動する 2 つのシリアル化フックのペアがあります。フックの一方のペアは新しい JSON シリアル化システムで動作し、もう一方のペアは以前の 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 を返す場合、extraState プロパティは JSON に追加されません。これにより、保存ファイルサイズを小さく保つことができます。

完全なシリアル化とバッキング データ

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

これには、次の 2 つの一般的なユースケースがあります。

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

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

mutTom と 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 をブロックに追加します。

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

Compose と Decompose

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

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

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

これらの関数は「変更される」ブロックに「混入」されるため、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 関数から返された「top block」をパラメータとして受け入れる必要があります。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();
}