Mutadores

Um mutator é um mixin que adiciona serialização extra (estado extra salvo e carregado) a um bloco. Por exemplo, os blocos controls_if e list_create_with integrados precisam de serialização extra para salvar quantos inputs eles têm. Também é possível adicionar uma interface para que o usuário mude o formato do bloco.

Três mutações de um bloco de criação de lista: sem entradas, três entradas e cinco entradas.

Duas mutações de um bloco if/do: if-do e if-do-else-if-do-else.

Mudar o formato do bloco não significa necessariamente que você precisa de mais serialização. Por exemplo, o bloco math_number_property muda de forma, mas isso é feito com base em um campo suspenso, cujo valor já é serializado. Assim, ele pode usar apenas um validador de campo e não precisa de um mutator.

O bloco "math_number_property" com o menu suspenso definido como "even". Ele tem uma entrada de valor único. O bloco `math_number_property`
com o menu suspenso definido como "divisível por". Ele tem duas entradas de valor.

Consulte a página de serialização para saber quando você precisa de um mutator e quando não precisa.

Os mutadores também oferecem uma interface integrada para que os usuários mudem as formas dos blocos se você fornecer alguns métodos opcionais.

Hooks de serialização

Os mutadores têm dois pares de hooks de serialização com que trabalham. Um par de hooks funciona com o novo sistema de serialização JSON, e o outro par funciona com o antigo sistema de serialização XML. É necessário fornecer 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 em JSON que representa o estado extra do bloco, e loadExtraState aceita esse mesmo valor serializável em 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á assim:

{
  "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 mantém o tamanho do arquivo de salvamento pequeno.

Serialização completa e dados de apoio

saveExtraState também recebe um parâmetro doFullSerialization opcional. Isso é usado por blocos que referenciam estados serializados por um serializador diferente (como modelos de dados de apoio). O parâmetro indica que o estado referenciado não estará disponível quando o bloco for desserializado. Portanto, o bloco precisa serializar todo o estado de suporte. Por exemplo, isso acontece quando um bloco individual é serializado ou quando um bloco é copiado e colado.

Dois casos de uso comuns são:

  • Quando um bloco individual é carregado em um espaço de trabalho em que o modelo de dados de suporte 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 suporte 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 suporte, que armazena o estado deles. Mas se o parâmetro doFullSerialization for verdadeiro, eles vão serializar todo o estado. Os blocos de procedimentos compartilháveis usam isso para garantir que, quando forem copiados e colados, eles criem um novo modelo de dados de suporte, em vez de fazer referência a um modelo existente.

mutationToDom e domToMutation

mutationToDom e domToMutation são hooks de serialização que funcionam com o antigo sistema de serialização XML. Só use esses hooks 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 vai ficar assim:

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

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

Hooks de UI

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

Um bloco &quot;if-do&quot; com o balão mutator aberto. Isso permite que os usuários adicionem cláusulas else-if e else ao bloco if-do.

Não é necessário usar essa interface se você quiser adicionar mais serialização. Você pode usar uma interface personalizada, como a fornecida pelo plug-in blocks-plus-minus, ou não usar nenhuma interface.

compor e decompor

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

decompose "explode" o bloco em sub-blocos 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 mutator a que os sub-blocos se conectam.

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

Essas funções são "combinadas" ao bloco que está sendo "alterado". Assim, this pode 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

Também é possível definir uma função saveConnections que funcione com a interface padrão. Essa função permite associar filhos do seu bloco principal (que existe no espaço de trabalho principal) a sub-blocos que existem no espaço de trabalho do mutator. Em seguida, use esses dados para garantir que sua função compose reconecte corretamente os filhos do bloco principal quando os sub-blocos forem reorganizados.

saveConnections precisa aceitar o "bloco superior" retornado pela função decompose como um parâmetro. Se a função saveConnections estiver definida, o Blockly vai chamá-la antes de chamar 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 mixin. Portanto, eles também precisam ser registrados antes de serem usados 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 para associar ao mutator e usar 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 adicionado.
  • opt_blockList: uma matriz opcional de tipos de bloco (como strings) que serão adicionados ao submenu flutuante na interface padrão do mutator, se os métodos da interface também forem definidos.

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

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

Função auxiliar

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

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

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