ミューテータは、ブロックに余分なシリアル化(保存および読み込みされる余分な状態)を追加するミックスインです。たとえば、組み込みの controls_if
ブロックと list_create_with
ブロックには、入力の数を保存できるようにするための追加のシリアル化が必要です。また、ユーザーがブロックの形状を変更できるように UI を追加することもあります。
ブロックの形状を変更しても、必ずしも追加のシリアル化が必要になるわけではありません。たとえば、math_number_property
ブロックは形状を変更しますが、その変更はドロップダウン フィールドに基づいて行われます。このフィールドの値はすでにシリアル化されています。そのため、フィールド バリデータを使用するだけでよく、ミューテーターは必要ありません。
ミューテータが必要な場合と不要な場合について詳しくは、シリアル化のページをご覧ください。
また、ミューテーターは、オプションのメソッドを提供すると、ユーザーがブロックの形状を変更するための組み込み UI も提供します。
シリアル化フック
ミューテータには、連携するシリアル化フックが 2 組あります。フックの 1 つのペアは新しい JSON シリアル化システムで動作し、もう 1 つのペアは古い XML シリアル化システムで動作します。これらのペアを少なくとも 1 つ指定する必要があります。
saveExtraState と loadExtraState
saveExtraState
と loadExtraState
は、新しい 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
mutationToDom
と domToMutation
は、古い XML シリアル化システムで動作するシリアル化フックです。これらのフックは、やむを得ない場合(まだ移行されていない古いコードベースで作業している場合など)にのみ使用してください。それ以外の場合は、saveExtraState
と loadExtraState
を使用してください。
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-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
: さまざまなミューテーション メソッドを含むオブジェクト。例:saveExtraState
、loadExtraState
。opt_helperFn
: ミックスインがミックスインされた後にブロックで実行される、オプションのヘルパー関数。opt_blockList
: UI メソッドも定義されている場合、デフォルトのミューテーター UI のフライアウトに追加されるブロックタイプ(文字列)の配列(省略可)。
拡張機能とは異なり、各ブロックタイプには 1 つのミューテータのみを設定できます。
{
//...
"mutator": "controls_if_mutator"
}
ヘルパー関数
ミキシンとともに、ミューテータはヘルパー関数を登録できます。この関数は、指定された型の各ブロックが作成され、mixinObj
が追加された後に実行されます。ミューテーションに追加のトリガーやエフェクトを追加するために使用できます。
たとえば、リストのようなブロックに、アイテムの初期数を設定するヘルパーを追加できます。
var helper = function() {
this.itemCount_ = 5;
this.updateShape();
}