Como criar blocos de procedimentos personalizados

A criação de blocos de procedimentos personalizados exige que você:

  1. Instale o plug-in @blockly/block-shareable-procedures, conforme descrito na página de procedimentos de uso.
  2. Use o sistema de serialização JSON, conforme explicado na página de visão geral.

Adicionar modelos de dados ao espaço de trabalho

Os blocos de definição e de autor da chamada de procedimento se referem a um modelo de dados de apoio, que define a assinatura do procedimento (nome, parâmetros e retorno). Isso permite mais flexibilidade na forma como você projeta o aplicativo (por exemplo, é possível permitir que procedimentos sejam definidos em um espaço de trabalho e referenciados em outro).

Isso significa que você precisará adicionar os modelos de dados do procedimento ao espaço de trabalho para que seus blocos funcionem. Há muitas maneiras de fazer isso (por exemplo, IUs personalizadas).

O @blockly/block-shareable-procedures faz isso com os blocos de definição de procedimento criam dinamicamente os modelos de dados de apoio quando são instanciados no espaço de trabalho. Para implementar isso por conta própria, crie o modelo em init e exclua-o em destroy.

import {ObservableProcedureModel} from '@blockly/block-shareable-procedures';

Blockly.Blocks['my_procedure_def'] = {
  init: function() {
    this.model = new ObservableProcedureModel('default name');
    this.workspace.getProcedureMap().add(model);
    // etc...
  }

  destroy: function() {
    // Optionally:
    // Destroy the model when the definition block is deleted.
    this.workpace.getProcedureMap().delete(model.getId());
  }
}

Retornar informações sobre os blocos

Os blocos de definição e chamada de procedimento precisam implementar os métodos getProcedureModel, isProcedureDef e getVarModels. Esses são os hooks que o código do Blockly usa para receber informações sobre os blocos de procedimento.

Blockly.Blocks['my_procedure_def'] = {
  getProcedureModel() {
    return this.model;
  },

  isProcedureDef() {
    return true;
  },

  getVarModels() {
    // If your procedure references variables
    // then you should return those models here.
    return [];
  },
};

Blockly.Blocks['my_procedure_call'] = {
  getProcedureModel() {
    return this.model;
  },

  isProcedureDef() {
    return false;
  },

  getVarModels() {
    // If your procedure references variables
    // then you should return those models here.
    return [];
  },
};

Acionar nova renderização em atualizações

Os blocos de definição e chamada de procedimento precisam implementar o método doProcedureUpdate. Esse é o gancho que os modelos de dados chamam para dizer aos blocos de procedimento para que se renderizem novamente.

Blockly.Blocks['my_procedure_def'] = {
  doProcedureUpdate() {
    this.setFieldValue('NAME', this.model.getName());
    this.setFieldValue(
        'PARAMS',
        this.model.getParameters()
            .map((p) => p.getName())
            .join(','));
    this.setFieldValue(
        'RETURN', this.model.getReturnTypes().join(',');
  }
};

Blockly.Blocks['my_procedure_call'] = {
  doProcedureUpdate() {
    // Similar to the def block above...
  }
};

Adicionar serialização personalizada

A serialização de blocos de procedimento precisa realizar duas ações separadas.

  1. Ao carregar do JSON, seus blocos precisarão buscar uma referência ao modelo de dados de apoio, já que eles são serializados separadamente.
  2. Ao copiar e colar um bloco de procedimento, ele precisará serializar todo o estado do modelo de procedimento para que ele possa ser replicado/duplicado.

Essas duas coisas são processadas usando saveExtraState e loadExtraState. Observe mais uma vez que os blocos de procedimentos personalizados só são compatíveis com o sistema de serialização JSON. Portanto, basta definir os ganchos de serialização JSON.

import {
    ObservableProcedureModel,
    ObservableParameterModel,
    isProcedureBlock
} from '@blockly/block-shareable-procedures';

Blockly.Blocks['my_procedure_def'] = {
  saveExtraState() {
    return {
      'procedureId': this.model.getId(),

      // These properties are only necessary for pasting.
      'name': this.model.getName(),
      'parameters': this.model.getParameters().map((p) => {
        return {name: p.getName(), p.getId()};
      }),
      'returnTypes': this.model.getReturnTypes(),
    };
  },

  loadExtraState(state) {
    const id = state['procedureId']
    const map = this.workspace.getProcedureMap();

    // Grab a reference to the existing procedure model.
    if (this.model.getId() != id && map.has(id) &&
        (this.isInsertionMarker || this.noBlockHasClaimedModel_(id))) {
      // Delete the existing model (created in init).
      this.workspace.getProcedureMap().delete(model.getId());
      // Grab a reference to the new model.
      this.model = this.workspace.getProcedureMap()
          .get(state['procedureId']);
      this.doProcedureUpdate();
      return;
    }

    // There is no existing procedure model (we are likely pasting), so
    // generate it from JSON.
    this.model
        .setName(state['name'])
        .setReturnTypes(state['returnTypes']);
    for (const [i, param] of state['parameters'].entries()) {
      this.model.insertParameter(
          i,
          new ObservableParameterModel(
              this.workspace, param['name'], param['id']));
    }
  },

  // We don't want to reference a model that some other procedure definition
  // is already referencing.
  noBlockHasClaimedModel_(procedureId) {
    const model =
      this.workspace.getProcedureMap().get(procedureId);
    return this.workspace.getAllBlocks(false).every(
      (block) =>
        !isProcedureBlock(block) ||
        !block.isProcedureDef() ||
        block.getProcedureModel() !== model);
  },
};

Blockly.Blocks['my_procedure_call'] = {
  saveExtraState() {
    return {
      'procedureId': this.model.getId(),
    };
  },

  loadExtraState(state) {
    // Delete our existing model (created in init).
    this.workspace.getProcedureMap().delete(model.getId());
    // Grab a reference to the new model.
    this.model = this.workspace.getProcedureMap()
        .get(state['procedureId']);
    if (this.model) this.doProcedureUpdate();
  },

  // Handle pasting after the procedure definition has been deleted.
  onchange(event) {
    if (event.type === Blockly.Events.BLOCK_CREATE &&
        event.blockId === this.id) {
      if(!this.model) { // Our procedure definition doesn't exist =(
        this.dispose();
      }
    }
  }
};

Modifique o modelo/assinatura do procedimento (opcional)

Também é possível permitir que os usuários modifiquem o modelo/assinatura do procedimento. Chamar os métodos insertParameter, deleteParameter ou setReturnTypes acionará automaticamente seus blocos para serem renderizados novamente (via doProcedureUpdate).

As opções para criar IUs e modificar o modelo de procedimento incluem o uso de mutadores (que os blocos de procedimentos integrados usam), campos de imagem com gerenciadores de cliques, algo totalmente externo ao Blockly etc.

Adicionar blocos à caixa de ferramentas

A categoria de procedimentos dinâmicos integrados do Blockly é específica dos blocos de procedimentos integrados do Blockly. Para acessar seus blocos, você precisa definir sua própria categoria dinâmica personalizada e adicioná-la à caixa de ferramentas.

const proceduresFlyoutCallback = function(workspace) {
  const blockList = [];
  blockList.push({
    'kind': 'block',
    'type': 'my_procedure_def',
  });
  for (const model of
        workspace.getProcedureMap().getProcedures()) {
    blockList.push({
      'kind': 'block',
      'type': 'my_procedure_call',
      'extraState': {
        'procedureId': model.getId(),
      },
    });
  }
  return blockList;
};

myWorkspace.registerToolboxCategoryCallback(
    'MY_PROCEDURES', proceduresFlyoutCallback);