Mutatori

Un mutatore è un mixin che aggiunge una serializzazione aggiuntiva (uno stato aggiuntivo che viene salvato e caricato) a un blocco. Ad esempio, i blocchi controls_if e list_create_with integrati richiedono una serializzazione aggiuntiva per poter salvare il numero di input. Potrebbe anche aggiungere un'interfaccia utente in modo che l'utente possa modificare la forma del blocco.

Tre mutazioni di un blocco di creazione di elenchi: nessun input, tre input e cinque input.

Due mutazioni di un blocco if/do: if-do e if-do-else-if-do-else.

Tieni presente che la modifica della forma del blocco non significa necessariamente che devi aggiungere una serializzazione. Ad esempio, il blocco math_number_property cambia forma, ma lo fa in base a un campo a discesa, il cui valore viene già serializzato. Pertanto, può utilizzare solo un validator di campo e non ha bisogno di un mutatore.

Il blocco `math_number_property` con il menu a discesa impostato su `even`. Ha un
unico input di valore. Il blocco `math_number_property`
con il menu a discesa impostato su `divisible by`. Ha due input
di valore.

Per ulteriori informazioni su quando è necessario un mutatore e quando non lo è, consulta la pagina sulla serializzazione.

I mutatori forniscono anche un'interfaccia utente integrata per consentire agli utenti di modificare le forme dei blocchi se fornisci alcuni metodi facoltativi.

Hook di serializzazione

I mutatori hanno due coppie di hook di serializzazione con cui lavorano. Una coppia di hook funziona con il nuovo sistema di serializzazione JSON, mentre l'altra coppia funziona con il vecchio sistema di serializzazione XML. Devi fornire almeno una di queste coppie.

saveExtraState e loadExtraState

saveExtraState e loadExtraState sono hook di serializzazione che funzionano con il nuovo sistema di serializzazione JSON. saveExtraState restituisce un valore serializzabile JSON che rappresenta lo stato aggiuntivo del blocco, mentre loadExtraState accetta lo stesso valore serializzabile JSON e lo applica al blocco.

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

Il JSON risultante sarà simile a questo:

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

Nessuno stato

Se il blocco è nel suo stato predefinito quando viene serializzato, il metodo saveExtraState può restituire null per indicarlo. Se il tuo metodo saveExtraState restituisce null, non viene aggiunta alcuna proprietà extraState al JSON. In questo modo, le dimensioni del file di salvataggio rimangono ridotte.

Serializzazione completa e dati di backup

saveExtraState riceve anche un parametro doFullSerialization facoltativo. Questo viene utilizzato dai blocchi che fanno riferimento allo stato serializzato da un serializzatore diverso (come i modelli di dati di supporto). Il parametro indica che lo stato a cui viene fatto riferimento non sarà disponibile quando il blocco viene deserializzato, quindi il blocco deve serializzare tutto lo stato di backing. Ad esempio, questo è vero quando un blocco individuale viene serializzato o quando un blocco viene copiato e incollato.

Due casi d'uso comuni sono:

  • Quando un singolo blocco viene caricato in uno spazio di lavoro in cui non esiste il modello di dati sottostante, contiene informazioni sufficienti nel proprio stato per creare un nuovo modello di dati.
  • Quando un blocco viene copiato e incollato, viene sempre creato un nuovo modello di dati di supporto anziché fare riferimento a uno esistente.

Alcuni blocchi che lo utilizzano sono i blocchi @blockly/block-shareable-procedures. Normalmente serializzano un riferimento a un modello di dati di supporto, che memorizza il loro stato. Tuttavia, se il parametro doFullSerialization è true, serializzano tutto il loro stato. I blocchi di procedure condivisibili lo utilizzano per assicurarsi che, quando vengono copiati e incollati, creino un nuovo modello di dati di supporto anziché fare riferimento a un modello esistente.

mutationToDom e domToMutation

mutationToDom e domToMutation sono hook di serializzazione che funzionano con il vecchio sistema di serializzazione XML. Utilizza questi hook solo se necessario (ad es. se stai lavorando su una base di codice precedente che non è ancora stata migrata), altrimenti utilizza saveExtraState e loadExtraState.

mutationToDom restituisce un nodo XML che rappresenta lo stato aggiuntivo del blocco, mentre domToMutation accetta lo stesso nodo XML e applica lo stato al blocco.

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

L'XML risultante avrà il seguente aspetto:

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

Se la funzione mutationToDom restituisce null, non verrà aggiunto alcun elemento extra all'XML.

UI Hooks

Se fornisci determinate funzioni come parte del mutatore, Blockly aggiungerà un'interfaccia utente "mutatore" predefinita al blocco.

Un blocco if-do con la bolla del mutatore aperta. Ciò consente agli utenti di aggiungere clausole else-if ed else al blocco if-do.

Non devi utilizzare questa UI se vuoi aggiungere una serializzazione aggiuntiva. Puoi utilizzare un'interfaccia utente personalizzata, come quella fornita dal plug-in blocks-plus-minus, oppure non utilizzare alcuna interfaccia utente.

comporre e scomporre

La UI predefinita si basa sulle funzioni compose e decompose.

decompose "esplode" il blocco in sottoblocchi più piccoli che possono essere spostati, aggiunti ed eliminati. Questa funzione deve restituire un "blocco superiore", ovvero il blocco principale nello spazio di lavoro del mutatore a cui si collegano i blocchi secondari.

compose interpreta quindi la configurazione dei sottoblocchi e li utilizza per modificare il blocco principale. Questa funzione deve accettare come parametro il "blocco superiore" restituito da decompose.

Tieni presente che queste funzioni vengono "mixate" nel blocco che viene "modificato", quindi this può essere utilizzato per fare riferimento a quel blocco.

// 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 vuoi, puoi anche definire una funzione saveConnections che funzioni con la UI predefinita. Questa funzione ti offre la possibilità di associare i blocchi secondari del blocco principale (che si trova nello spazio di lavoro principale) ai blocchi secondari che si trovano nello spazio di lavoro del mutatore. Puoi quindi utilizzare questi dati per assicurarti che la funzione compose ricolleghi correttamente i figli del blocco principale quando i blocchi secondari vengono riorganizzati.

saveConnections deve accettare come parametro il "blocco superiore" restituito dalla funzione decompose. Se la funzione saveConnections è definita, Blockly la chiamerà prima di chiamare 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();
  }
},

In fase di registrazione

I mutatori sono solo un tipo speciale di mixin, quindi devono essere registrati prima di poter essere utilizzati nella definizione JSON del tipo di blocco.

// 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: una stringa da associare al mutatore in modo da poterlo utilizzare in JSON.
  • mixinObj: un oggetto contenente i vari metodi di mutazione. Ad es. saveExtraState e loadExtraState.
  • opt_helperFn: una funzione helper facoltativa che verrà eseguita sul blocco dopo l'inclusione del mixin.
  • opt_blockList: Un array facoltativo di tipi di blocchi (come stringhe) che verranno aggiunti al riquadro a comparsa nell'interfaccia utente del mutatore predefinita, se sono definiti anche i metodi dell'interfaccia utente.

Tieni presente che, a differenza delle estensioni, ogni tipo di blocco può avere un solo mutatore.

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

Funzione helper

Oltre al mixin, un mutatore può registrare una funzione helper. Questa funzione viene eseguita su ogni blocco del tipo specificato dopo la creazione e l'aggiunta di mixinObj. Può essere utilizzato per aggiungere trigger o effetti aggiuntivi a una mutazione.

Ad esempio, potresti aggiungere un helper al blocco di tipo elenco che imposta il numero iniziale di elementi:

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