Расширения и мутаторы

Расширения — это функции, которые выполняются для каждого блока заданного типа при его создании. Они часто добавляют к блоку некоторую пользовательскую конфигурацию или поведение .

Мутатор — это особый вид расширения, которое добавляет к блоку пользовательскую сериализацию , а иногда и пользовательский интерфейс.

Расширения

Расширения — это функции, которые выполняются для каждого блока заданного типа при его создании. Они могут добавлять пользовательскую конфигурацию (например, настройку всплывающей подсказки блока) или собственное поведение (например, добавление прослушивателя событий в блок).

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

Расширения должны быть «зарегистрированы», чтобы их можно было связать со строковым ключом. Затем вы можете назначить этот строковый ключ свойству extensions определения JSON вашего типа блока, чтобы применить расширение к блоку.

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

Вы также можете добавить несколько расширений одновременно. Обратите внимание, что свойство extensions должно быть массивом, даже если вы применяете только одно расширение.

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

Миксины

Blockly также предоставляет удобный метод для ситуаций, когда вы хотите добавить в блок некоторые свойства/вспомогательные функции, но не запускать их немедленно. Это работает, позволяя вам зарегистрировать объект примеси , который содержит все ваши дополнительные свойства/методы. Затем объект примеси оборачивается функцией, которая применяет примесь каждый раз, когда создается экземпляр данного типа блока.

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

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

На строковые ключи, связанные с миксинами, можно ссылаться в JSON, как и на любое другое расширение.

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

Мутаторы

Мутатор — это особый тип расширения, который добавляет к блоку дополнительную сериализацию (дополнительное состояние, которое сохраняется и загружается). Например, встроенные блоки controls_if и list_create_with нуждаются в дополнительной сериализации, чтобы они могли сохранять количество входных данных, которые у них есть.

Обратите внимание: изменение формы вашего блока не обязательно означает, что вам нужна дополнительная сериализация. Например, блок math_number_property меняет форму, но делает это на основе раскрывающегося поля, значение которого уже сериализовано. Таким образом, он может просто использовать валидатор поля и не нуждается в мутаторе.

См. страницу сериализации для получения дополнительной информации о том, когда вам нужен мутатор, а когда нет.

Мутаторы также предоставляют пользователям встроенный пользовательский интерфейс для изменения формы блоков, если вы предоставляете некоторые дополнительные методы.

Хуки сериализации

Мутаторы имеют две пары хуков сериализации, с которыми они работают. Одна пара перехватчиков работает с новой системой сериализации JSON, а другая пара — со старой системой сериализации XML. Вы должны предоставить хотя бы одну из этих пар.

saveExtraState и loadExtraState

saveExtraState и loadExtraState — это перехватчики сериализации, которые работают с новой системой сериализации JSON. saveExtraState возвращает сериализуемое значение JSON, которое представляет дополнительное состояние блока, а loadExtraState принимает то же самое сериализуемое значение JSON и применяет его к блоку.

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

Результирующий JSON будет выглядеть так:

{
  "type": "lists_create_with",
  "extraState": {
    "itemCount": 3 // or whatever the count is
  }
}
Нет состояния

Если ваш блок находится в состоянии по умолчанию при сериализации, то ваш метод saveExtraState может возвращать null , указывающее на это. Если ваш метод saveExtraState возвращает null , то в JSON свойство extraState не добавляется. Благодаря этому размер файла сохранения будет небольшим.

Полная сериализация и резервные данные

saveExtraState также получает необязательный параметр doFullSerialization . Это используется блоками, которые ссылаются на состояние, сериализованное другим сериализатором (например, резервными моделями данных). Параметр сигнализирует, что указанное состояние не будет доступно при десериализации блока, поэтому блок должен сам сериализовать все резервное состояние. Например, это справедливо при сериализации отдельного блока или при копировании блока.

Два распространенных случая использования:

  • Когда отдельный блок загружается в рабочую область, где не существует базовой модели данных, в его собственном состоянии имеется достаточно информации для создания новой модели данных.
  • Когда блок копируется, он всегда создает новую модель данных поддержки вместо ссылки на существующую.

Некоторые блоки, которые используют это, — это блоки @blockly/block-shareable-procedures . Обычно они сериализуют ссылку на резервную модель данных, в которой хранится их состояние. Но если параметр doFullSerialization имеет значение true, они сериализуют все свое состояние. Блоки общих процедур используют это, чтобы гарантировать, что при копировании они создают новую резервную модель данных, а не ссылаются на существующую модель.

мутацияToDom и domToMutation

mutationToDom и domToMutation — это перехватчики сериализации, которые работают со старой системой сериализации XML. Используйте эти перехватчики только в случае необходимости (например, вы работаете над старой кодовой базой, которая еще не была перенесена), в противном случае используйте saveExtraState и loadExtraState .

mutationToDom возвращает узел XML, который представляет дополнительное состояние блока, а domToMutation принимает тот же узел XML и применяет состояние к блоку.

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

Результирующий XML будет выглядеть так:

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

Если ваша mutationToDom возвращает значение null, то в XML не будет добавлено никаких дополнительных элементов.

Хуки пользовательского интерфейса

Если вы предоставляете определенные функции как часть своего мутатора, Blockly добавит в ваш блок пользовательский интерфейс «мутатора» по умолчанию.

Вам не обязательно использовать этот пользовательский интерфейс, если вы хотите добавить дополнительную сериализацию. Вы можете использовать собственный пользовательский интерфейс, например , плагин «блоки-плюс-минус» , или вообще не использовать пользовательский интерфейс!

сочинять и разлагать

Пользовательский интерфейс по умолчанию основан на функциях compose и decompose .

decompose «разбивает» блок на более мелкие подблоки, которые можно перемещать, добавлять и удалять. Эта функция должна возвращать «верхний блок», который является основным блоком в рабочей области мутатора, к которому подключаются подблоки.

Затем compose интерпретирует конфигурацию подблоков и использует их для изменения основного блока. Эта функция должна принимать «верхний блок», который был возвращен при decompose , в качестве параметра.

Обратите внимание, что эти функции «примешиваются» к «мутируемому» блоку, поэтому this можно использовать для ссылки на этот блок.

// 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 , которая работает с пользовательским интерфейсом по умолчанию. Эта функция дает вам возможность связать дочерние элементы вашего основного блока (который существует в основной рабочей области) с подблоками, которые существуют в вашей рабочей области мутатора. Затем вы можете использовать эти данные, чтобы убедиться, что ваша функция compose правильно повторно соединяет дочерние элементы вашего основного блока при реорганизации ваших подблоков.

saveConnections должен принимать «верхний блок», возвращаемый вашей функцией decompose в качестве параметра. Если функция saveConnections определена, Blockly вызовет ее перед вызовом 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();
  }
},

Регистрация

Мутаторы — это всего лишь особый вид расширения, поэтому их также необходимо зарегистрировать, прежде чем вы сможете использовать их в определении JSON вашего типа блока.

// 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 : строка, которую нужно связать с мутатором, чтобы вы могли использовать ее в JSON.
  • mixinObj : объект, содержащий различные методы мутации. Например saveExtraState и loadExtraState .
  • opt_helperFn : необязательная вспомогательная функция , которая будет выполняться в блоке после примешивания примеси.
  • opt_blockList : необязательный массив типов блоков (в виде строк), который будет добавлен во всплывающее окно в пользовательском интерфейсе мутатора по умолчанию, если также определены методы пользовательского интерфейса.

Обратите внимание, что в отличие от расширений, каждый тип блока может иметь только один мутатор.

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

Вспомогательная функция

Наряду с миксином мутатор может зарегистрировать вспомогательную функцию. Эта функция запускается для каждого блока данного типа после его создания и добавления mixinObj. Его можно использовать для добавления дополнительных триггеров или эффектов к мутации.

Например, вы можете добавить в блок, похожий на список, помощник, который устанавливает начальное количество элементов:

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