Extensões e mutadores

Extensões são funções executadas em cada bloco de um determinado tipo à medida que o bloco é criado. Eles geralmente adicionam configuração ou comportamento personalizado a um bloco.

Um mutador é um tipo especial de extensão que adiciona serialização personalizada e, às vezes, interface do usuário a um bloco.

Extensões

Extensões são funções executadas em cada bloco de um determinado tipo à medida que o bloco é criado. Eles podem adicionar uma configuração personalizada (por exemplo, definir a dica do bloco) ou comportamento personalizado (por exemplo, adicionar um listener de eventos ao bloco).

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

As extensões precisam ser "registradas" para que possam ser associadas a uma chave de string. Em seguida, é possível atribuir essa chave de string à propriedade extensions da definição JSON do tipo de bloco para aplicar a extensão ao bloco.

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

Você também pode adicionar várias extensões de uma só vez. Observe que a propriedade extensions precisa ser uma matriz, mesmo que você esteja aplicando apenas uma extensão.

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

Mixes

O Blockly também oferece um método de conveniência para situações em que você quer adicionar algumas propriedades/funções auxiliares a um bloco, mas não executá-las imediatamente. Isso funciona, permitindo que você registre um objeto mixin que contém todas as propriedades/métodos extras. O objeto mixin é então envolvido em uma função que aplica o mixin sempre que uma instância do tipo de bloco especificado é criada.

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

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

As chaves de string associadas a mixins podem ser referenciadas em JSON, assim como qualquer outra extensão.

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

Mutadores

Um mutador é um tipo especial de extensão que adiciona serialização extra (estado extra que é salvo e carregado) a um bloco. Por exemplo, os blocos integrados controls_if e list_create_with precisam de serialização extra para que possam salvar quantas entradas eles têm.

Mudar a forma do bloco não necessariamente significa que você vai precisar de serialização extra. Por exemplo, o bloco math_number_property muda de forma, mas faz isso com base em um campo de menu suspenso, cujo valor já é serializado. Dessa forma, ele pode usar apenas um validador de campo e não precisa de um mutador.

Consulte a página de serialização para mais informações sobre quando e quando não precisa de um mutador.

Os mutadores também fornecem uma interface integrada para que os usuários mudem as formas dos blocos, caso você forneça alguns métodos opcionais.

Ganchos de serialização

Os mutadores trabalham com dois pares de ganchos de serialização. Um par de hooks funciona com o novo sistema de serialização JSON, e o outro funciona com o antigo sistema de serialização XML. É necessário informar pelo menos um desses pares.

saveExtraState e loadExtraState

saveExtraState e loadExtraState são hooks de serialização que funcionam com o novo sistema de serialização JSON. saveExtraState retorna um valor serializável JSON que representa o estado extra do bloco, e loadExtraState aceita esse mesmo valor serializável JSON e o aplica ao bloco.

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

O JSON resultante será semelhante a este:

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

Se o bloco estiver no estado padrão quando for serializado, o método saveExtraState poderá retornar null para indicar isso. Se o método saveExtraState retornar null, nenhuma propriedade extraState será adicionada ao JSON. Isso reduz o tamanho do arquivo.

Serialização completa e dados de backup

saveExtraState também recebe um parâmetro doFullSerialization opcional. Isso é usado por blocos que fazem referência ao estado serializado por um serializador diferente (como modelos de dados de apoio). O parâmetro sinaliza que o estado referenciado não estará disponível quando o bloco for desserializado, portanto, o bloco precisa serializar todo o próprio estado de apoio. Por exemplo, isso é verdadeiro quando um bloco individual é serializado ou quando um bloco é copiado e colado.

Dois casos de uso comuns para isso são:

  • Quando um bloco individual é carregado em um espaço de trabalho em que o modelo de dados de apoio não existe, ele tem informações suficientes no próprio estado para criar um novo modelo de dados.
  • Quando um bloco é copiado e colado, ele sempre cria um novo modelo de dados de apoio em vez de referenciar um existente.

Alguns blocos que usam isso são os blocos @blockly/block-shareable-procedures. Normalmente, eles serializam uma referência a um modelo de dados de apoio, que armazena o estado delas. No entanto, se o parâmetro doFullSerialization for verdadeiro, eles serializarão todo o estado. Os blocos de procedimentos compartilháveis usam isso para garantir que, quando forem copiados e colados, criem um novo modelo de dados de apoio, em vez de referenciar um modelo existente.

mutateToDom e domToMutation

mutationToDom e domToMutation são hooks de serialização que funcionam com o sistema de serialização XML antigo. Use esses hooks apenas se for necessário (por exemplo, se você estiver trabalhando em uma base de código antiga que ainda não foi migrada). Caso contrário, use saveExtraState e loadExtraState.

mutationToDom retorna um nó XML que representa o estado extra do bloco, e domToMutation aceita esse mesmo nó XML e aplica o estado ao bloco.

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

O XML resultante será semelhante a este:

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

Se a função mutationToDom retornar nulo, nenhum elemento extra será adicionado ao XML.

Ganchos de interface

Se você fornecer determinadas funções como parte do seu mutador, o Blockly adicionará uma interface de "mutador" padrão ao seu bloco.

Não é necessário usar essa interface se você quiser adicionar mais serialização. Você pode usar uma interface personalizada, como o plug-in blocks-plus-menos (link em inglês), ou você pode não usar nenhuma interface.

compor e decompor

A interface padrão depende das funções compose e decompose.

decompose "explode" o bloco em subblocos menores que podem ser movidos, adicionados e excluídos. Essa função precisa retornar um "bloco superior", que é o bloco principal no espaço de trabalho do mutador ao qual os sub-blocos se conectam.

Em seguida, compose interpreta a configuração dos sub-blocos e os usa para modificar o bloco principal. Essa função precisa aceitar o "bloco superior", que foi retornado por decompose como um parâmetro.

Observe que essas funções são "combinadas" com o bloco que está sendo modificado para que this possa ser usado para se referir a esse bloco.

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

Se quiser, também é possível definir uma função saveConnections que funcione com a interface padrão. Essa função oferece a chance de associar filhos do bloco principal (que existe no espaço de trabalho principal) com sub-blocos que existem no espaço de trabalho do mutador. Você pode usar esses dados para garantir que a função compose reconecte corretamente os filhos do bloco principal quando os sub-blocos forem reorganizados.

saveConnections precisa aceitar o "top block" retornado pela função decompose como um parâmetro. Se a função saveConnections estiver definida, o Blockly a chamará antes de 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();
  }
},

Registrando

Os mutadores são apenas um tipo especial de extensão. Portanto, eles também precisam ser registrados antes que você possa usá-los na definição JSON do tipo de bloco.

// 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: uma string a ser associada ao mutador para que você possa usá-lo em JSON.
  • mixinObj: um objeto que contém os vários métodos de mutação. Por exemplo, saveExtraState e loadExtraState.
  • opt_helperFn: uma função auxiliar opcional que será executada no bloco depois que o mixin for misturado.
  • opt_blockList: uma matriz opcional de tipos de bloco (como strings) que será adicionada ao menu suspenso na interface do mutador padrão, se os métodos da interface também forem definidos.

Ao contrário das extensões, cada tipo de bloqueio só pode ter um mutador.

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

Função auxiliar

Junto com o mixin, um mutador pode registrar uma função auxiliar. Essa função é executada em cada bloco do tipo especificado depois que ele é criado e o mixinObj é adicionado. Pode ser usado para adicionar outros gatilhos ou efeitos a uma mutação.

Por exemplo, você pode adicionar um auxiliar ao bloco semelhante a uma lista que define o número inicial de itens:

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