Estensioni e mutatori

Le estensioni sono funzioni che vengono eseguite su ogni blocco di un determinato tipo durante la creazione. Queste spesso aggiungono configurazione o comportamento personalizzati a un blocco.

Un mutatore è un tipo speciale di estensione che aggiunge la serializzazione personalizzata, e talvolta UI, a un blocco.

Estensioni

Le estensioni sono funzioni che vengono eseguite su ogni blocco di un determinato tipo durante la creazione. Possono aggiungere configurazioni personalizzate (ad esempio l'impostazione della descrizione comando del blocco) o comportamenti personalizzati (ad esempio l'aggiunta di un listener di eventi al blocco).

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

Le estensioni devono essere "registrate" in modo da poter essere associate a una chiave stringa. Dopodiché puoi assegnare questa chiave stringa alla proprietà extensions della definizione JSON del tuo tipo di blocco per applicare l'estensione al blocco.

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

Puoi anche aggiungere più estensioni contemporaneamente. Tieni presente che la proprietà extensions deve essere un array, anche se stai applicando una sola estensione.

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

Mixin

Blockly fornisce anche un metodo pratico per situazioni in cui vuoi aggiungere proprietà/funzioni helper a un blocco, ma non eseguirle immediatamente. Questo permette di registrare un oggetto mixin che contiene tutte le proprietà/i metodi aggiuntivi. L'oggetto mixin viene quindi aggregato in una funzione che applica il mixin ogni volta che viene creata un'istanza del tipo di blocco specificato.

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

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

È possibile fare riferimento alle chiavi stringa associate ai mixin in JSON come qualsiasi altra estensione.

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

Mutatori

Un mutatore è un tipo speciale di estensione che aggiunge un'ulteriore serializzazione (extra stato che viene salvato e caricato) a un blocco. Ad esempio, i blocchi controls_if e list_create_with integrati richiedono una serializzazione aggiuntiva in modo da poter salvare il numero di input presenti.

Tieni presente che modificare la forma del blocco non significa necessariamente che sia necessaria un'ulteriore serializzazione. Ad esempio, il blocco math_number_property cambia forma, ma questo avviene in base a un campo a discesa il cui valore viene già serializzato. Di conseguenza, può utilizzare uno strumento di convalida dei campi e non ha bisogno di un modificatore.

Consulta la pagina relativa alla serializzazione per ulteriori informazioni su quando è necessario un mutatore e quando non è necessario.

I mutatori forniscono anche un'interfaccia utente integrata per consentire agli utenti di cambiare 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 precedente 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, loadExtraState accetta lo stesso valore seriale 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 è nello stato predefinito quando è serializzato, il metodo saveExtraState può restituire null per indicarlo. Se il metodo saveExtraState restituisce null, non viene aggiunta alcuna proprietà extraState al JSON. In questo modo, le dimensioni del file salvato vengono ridotte.

Serializzazione e backup completi dei dati

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

Ecco due casi d'uso comuni:

  • Quando un singolo blocco viene caricato in un'area di lavoro in cui il modello dei dati di supporto non esiste, ha informazioni sufficienti nel proprio stato per creare un nuovo modello dei dati.
  • Quando un blocco viene copiato e incollato, viene sempre creato un nuovo modello di dati di supporto invece di fare riferimento a uno esistente.

Alcuni dei blocchi che utilizzano questa funzione sono i blocchi @blockly/block-shareable-procedures. Normalmente serializza un riferimento a un modello dei dati di supporto, in modo da archiviare il relativo stato. Tuttavia, se il parametro doFullSerialization è true, serializzano tutto il loro stato. I blocchi di procedure condivisibili lo utilizzano per garantire che, quando vengono copiati e incollati, crei un nuovo modello di dati di supporto, invece di fare riferimento a un modello esistente.

mutationToDom e domToMutation

mutationToDom e domToMutation sono hook di serializzazione che funzionano con il precedente sistema di serializzazione XML. Utilizza questi hook solo se necessario (ad esempio se stai lavorando su un vecchio codebase di cui non è stata ancora eseguita la migrazione), altrimenti usa 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_();
},

Il file XML risultante sarà simile al seguente:

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

Se la funzione mutationToDom restituisce null, non verrà aggiunto alcun elemento aggiuntivo al file XML.

Hook dell'interfaccia utente

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

Non è necessario utilizzare questa UI se vuoi aggiungere un'ulteriore serializzazione. Puoi utilizzare un'interfaccia utente personalizzata, come il plug-in blocks-plus-minus, oppure non utilizzare affatto.

comporre e scomporre

L'interfaccia utente predefinita si basa sulle funzioni compose e decompose.

decompose "esplode" il blocco in blocchi secondari più piccoli che possono essere spostati, aggiunti ed eliminati. Questa funzione deve restituire un "blocco superiore", ovvero il blocco principale nell'area di lavoro di modifica a cui si connettono i blocchi secondari.

compose interpreta quindi la configurazione dei blocchi secondari e li utilizza per modificare il blocco principale. Questa funzione deve accettare il "blocco in alto" che è stato restituito da decompose come parametro.

Tieni presente che queste funzioni vengono "combinate" con il blocco "mutato" in modo che this possa 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

Facoltativamente, puoi anche definire una funzione saveConnections che funzioni con l'interfaccia utente predefinita. Questa funzione ti offre la possibilità di associare i componenti secondari del tuo blocco principale (che esiste nell'area di lavoro principale) ai blocchi secondari esistenti nell'area di lavoro di modifica. Puoi quindi utilizzare questi dati per assicurarti che la funzione compose riconnetti correttamente gli elementi secondari del blocco principale quando i blocchi secondari vengono riorganizzati.

saveConnections deve accettare il "blocco superiore" restituito dalla funzione decompose come parametro. 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 estensione, quindi devono anche essere registrati prima di poter essere utilizzati nella definizione JSON del tuo 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 poterla utilizzare in JSON.
  • mixinObj: un oggetto contenente i vari metodi di mutazione. Ad esempio, saveExtraState e loadExtraState.
  • opt_helperFn: una funzione helper facoltativa che viene eseguita sul blocco dopo l'unione del mixin.
  • opt_blockList: un array facoltativo di tipi di blocco (come stringhe) che verrà aggiunto al riquadro a comparsa nell'interfaccia utente mutator predefinita, se sono stati 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 di supporto

Insieme alla 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 usato per aggiungere ulteriori attivatori o effetti a una mutazione.

Ad esempio, puoi aggiungere un helper al blocco simile a un elenco che imposti il numero iniziale di elementi:

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