Erweiterungen und Mutators

Erweiterungen sind Funktionen, die bei der Erstellung des Blocks für jeden Block eines bestimmten Typs ausgeführt werden. Durch sie wird einem Block häufig eine benutzerdefinierte Konfiguration oder ein benutzerdefiniertes Verhalten hinzugefügt.

Ein Mutator ist eine spezielle Erweiterung, die einem Block benutzerdefinierte Serialisierung und manchmal auch UI hinzufügt.

Erweiterungen

Erweiterungen sind Funktionen, die bei der Erstellung des Blocks für jeden Block eines bestimmten Typs ausgeführt werden. Er kann benutzerdefinierte Konfigurationen (z.B. Festlegen der Kurzinfo des Blocks) oder benutzerdefiniertes Verhalten (z.B. Hinzufügen eines Event-Listeners zum Block) hinzufügen.

// 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;
      });
    });

Erweiterungen müssen „registriert“ werden, damit sie einem Stringschlüssel zugeordnet werden können. Anschließend können Sie diesen Stringschlüssel dem Attribut extensions der JSON-Definition Ihres Blocktyps zuweisen, um die Erweiterung auf den Block anzuwenden.

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

Sie können auch mehrere Erweiterungen gleichzeitig hinzufügen. Die Eigenschaft extensions muss ein Array sein, auch wenn Sie nur eine Erweiterung anwenden.

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

Mixins

Blockly bietet auch eine praktische Methode für Situationen, in denen Sie einige Attribute/Hilfsfunktionen zu einem Block hinzufügen, aber nicht sofort ausführen möchten. Dazu können Sie ein mixin-Objekt registrieren, das alle zusätzlichen Attribute/Methoden enthält. Das Mixin-Objekt wird dann in eine Funktion eingeschlossen, die das Mixin jedes Mal anwendet, wenn eine Instanz des angegebenen Blocktyps erstellt wird.

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

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

Mit Mixins verknüpfte Stringschlüssel können in JSON wie jede andere Erweiterung referenziert werden.

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

Mutatoren

Ein Mutator ist ein spezieller Erweiterungstyp, mit dem einem Block eine zusätzliche Serialisierung (zusätzlicher Status, der gespeichert und geladen wird) hinzugefügt wird. Die integrierten Blöcke controls_if und list_create_with benötigen beispielsweise eine zusätzliche Serialisierung, damit sie die Anzahl ihrer Eingaben sparen können.

Beachten Sie, dass eine Änderung der Blockform nicht unbedingt bedeutet, dass Sie eine zusätzliche Serialisierung benötigen. Beispiel: Der math_number_property-Block ändert die Form, dies jedoch basierend auf einem Drop-down-Feld, dessen Wert bereits serialisiert ist. Daher kann einfach ein Feldvalidator verwendet werden und benötigt keinen Mutator.

Weitere Informationen dazu, wann Sie einen Mutator benötigen und wann nicht, finden Sie auf der Seite zur Serialisierung.

Mutators bieten Nutzern auch eine integrierte UI, über die sie die Form von Blöcken ändern können, wenn Sie einige optionale Methoden angeben.

Serialisierungs-Hooks

Mutators haben zwei Paare von Serialisierungs-Hooks, mit denen sie arbeiten. Ein Hook-Paar funktioniert mit dem neuen JSON-Serialisierungssystem und das andere mit dem alten XML-Serialisierungssystem. Sie müssen mindestens eines dieser Paare angeben.

„saveExtraState“ und „loadExtraState“

saveExtraState und loadExtraState sind Serialisierungs-Hooks, die mit dem neuen JSON-Serialisierungssystem funktionieren. saveExtraState gibt einen JSON-serialisierbaren Wert zurück, der den zusätzlichen Status des Blocks darstellt. loadExtraState akzeptiert denselben JSON-serialisierbaren Wert und wendet ihn auf den Block an.

// 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_();
},

Die resultierende JSON-Datei sieht so aus:

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

Wenn sich der Block bei der Serialisierung im Standardzustand befindet, kann die Methode saveExtraState zur Angabe null zurückgeben. Wenn die Methode saveExtraState null zurückgibt, wird der JSON-Datei kein extraState-Attribut hinzugefügt. So ist die gespeicherte Dateigröße klein.

Vollständige Serialisierung und Sicherung von Daten

saveExtraState empfängt außerdem einen optionalen doFullSerialization-Parameter. Dies wird von Blöcken verwendet, die auf Status verweisen, die von einem anderen Serializer (wie Sicherungsdatenmodelle) serialisiert wurden. Der Parameter signalisiert, dass der referenzierte Status nicht verfügbar ist, wenn der Block deserialisiert wird. Daher sollte der Block den gesamten Sicherungsstatus selbst serialisieren. Dies ist beispielsweise der Fall, wenn ein einzelner Block serialisiert oder ein Block kopiert und eingefügt wird.

Zwei häufige Anwendungsfälle sind:

  • Wenn ein einzelner Block in einen Arbeitsbereich geladen wird, in dem das unterstützende Datenmodell nicht vorhanden ist, verfügt er über genügend Informationen in seinem eigenen Zustand, um ein neues Datenmodell zu erstellen.
  • Wenn ein Block kopiert und eingefügt wird, wird immer ein neues unterstützendes Datenmodell erstellt, anstatt auf ein vorhandenes Modell zu verweisen.

Einige Blöcke, die diese Prozedur verwenden, sind die Blöcke @blockly/block-shareable-procedures. Normalerweise serialisieren sie einen Verweis auf ein unterstützendes Datenmodell, das ihren Status speichert. Wenn der Parameter doFullSerialization jedoch auf „true“ gesetzt ist, werden alle Zustände serialisiert. Die gemeinsam nutzbaren Prozedurblöcke nutzen dies, um sicherzustellen, dass beim Kopieren und Einfügen ein neues unterstützendes Datenmodell erstellt wird, anstatt auf ein vorhandenes Modell zu verweisen.

mutationToDom und domToMutation

mutationToDom und domToMutation sind Serialisierungs-Hooks, die mit dem alten XML-Serialisierungssystem funktionieren. Verwenden Sie diese Hooks nur bei Bedarf (z. B. wenn Sie mit einer alten Codebasis arbeiten, die noch nicht migriert wurde). Andernfalls verwenden Sie saveExtraState und loadExtraState.

mutationToDom gibt einen XML-Knoten zurück, der den zusätzlichen Status des Blocks darstellt, und domToMutation akzeptiert denselben XML-Knoten und wendet den Status auf den Block an.

// 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_();
},

Der resultierende XML-Code sieht in etwa so aus:

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

Wenn Ihre mutationToDom-Funktion null zurückgibt, wird der XML kein zusätzliches Element hinzugefügt.

UI-Hooks

Wenn Sie bestimmte Funktionen als Teil Ihres Mutators bereitstellen, fügt Blockly eine standardmäßige „Mutator“-UI zu Ihrem Block hinzu.

Sie müssen diese UI nicht verwenden, wenn Sie zusätzliche Serialisierung hinzufügen möchten. Sie können eine benutzerdefinierte UI wie das blocks-plus-minus-Plug-in verwenden oder auch gar keine UI verwenden.

compose und zersetzen

Die Standard-UI basiert auf den Funktionen compose und decompose.

decompose löst den Block in kleinere Unterblöcke auf, die verschoben, hinzugefügt und gelöscht werden können. Diese Funktion sollte einen „oberen Block“ zurückgeben. Dies ist der Hauptblock im Mutator-Arbeitsbereich, mit dem die Unterblöcke verbunden sind.

compose interpretiert dann die Konfiguration der Unterblöcke und verwendet sie, um den Hauptblock zu ändern. Diese Funktion sollte den „oberen Block“ akzeptieren, der von decompose als Parameter zurückgegeben wurde.

Beachten Sie, dass diese Funktionen in den zu „mutierten“ Block „gemischt“ werden, sodass this verwendet werden kann, um auf diesen Block zu verweisen.

// 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

Optional können Sie auch eine saveConnections-Funktion definieren, die mit der Standard-UI funktioniert. Mit dieser Funktion haben Sie die Möglichkeit, untergeordnete Blöcke des Hauptblocks, der im Hauptarbeitsbereich vorhanden ist, mit Unterblöcken in Ihrem Mutator-Arbeitsbereich zu verknüpfen. Anhand dieser Daten können Sie dann dafür sorgen, dass die Funktion compose die untergeordneten Blöcke des Hauptblocks wieder richtig verbindet, wenn die untergeordneten Blöcke neu organisiert werden.

saveConnections sollte den von der decompose-Funktion zurückgegebenen „oberen Block“ als Parameter akzeptieren. Wenn die Funktion saveConnections definiert ist, ruft Blockly sie auf, bevor compose aufgerufen wird.

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();
  }
},

Wird registriert

Mutators sind nur eine spezielle Erweiterung. Sie müssen also auch registriert werden, bevor Sie sie in der JSON-Definition Ihres Blocktyps verwenden können.

// 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: Ein String, der mit dem Mutator verknüpft wird, damit Sie ihn in JSON verwenden können.
  • mixinObj: Ein Objekt, das die verschiedenen Mutationsmethoden enthält. Beispiel: saveExtraState und loadExtraState.
  • opt_helperFn: Eine optionale Hilfsfunktion, die für den Block ausgeführt wird, nachdem das Mixin hinzugefügt wurde.
  • opt_blockList: Ein optionales Array von Blocktypen (als Strings), das dem Flyout in der Standard-Mutator-UI hinzugefügt wird, wenn die UI-Methoden definiert sind.

Im Gegensatz zu Erweiterungen kann jeder Blocktyp nur einen Mutator haben.

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

Hilfsfunktion

Zusammen mit dem Mixin kann ein Mutator eine Hilfsfunktion registrieren. Diese Funktion wird für jeden Block des angegebenen Typs ausgeführt, nachdem er erstellt und das mixinObj hinzugefügt wurde. Damit können einer Mutation zusätzliche Auslöser oder Effekte hinzugefügt werden.

Sie können dem listenähnlichen Block beispielsweise einen Hilfsprogramm hinzufügen, das die anfängliche Anzahl von Elementen festlegt:

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