Extensions et mutations

Les extensions sont des fonctions qui s'exécutent sur chaque bloc d'un type donné à mesure que le bloc est créé. Celles-ci ajoutent souvent une configuration ou un comportement personnalisés à un bloc.

Un mutateur est un type particulier d'extension qui ajoute une sérialisation personnalisée, et parfois une UI, à un bloc.

Extensions

Les extensions sont des fonctions qui s'exécutent sur chaque bloc d'un type donné à mesure que le bloc est créé. Ils peuvent ajouter une configuration personnalisée (par exemple, l'info-bulle du bloc) ou un comportement personnalisé (par exemple, ajouter un écouteur d'événements au bloc).

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

Les extensions doivent être "enregistrées" pour pouvoir être associées à une clé de chaîne. Vous pouvez ensuite attribuer cette clé de chaîne à la propriété extensions de la définition JSON de votre type de bloc pour appliquer l'extension au bloc.

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

Vous pouvez également ajouter plusieurs extensions à la fois. Notez que la propriété extensions doit être un tableau, même si vous n'appliquez qu'une seule extension.

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

Mix

Blockly fournit également une méthode pratique pour les cas où vous souhaitez ajouter des propriétés/fonctions d'aide à un bloc, mais pas les exécuter immédiatement. Cela vous permet d'enregistrer un objet mixin contenant l'ensemble de vos propriétés/méthodes supplémentaires. L'objet mixin est ensuite encapsulé dans une fonction qui applique le mixin chaque fois qu'une instance du type de bloc donné est créée.

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

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

Les clés de chaîne associées à des mixins peuvent être référencées en JSON comme n'importe quelle autre extension.

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

Mutateurs

Un mutateur est un type particulier d'extension qui ajoute une sérialisation supplémentaire (un état supplémentaire enregistré et chargé) à un bloc. Par exemple, les blocs intégrés controls_if et list_create_with nécessitent une sérialisation supplémentaire pour pouvoir enregistrer le nombre d'entrées dont ils disposent.

Notez que la modification de la forme de votre bloc ne signifie pas nécessairement que vous aurez besoin d'une sérialisation supplémentaire. Par exemple, le bloc math_number_property change de forme, mais il le fait en fonction d'un champ déroulant, dont la valeur est déjà sérialisée. Par conséquent, il peut simplement utiliser un valideur de champ et n'a pas besoin d'un mutateur.

Consultez la page de sérialisation pour savoir dans quels cas vous avez besoin d'un mutateur ou non.

Les mutateurs fournissent également une UI intégrée permettant aux utilisateurs de modifier la forme des blocs si vous fournissez des méthodes facultatives.

Hooks de sérialisation

Les mutateurs utilisent deux paires de hooks de sérialisation. Une paire de hooks fonctionne avec le nouveau système de sérialisation JSON, et l'autre paire fonctionne avec l'ancien système de sérialisation XML. Vous devez fournir au moins l'une de ces paires.

saveExtraState et loadExtraState

saveExtraState et loadExtraState sont des hooks de sérialisation qui fonctionnent avec le nouveau système de sérialisation JSON. saveExtraState renvoie une valeur JSON sérialisable qui représente l'état supplémentaire du bloc. loadExtraState accepte cette même valeur sérialisable JSON et l'applique au bloc.

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

Le fichier JSON ainsi obtenu se présentera comme suit:

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

Si votre bloc est dans son état par défaut lorsqu'il est sérialisé, votre méthode saveExtraState peut renvoyer null pour l'indiquer. Si votre méthode saveExtraState renvoie null, aucune propriété extraState n'est ajoutée au fichier JSON. Cela permet de réduire la taille de votre fichier d'enregistrement.

Sérialisation complète et sauvegarde des données

saveExtraState reçoit également un paramètre doFullSerialization facultatif. Elle est utilisée par les blocs qui font référence à un état sérialisé par un autre sérialiseur (comme des modèles de données de sauvegarde). Le paramètre indique que l'état référencé ne sera pas disponible lorsque le bloc est désérialisé. Le bloc doit donc sérialiser l'ensemble de l'état de sauvegarde lui-même. C'est le cas lorsqu'un bloc individuel est sérialisé ou lorsqu'un bloc est copié-collé.

Voici deux cas d'utilisation courants:

  • Lorsqu'un bloc individuel est chargé dans un espace de travail où le modèle de données de sauvegarde n'existe pas, il dispose de suffisamment d'informations dans son propre état pour créer un modèle de données.
  • Lorsqu'un bloc est copié-collé, il crée toujours un modèle de données de sauvegarde au lieu de référencer un modèle existant.

Certains blocs qui l'utilisent sont les blocs @blockly/block-shareable-procedures. Normalement, ils sérialisent une référence à un modèle de données de sauvegarde, qui stocke leur état. Toutefois, si le paramètre doFullSerialization est "true", ils sérialisent tous leurs états. Les blocs de procédure partageable utilisent cette méthode pour s'assurer que, lorsqu'ils sont copiés et collés, ils créent un nouveau modèle de données de sauvegarde au lieu de faire référence à un modèle existant.

mutationToDom et domToMutation

mutationToDom et domToMutation sont des hooks de sérialisation qui fonctionnent avec l'ancien système de sérialisation XML. N'utilisez ces hooks que si vous le devez (par exemple, si vous travaillez sur un ancien codebase qui n'a pas encore été migré), sinon utilisez saveExtraState et loadExtraState.

mutationToDom renvoie un nœud XML qui représente l'état supplémentaire du bloc, et domToMutation accepte ce même nœud XML et applique l'état au bloc.

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

Le code XML obtenu se présente comme suit:

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

Si votre fonction mutationToDom renvoie une valeur nulle, aucun élément supplémentaire ne sera ajouté au code XML.

Crochets d'interface utilisateur

Si vous fournissez certaines fonctions dans votre mutateur, Blockly ajoute une UI de "mutateur" par défaut à votre bloc.

Vous n'avez pas besoin d'utiliser cette interface utilisateur si vous souhaitez ajouter une sérialisation supplémentaire. Vous pouvez utiliser une interface utilisateur personnalisée, comme le fournit le plug-in blocks-plus-moins, ou n'utiliser aucune interface utilisateur.

composer et décomposer

L'interface utilisateur par défaut repose sur les fonctions compose et decompose.

decompose "explose" le bloc en sous-blocs plus petits qui peuvent être déplacés, ajoutés et supprimés. Cette fonction doit renvoyer un "bloc supérieur" qui est le bloc principal de l'espace de travail du mutateur auquel les sous-blocs se connectent.

compose interprète ensuite la configuration des sous-blocs et les utilise pour modifier le bloc principal. Cette fonction doit accepter le "bloc supérieur" qui a été renvoyé par decompose en tant que paramètre.

Notez que ces fonctions sont "mélangées" au bloc en cours de "mutation". this peut donc être utilisé pour faire référence à ce bloc.

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

Vous pouvez également définir une fonction saveConnections qui fonctionne avec l'interface utilisateur par défaut. Cette fonction vous permet d'associer les enfants de votre bloc principal (qui existe dans l'espace de travail principal) à des sous-blocs qui existent dans votre espace de travail de mutation. Vous pouvez ensuite utiliser ces données pour vous assurer que votre fonction compose reconnecte correctement les enfants de votre bloc principal lorsque vos sous-blocs sont réorganisés.

saveConnections doit accepter le "bloc supérieur" renvoyé par votre fonction decompose en tant que paramètre. Si la fonction saveConnections est définie, Blockly l'appellera avant 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();
  }
},

Enregistrement…

Les mutateurs ne sont qu'un type particulier d'extension. Ils doivent donc également être enregistrés avant que vous puissiez les utiliser dans la définition JSON de votre type de bloc.

// 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: chaîne à associer au mutateur afin de pouvoir l'utiliser en JSON.
  • mixinObj: objet contenant les différentes méthodes de mutation. Par exemple, saveExtraState et loadExtraState.
  • opt_helperFn: fonction d'assistance facultative qui s'exécutera sur le bloc une fois le mixin mélangé.
  • opt_blockList: tableau facultatif de types de blocs (sous forme de chaînes) qui sera ajouté au menu déroulant dans l'UI du mutateur par défaut, si les méthodes d'UI sont également définies.

Notez que contrairement aux extensions, chaque type de blocage ne peut avoir qu'un seul mutateur.

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

Fonction d'assistance

En plus du mixin, un mutateur peut enregistrer une fonction d'assistance. Cette fonction est exécutée sur chaque bloc du type donné après sa création et l'ajout du mixinObj. Elle permet d'ajouter des déclencheurs ou des effets supplémentaires à une mutation.

Par exemple, vous pouvez ajouter à votre bloc de type liste un assistant qui définit le nombre initial d'éléments:

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