Rozszerzenia i mutatory

Rozszerzenia to funkcje, które uruchamiają się w poszczególnych blokach danego typu w miarę ich tworzenia. Często dodają one do bloku niestandardową konfigurację lub działanie.

Mutator to specjalne rozszerzenie, które dodaje do bloku niestandardową serializację, a czasem także interfejs użytkownika.

Rozszerzenia

Rozszerzenia to funkcje, które uruchamiają się w poszczególnych blokach danego typu w miarę ich tworzenia. Może on dodać konfigurację niestandardową (np. ustawianie etykietki bloku) lub zachowanie niestandardowe (np. dodanie do bloku odbiornika).

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

Rozszerzenia muszą zostać „zarejestrowane”, aby można było powiązać je z kluczem ciągu. Następnie możesz przypisać ten klucz ciągu do właściwości extensions w definicji JSON typu bloku, aby zastosować rozszerzenie do bloku.

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

Możesz też dodać wiele rozszerzeń naraz. Pamiętaj, że właściwość extensions musi być tablicą, nawet jeśli stosujesz tylko jedno rozszerzenie.

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

Składanki

Blockly jest też wygodne w sytuacjach, gdy chcesz dodać do bloku pewne właściwości lub funkcje pomocnicze, ale nie uruchamiać ich od razu. Jest to możliwe dzięki możliwości zarejestrowania obiektu mixin, który zawiera wszystkie dodatkowe właściwości/metody. Obiekt Mixin jest następnie pakowany do funkcji, która stosuje miksin za każdym razem, gdy tworzone jest wystąpienie danego typu bloku.

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

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

Do kluczy ciągów znaków powiązanych z miksinami można się odwoływać w formacie JSON tak jak do każdego innego rozszerzenia.

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

Mutatory

Mutator to specjalny typ rozszerzenia, który dodaje do bloku dodatkową serializację (dodatkowy stan, który jest zapisywany i ładowany). Na przykład wbudowane bloki controls_if i list_create_with wymagają dodatkowej serializacji, aby można było zapisać liczbę wejść.

Pamiętaj, że zmiana kształtu bloku niekoniecznie oznacza konieczność dodatkowej serializacji. Na przykład blok math_number_property zmienia kształt, ale dzieje się to na podstawie pola z menu, którego wartość została już zserializowana. Dzięki temu może używać tylko walidatora pól i nie potrzebuje mutatora.

Na stronie serializacji znajdziesz więcej informacji o tym, kiedy potrzebny jest mutator, a kiedy nie.

Mutatory mają też wbudowany interfejs, który pozwala użytkownikom zmieniać kształty bloków, jeśli podasz kilka opcjonalnych metod.

Punkty zaczepienia serializacji

Mutatory mają 2 pary punktów zaczepienia serializacji, z którymi współpracują. Jedna para zaczepów działa z nowym systemem serializacji JSON, a druga ze starym systemem serializacji XML. Musisz podać co najmniej jedną z tych par.

zachowają stan”.

saveExtraState i loadExtraState to punkty zaczepienia serializacji, które działają z nowym systemem serializacji JSON. saveExtraState zwraca wartość zserializowaną w formacie JSON, która reprezentuje dodatkowy stan bloku, a loadExtraState akceptuje tę samą wartość zserializowaną w formacie JSON i stosuje ją do bloku.

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

Powstały plik JSON będzie wyglądał tak:

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

Jeśli blok jest w stanie domyślnym po zserializowaniu, metoda saveExtraState może zwracać wartość null, aby to zasygnalizować. Jeśli metoda saveExtraState zwraca wartość null, do pliku JSON nie jest dodawana żadna właściwość extraState. Dzięki temu rozmiar zapisywanych plików jest niewielki.

Pełna serializacja i tworzenie kopii zapasowych danych

saveExtraState otrzymuje też opcjonalny parametr doFullSerialization. Jest ona używana przez bloki, które odwołują się do stanu zserializowanego przez inny serializer (np. zapasowe modele danych). Parametr sygnalizuje, że dany stan nie będzie dostępny, gdy blok zostanie poddany deserializacji, dlatego blok powinien zserializować cały stan backendu. Dzieje się tak na przykład wtedy, gdy pojedynczy blok jest zserializowany lub gdy blok jest kopiowany i wklejany.

Oto 2 typowe przypadki użycia tej funkcji:

  • Gdy pojedynczy blok jest wczytywany do obszaru roboczego, w którym nie istnieje bazowy model danych, ma on wystarczającą ilość informacji w swoim stanie, aby utworzyć nowy model danych.
  • Gdy blok jest wklejany w wyniku kopiowania i wklejania, zawsze tworzy nowy zapasowy model danych, zamiast odwoływać się do istniejącego modelu.

Przykładowe blokady to bloki @blockly/block-shareable-procedures. Zwykle zserializują odwołanie do zapasowego modelu danych, w którym przechowywany jest ich stan. Jeśli jednak parametr doFullSerialization ma wartość prawda, zserializują cały swój stan. Bloki procedur z możliwością udostępniania korzystają z tej funkcji, aby mieć pewność, że kopiowanie i wklejanie powoduje utworzenie nowego zapasowego modelu danych, a nie odwoływania się do istniejącego modelu.

mutationToDom i domToMutation,

mutationToDom i domToMutation to punkty zaczepienia serializacji, które działają ze starym systemem serializacji XML. Używaj tych punktów zaczepienia tylko wtedy, gdy to konieczne (np.używasz starej bazy kodu, która nie została jeszcze przeniesiona). W innych przypadkach używaj znaczników saveExtraState i loadExtraState.

mutationToDom zwraca węzeł XML, który reprezentuje dodatkowy stan bloku, a domToMutation akceptuje ten sam węzeł XML i stosuje ten stan do bloku.

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

Powstały kod XML będzie wyglądał tak:

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

Jeśli funkcja mutationToDom zwraca wartość null, do kodu XML nie jest dodawany żaden dodatkowy element.

Elementy interfejsu

Jeśli w ramach mutatora podasz określone funkcje, Blockly doda do bloku domyślny interfejs użytkownika.

Nie musisz używać tego interfejsu, jeśli chcesz dodać dodatkową serializację. Możesz użyć niestandardowego interfejsu, takiego jak blocks-plus-minus wtyczka, albo w ogóle nie używać interfejsu.

utwórz i rozłóż

Domyślny interfejs użytkownika korzysta z funkcji compose i decompose.

decompose „rozbija” blok na mniejsze podrzędne bloki, które można przesuwać, dodawać i usuwać. Ta funkcja powinna zwracać „górny blok”, który jest głównym blokiem w obszarze roboczym mutatora, z którym są połączone bloki podrzędne.

compose interpretuje konfigurację bloków podrzędnych i wykorzystuje je do zmiany bloku głównego. Ta funkcja powinna akceptować jako parametr „górny blok”, który został zwrócony przez funkcję decompose.

Pamiętaj, że te funkcje są „mieszane” z blokiem, który jest „mutowany”, więc można użyć funkcji this do odwoływania się do tego bloku.

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

Opcjonalnie możesz też zdefiniować funkcję saveConnections zgodną z domyślnym interfejsem użytkownika. Ta funkcja umożliwia powiązanie elementów podrzędnych bloku głównego (istniejącego w głównym obszarze roboczym) z blokami podrzędnymi, które znajdują się w obszarze roboczym mutatora. Możesz potem użyć tych danych, aby upewnić się, że po zmianie organizacji bloków podrzędnych funkcja compose ponownie łączy elementy podrzędne bloku głównego.

saveConnections powinien akceptować jako parametr „górny blok” zwrócony przez funkcję decompose. Jeśli funkcja saveConnections jest zdefiniowana, Blockly wywoła ją przed wywołaniem 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();
  }
},

Rejestruję…

Mutatory to po prostu specjalny rodzaj rozszerzenia, dlatego przed użyciem w definicji JSON typu bloku musisz je zarejestrować.

// 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: ciąg znaków do powiązania z mutatorem, aby można było go używać w formacie JSON.
  • mixinObj: obiekt zawierający różne metody mutacji. Na przykład: saveExtraState i loadExtraState.
  • opt_helperFn: opcjonalna funkcja pomocnicza, która będzie uruchamiana na bloku po wymieszaniu składanki.
  • opt_blockList: opcjonalna tablica typów bloków (jako ciągi znaków), która zostanie dodana do menu w domyślnym interfejsie mutatora, jeśli będą też zdefiniowane metody interfejsu.

Pamiętaj, że w przeciwieństwie do rozszerzeń każdy typ bloku może mieć tylko 1 mutator.

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

Funkcja pomocnicza

Wraz z miksinem mutator może zarejestrować funkcję pomocniczą. Ta funkcja jest uruchamiana w każdym bloku danego typu po jej utworzeniu i dodaniu obiektu MixinObj. Można go używać do dodawania do mutacji dodatkowych wyzwalaczy lub efektów.

Do bloku podobnego do listy możesz na przykład dodać element pomocniczy, który ustawia początkową liczbę elementów:

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