Extensiones y mutadores

Las extensiones son funciones que se ejecutan en cada bloque de un tipo determinado a medida que se crea el bloque. Estos suelen agregar configuración o comportamiento personalizados a un bloque.

Un mutador es un tipo especial de extensión que agrega serialización personalizada y, a veces, IU, a un bloque.

Extensiones

Las extensiones son funciones que se ejecutan en cada bloque de un tipo determinado a medida que se crea el bloque. Pueden agregar una configuración personalizada (p.ej., configurar la información sobre la herramienta del bloque) o un comportamiento personalizado (p.ej., agregar un objeto de escucha de eventos al bloque).

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

Las extensiones se deben “registrar” para que se puedan asociar con una clave de string. Luego, puedes asignar esta clave de string a la propiedad extensions de la definición JSON de tu tipo de bloque para aplicar la extensión al bloque.

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

También puede agregar varias extensiones a la vez. Ten en cuenta que la propiedad extensions debe ser un array, incluso si solo estás aplicando una extensión.

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

Mixes

Blockly también proporciona un método de conveniencia para situaciones en las que deseas agregar algunas propiedades o funciones auxiliares a un bloque, pero no ejecutarlas inmediatamente. Esto te permite registrar un objeto mixin que contenga todas tus propiedades o métodos adicionales. El objeto mixin se une a una función que aplica la combinación cada vez que se crea una instancia del tipo de bloque determinado.

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

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

Las claves de string asociadas con mixins se pueden hacer referencia en JSON como cualquier otra extensión.

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

Mutadores

Un mutador es un tipo especial de extensión que agrega serialización adicional (estado adicional que se guarda y carga) a un bloque. Por ejemplo, los bloques integrados controls_if y list_create_with necesitan serialización adicional para que puedan guardar cuántas entradas tienen.

Ten en cuenta que cambiar la forma del bloque no necesariamente significa que necesites una serialización adicional. Por ejemplo, el bloque math_number_property cambia de forma, pero lo hace en función de un campo desplegable, cuyo valor ya se serializó. Por lo tanto, puede usar un validador de campo y no necesita un mutador.

Consulta la página de serialización para obtener más información sobre cuándo necesitas un mutador y cuándo no.

Los mutadores también proporcionan una IU integrada para que los usuarios cambien las formas de los bloques si proporcionas algunos métodos opcionales.

Hooks de serialización

Los mutadores tienen dos pares de hooks de serialización con los que trabajan. Un par de hooks funciona con el nuevo sistema de serialización JSON y el otro funciona con el sistema de serialización XML anterior. Debes proporcionar al menos uno de estos pares.

SaveExtraState y loadExtraState

saveExtraState y loadExtraState son hooks de serialización que funcionan con el nuevo sistema de serialización JSON. saveExtraState muestra un valor JSON serializable que representa el estado adicional del bloque, y loadExtraState acepta ese mismo valor serializable en JSON y lo aplica al bloque.

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

El JSON resultante se verá de la siguiente manera:

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

Si tu bloque está en su estado predeterminado cuando se serializa, tu método saveExtraState puede mostrar null para indicar esto. Si el método saveExtraState muestra null, no se agrega ninguna propiedad extraState al JSON. De esta manera, el tamaño del archivo guardado será pequeño.

Serialización completa y datos de copia de seguridad

saveExtraState también recibe un parámetro opcional doFullSerialization. Los bloques que hacen referencia al estado serializado por un serializador diferente (como modelos de datos de copia de seguridad) lo usan. El parámetro indica que el estado al que se hace referencia no estará disponible cuando se deserializa el bloque, por lo que el bloque debe serializar todo el estado de copia de seguridad. Por ejemplo, esto se aplica cuando se serializa un bloque individual o cuando se copia y pega un bloque.

Dos casos de uso comunes son los siguientes:

  • Cuando un bloque individual se carga en un lugar de trabajo en el que no existe el modelo de datos de copia de seguridad, tiene suficiente información en su propio estado para crear un modelo de datos nuevo.
  • Cuando se copia un bloque y se pega, siempre se crea un nuevo modelo de datos de copia de seguridad en lugar de hacer referencia a uno existente.

Algunos bloques que usan esto son los bloques @blockly/block-shareable-procedures. Por lo general, serializan una referencia a un modelo de datos de copia de seguridad, que almacena su estado. Sin embargo, si el parámetro doFullSerialization es verdadero, serializan todo su estado. Los bloques del procedimiento que se pueden compartir usan esto para garantizar que, cuando se copian y pegan, crean un nuevo modelo de datos de copia de seguridad, en lugar de hacer referencia a un modelo existente.

mutaToDom y domToMutation

mutationToDom y domToMutation son hooks de serialización que funcionan con el sistema de serialización XML anterior. Usa estos hooks solo si es necesario (p. ej., si trabajas en una base de código antigua que aún no se haya migrado). De lo contrario, usa saveExtraState y loadExtraState.

mutationToDom muestra un nodo XML que representa el estado adicional del bloque, y domToMutation acepta ese mismo nodo XML y aplica el estado al bloque.

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

El XML resultante se verá de la siguiente manera:

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

Si tu función mutationToDom muestra un valor nulo, no se agregará ningún elemento adicional al XML.

Hooks de IU

Si proporcionas ciertas funciones como parte de tu mutador, Blockly agregará una IU de "mutador" predeterminada a tu bloque.

No necesitas usar esta IU si quieres agregar serialización adicional. Puedes usar una IU personalizada, como la que proporciona el complemento blocks-plus-minus, o bien no utilizar ninguna IU en absoluto.

componer y descomponer

La IU predeterminada se basa en las funciones compose y decompose.

decompose "explode" el bloque en subbloques más pequeños que se pueden mover, agregar y borrar. Esta función debe mostrar un “bloque superior”, que es el bloque principal en el lugar de trabajo del mutador al que se conectan los subbloques.

Luego, compose interpreta la configuración de los subbloques y los usa para modificar el bloque principal. Esta función debe aceptar el "bloque superior" que mostró decompose como parámetro.

Ten en cuenta que estas funciones se “mezclan” en el bloque que se “muta”, por lo que se puede usar this para hacer referencia a ese bloque.

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

De manera opcional, también puedes definir una función saveConnections que funcione con la IU predeterminada. Esta función te permite asociar elementos secundarios del bloque principal (que existe en el lugar de trabajo principal) con subbloques que existen en el lugar de trabajo de mutador. Luego, puedes usar estos datos para asegurarte de que la función compose vuelva a conectar correctamente los elementos secundarios del bloque principal cuando se reorganicen los subbloques.

saveConnections debe aceptar el "bloque superior" que muestra tu función decompose como parámetro. Si se definió la función saveConnections, Blockly la llamará antes de llamar a 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();
  }
},

Registrándose

Los mutadores son solo un tipo especial de extensión, por lo que también deben registrarse antes de que puedas usarlos en la definición JSON del tipo de bloque.

// 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: Es una string para asociar con el mutador de modo que puedas usarlo en JSON.
  • mixinObj: Es un objeto que contiene los distintos métodos de mutación. P.ej., saveExtraState y loadExtraState.
  • opt_helperFn: Es una función auxiliar opcional que se ejecutará en el bloque después de que se mezcle la combinación.
  • opt_blockList: Es un array opcional de tipos de bloques (como cadenas) que se agregarán al menú flotante en la IU del mutador predeterminada si también se definieron los métodos de la IU.

Ten en cuenta que, a diferencia de las extensiones, cada tipo de bloque solo puede tener un mutador.

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

Función auxiliar

Junto con la mezcla, un mutador puede registrar una función auxiliar. Esta función se ejecuta en cada bloque del tipo específico después de que se crea y se agrega el mixinObj. Se puede usar para agregar activadores o efectos adicionales a una mutación.

Por ejemplo, puedes agregar un asistente a tu bloque similar a una lista que establezca el número inicial de elementos:

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