Como criar um novo tipo de campo

Antes de criar um novo tipo de campo, considere se um dos outros métodos para personalizar campos atende às suas necessidades. Se o aplicativo precisar armazenar um novo tipo de valor ou se você quiser criar uma nova IU para um tipo de valor existente, provavelmente vai ser necessário criar um novo tipo de campo.

Para criar um novo campo, faça o seguinte:

  1. Implementar um construtor.
  2. Registre uma chave JSON e implemente o fromJson.
  3. Processar a inicialização da IU do bloco e dos listeners de eventos.
  4. Processar o descarte de listeners de eventos (o descarte da interface é feito para você).
  5. Implemente o processamento de valores.
  6. Adicione uma representação em texto do valor do campo para fins de acessibilidade.
  7. Adicione outras funcionalidades, como:
  8. Configure outros aspectos do campo, como:

Nesta seção, presumimos que você tenha lido e familiarizado com o conteúdo em Anatomia de um campo.

Para ver um exemplo de campo personalizado, consulte a demonstração dos campos personalizados.

Como implementar um construtor

O construtor do campo é responsável por configurar o valor inicial do campo e, opcionalmente, configurar um validador local. O construtor do campo personalizado é chamado durante a inicialização do bloco de origem, não importa se o bloco de origem está definido em JSON ou JavaScript. Portanto, o campo personalizado não tem acesso ao bloco de origem durante a construção.

O exemplo de código a seguir cria um campo personalizado chamado GenericField:

class GenericField extends Blockly.Field {
  constructor(value, validator) {
    super(value, validator);

    this.SERIALIZABLE = true;
  }
}

Assinatura do método

Construtores de campo geralmente recebem um valor e um validador local. O valor é opcional e, se você não transmitir um valor (ou transmitir um valor que falhe na validação da classe), o valor padrão da superclasse será usado. Para a classe Field padrão, esse valor é null. Se você não quiser esse valor padrão, transmita um valor adequado. O parâmetro do validador está presente apenas para campos editáveis e geralmente é marcado como opcional. Saiba mais sobre os validadores neste documento.

Estrutura

A lógica dentro do construtor precisa seguir este fluxo:

  1. Chame o superconstrutor herdado (todos os campos personalizados precisam herdar de Blockly.Field ou uma das subclasses) para inicializar corretamente o valor e definir o validador local para seu campo.
  2. Se o campo for serializável, defina a propriedade correspondente no construtor. Os campos editáveis precisam ser serializáveis e os campos são editáveis por padrão. Portanto, você provavelmente precisa definir essa propriedade como verdadeira, a menos que saiba que não pode ser serializável.
  3. Opcional: aplique outras personalizações. Por exemplo, os campos de rótulo permitem que uma classe CSS seja transmitida e aplicada ao texto.

JSON e registro

Nas definições de bloco JSON, os campos são descritos por uma string (por exemplo, field_number, field_textinput). Mantém blockly um mapa dessas strings para objetos de campo e chama fromJson no objeto apropriado durante a construção.

Chame Blockly.fieldRegistry.register para adicionar seu tipo de campo a esse mapa, transmitindo a classe de campo como o segundo argumento:

Blockly.fieldRegistry.register('field_generic', GenericField);

Também é necessário definir a função fromJson. Primeiro, a implementação precisa cancelar a referência a todas as referências a tabelas de strings usando replaceMessageReferences e, em seguida, transmitir os valores para o construtor.

GenericField.fromJson = function(options) {
  const value = Blockly.utils.parsing.replaceMessageReferences(
      options['value']);
  return new CustomFields.GenericField(value);
};

Inicializando

Quando seu campo é construído, ele basicamente contém apenas um valor. A inicialização é onde o DOM é criado, o modelo (se o campo tiver um modelo) e os eventos são vinculados.

Exibição no bloco

Durante a inicialização, você é responsável por criar tudo o que precisará para a exibição em bloco do campo.

Padrões, plano de fundo e texto

A função initView padrão cria um elemento rect de cor clara e um elemento text. Se você quiser que seu campo tenha ambos, além de outros produtos, chame a função initView da superclasse antes de adicionar o restante dos elementos DOM. Se você quiser que seu campo tenha um desses elementos, mas não ambos, use as funções createBorderRect_ ou createTextElement_.

Como personalizar a construção do DOM

Se o campo for de texto genérico (por exemplo, Entrada de texto), a construção do DOM será processada para você. Caso contrário, você terá que substituir a função initView para criar os elementos DOM necessários durante a renderização futura do campo.

Por exemplo, um campo suspenso pode conter imagens e texto. Em initView, ele cria um único elemento de imagem e um único elemento de texto. Em seguida, durante render_, ele mostra o elemento ativo e oculta o outro, com base no tipo da opção selecionada.

Para criar elementos DOM, use o método Blockly.utils.dom.createSvgElement ou os métodos tradicionais de criação.

Os requisitos da exibição em bloco de um campo são:

  • Todos os elementos DOM precisam ser filhos do fieldGroup_ do campo. O grupo de campos é criado automaticamente.
  • Todos os elementos DOM precisam estar dentro das dimensões informadas do campo.

Consulte a seção Renderização para ver mais detalhes sobre como personalizar e atualizar a tela no bloco.

Como adicionar símbolos de texto

Para adicionar símbolos ao texto de um campo (como o símbolo de grau do campo Angle), anexe o elemento de símbolo (geralmente contido em uma <tspan>) diretamente ao textElement_ do campo.

Eventos de entrada

Por padrão, os campos registram eventos de dica e eventos de mousedown, que serão usados para mostrar editores. Se você quiser detectar outros tipos de eventos (por exemplo, se quiser processar o arrasto em um campo), modifique a função bindEvents_ do campo.

bindEvents_() {
  // Call the superclass function to preserve the default behavior as well.
  super.bindEvents_();

  // Then register your own additional event listeners.
  this.mouseDownWrapper_ =
  Blockly.browserEvents.conditionalBind(this.getClickTarget_(), 'mousedown', this,
      function(event) {
        this.originalMouseX_ = event.clientX;
        this.isMouseDown_ = true;
        this.originalValue_ = this.getValue();
        event.stopPropagation();
      }
  );
  this.mouseMoveWrapper_ =
    Blockly.browserEvents.conditionalBind(document, 'mousemove', this,
      function(event) {
        if (!this.isMouseDown_) {
          return;
        }
        var delta = event.clientX - this.originalMouseX_;
        this.setValue(this.originalValue_ + delta);
      }
  );
  this.mouseUpWrapper_ =
    Blockly.browserEvents.conditionalBind(document, 'mouseup', this,
      function(_event) {
        this.isMouseDown_ = false;
      }
  );
}

Para vincular a um evento, geralmente é necessário usar a função Blockly.utils.browserEvents.conditionalBind. Esse método de vinculação de eventos filtra toques secundários durante as ações de arrastar. Se você quiser que o gerenciador seja executado mesmo no meio de uma ação de arrastar em andamento, use a função Blockly.browserEvents.bind.

Descarte

Se você registrou listeners de eventos personalizados dentro da função bindEvents_ do campo, eles precisarão ser cancelados dentro da função dispose.

Se você inicializou corretamente a visualização do campo (anexando todos os elementos DOM ao fieldGroup_), o DOM do campo será descartado automaticamente.

Processamento de valor

→ Para saber mais sobre o valor de um campo x seu texto, consulte Anatomia de um campo.

Pedido de validação

Fluxograma que descreve a ordem em que os validadores são executados

Como implementar um validador de classes

Os campos só podem aceitar alguns valores. Por exemplo, os campos numéricos precisam aceitar apenas números, e os campos de cor devem aceitar apenas cores, etc. Isso é garantido por meio de validadores de classe e locais. O validador de classe segue as mesmas regras dos validadores locais, mas também é executado no construtor e, como tal, não pode referenciar o bloco de origem.

Para implementar o validador de classe do seu campo, substitua a função doClassValidation_.

doClassValidation_(newValue) {
  if (typeof newValue != 'string') {
    return null;
  }
  return newValue;
};

Como processar valores válidos

Se o valor transmitido para um campo com setValue for válido, você receberá um callback doValueUpdate_. Por padrão, a função doValueUpdate_:

  • Define a propriedade value_ como newValue.
  • Define a propriedade isDirty_ como true.

Se você só precisa armazenar o valor e não quer fazer nenhum processamento personalizado, não precisa substituir doValueUpdate_.

Caso contrário, se você quiser:

  • Armazenamento personalizado de newValue.
  • Mude outras propriedades com base em newValue.
  • Salva se o valor atual é válido ou não.

Você vai precisar substituir doValueUpdate_:

doValueUpdate_(newValue) {
  super.doValueUpdate_(newValue);
  this.displayValue_ = newValue;
  this.isValueValid_ = true;
}

Como lidar com valores inválidos

Se o valor transmitido ao campo com setValue for inválido, você vai receber um callback doValueInvalid_. Por padrão, a função doValueInvalid_ não faz nada. Isso significa que, por padrão, valores inválidos não serão mostrados. Isso também significa que o campo não será renderizado novamente, porque a propriedade isDirty_ não será definida.

Se você quiser exibir valores inválidos, substitua doValueInvalid_. Na maioria das vezes, é necessário definir uma propriedade displayValue_ como o valor inválido, definir isDirty_ como true e substituir render_ para que a tela no bloco seja atualizada com base em displayValue_ em vez de value_.

doValueInvalid_(newValue) {
  this.displayValue_ = newValue;
  this.isDirty_ = true;
  this.isValueValid_ = false;
}

Valores com várias partes

Quando seu campo contém um valor de várias partes (por exemplo, listas, vetores, objetos), convém que as partes sejam tratadas como valores individuais.

doClassValidation_(newValue) {
  if (FieldTurtle.PATTERNS.indexOf(newValue.pattern) == -1) {
    newValue.pattern = null;
  }

  if (FieldTurtle.HATS.indexOf(newValue.hat) == -1) {
    newValue.hat = null;
  }

  if (FieldTurtle.NAMES.indexOf(newValue.turtleName) == -1) {
    newValue.turtleName = null;
  }

  if (!newValue.pattern || !newValue.hat || !newValue.turtleName) {
    this.cachedValidatedValue_ = newValue;
    return null;
  }
  return newValue;
}

No exemplo acima, cada propriedade de newValue é validada individualmente. Em seguida, no final da função doClassValidation_, se alguma propriedade individual for inválida, o valor será armazenado em cache na propriedade cacheValidatedValue_ antes de retornar null (inválido). Armazenar o objeto em cache com propriedades validadas individualmente permite que a função doValueInvalid_ os processe separadamente, simplesmente fazendo uma verificação de !this.cacheValidatedValue_.property, em vez de revalidar cada propriedade individualmente.

Esse padrão para validar valores de várias partes também pode ser usado em validadores locais, mas atualmente não há como impor esse padrão.

isDirty_

isDirty_ é uma sinalização usada na função setValue, bem como em outras partes do campo, para indicar se ele precisa ser renderizado novamente. Se o valor de exibição do campo mudou, o isDirty_ normalmente precisa ser definido como true.

Texto

→ Para saber mais sobre onde o texto de um campo é usado e como ele é diferente do valor do campo, consulte Anatomia de um campo.

Se o texto do seu campo for diferente do valor dele, modifique a função getText para fornecer o texto correto.

getText() {
  let text = this.value_.turtleName + ' wearing a ' + this.value_.hat;
  if (this.value_.hat == 'Stovepipe' || this.value_.hat == 'Propeller') {
    text += ' hat';
  }
  return text;
}

Como criar um editor

Se você definir a função showEditor_, o Blockly detectará automaticamente os cliques e chamará showEditor_ no momento adequado. Você pode exibir qualquer HTML no seu editor envolvendo-o em um dos dois divs especiais, chamados DropDownDiv e WidgetDiv, que flutuam sobre o restante da IU do Blockly.

O DropDownDiv é usado para fornecer editores que ficam dentro de uma caixa conectada a um campo. Ele se posiciona automaticamente para ficar perto do campo, permanecendo dentro dos limites visíveis. Os seletores de ângulo e de cores são bons exemplos de DropDownDiv.

Imagem do seletor de ângulo

O WidgetDiv é usado para fornecer editores que não ficam dentro de uma caixa. Os campos numéricos usam WidgetDiv para cobrir o campo com uma caixa de entrada de texto HTML. O DropDownDiv processa o posicionamento para você, mas o WidgetDiv não. Os elementos precisarão ser posicionados manualmente. O sistema de coordenadas está em coordenadas de pixel relativas ao canto superior esquerdo da janela. O editor de entrada de texto é um bom exemplo do WidgetDiv.

Imagem do editor de entrada de texto

showEditor_() {
  // Create the widget HTML
  this.editor_ = this.dropdownCreate_();
  Blockly.DropDownDiv.getContentDiv().appendChild(this.editor_);

  // Set the dropdown's background colour.
  // This can be used to make it match the colour of the field.
  Blockly.DropDownDiv.setColour('white', 'silver');

  // Show it next to the field. Always pass a dispose function.
  Blockly.DropDownDiv.showPositionedByField(
      this, this.disposeWidget_.bind(this));
}

Exemplo de código de WidgetDiv

showEditor_() {
  // Show the div. This automatically closes the dropdown if it is open.
  // Always pass a dispose function.
  Blockly.WidgetDiv.show(
    this, this.sourceBlock_.RTL, this.widgetDispose_.bind(this));

  // Create the widget HTML.
  var widget = this.createWidget_();
  Blockly.WidgetDiv.getDiv().appendChild(widget);
}

Limpar

Tanto o DropDownDiv quanto o WidgetDiv processam a destruição dos elementos HTML do widget, mas é necessário descartar manualmente os listeners de eventos aplicados a esses elementos.

widgetDispose_() {
  for (let i = this.editorListeners_.length, listener;
      listener = this.editorListeners_[i]; i--) {
    Blockly.browserEvents.unbind(listener);
    this.editorListeners_.pop();
  }
}

A função dispose é chamada em um contexto null na DropDownDiv. No WidgetDiv, ele é chamado no contexto do WidgetDiv. Em ambos os casos, é melhor usar a função bind ao transmitir uma função de descarte, conforme mostrado nos exemplos de DropDownDiv e WidgetDiv acima.

→ Para saber mais sobre como descartar editores não, consulte Descarte.

Atualizar a tela no bloco

A função render_ é usada para atualizar a exibição no bloco do campo de acordo com o valor interno.

São exemplos comuns:

  • Mudar o texto (menu suspenso)
  • Mudar a cor (cor)

Padrões

A função render_ padrão define o texto de exibição como o resultado da função getDisplayText_. A função getDisplayText_ retorna a propriedade value_ do campo convertida em uma string, depois que ela é truncada para respeitar o tamanho máximo do texto.

Se você estiver usando a tela padrão no bloco, e o comportamento de texto padrão funcionar para seu campo, não será necessário substituir render_.

Se o comportamento de texto padrão funcionar para seu campo, mas a exibição no bloco dele tiver outros elementos estáticos, você poderá chamar a função render_ padrão, mas ainda será necessário substituí-la para atualizar o tamanho do campo.

Se o comportamento de texto padrão não funcionar para seu campo ou se a tela em bloco tiver outros elementos dinâmicos, será necessário personalizar a função render_.

Fluxograma descrevendo como tomar a decisão de substituir render_

Como personalizar a renderização

Se o comportamento de renderização padrão não funcionar para seu campo, será necessário definir o comportamento de renderização personalizado. Isso pode envolver desde a configuração de um texto de exibição personalizado até a mudança de elementos da imagem e a atualização das cores de segundo plano.

Todas as alterações de atributos do DOM são legais. Lembre-se apenas de duas coisas:

  1. A criação do DOM precisa ser feita durante a inicialização, porque é mais eficiente.
  2. Sempre atualize a propriedade size_ para corresponder ao tamanho da tela no bloco.
render_() {
  switch(this.value_.hat) {
    case 'Stovepipe':
      this.stovepipe_.style.display = '';
      break;
    case 'Crown':
      this.crown_.style.display = '';
      break;
    case 'Mask':
      this.mask_.style.display = '';
      break;
    case 'Propeller':
      this.propeller_.style.display = '';
      break;
    case 'Fedora':
      this.fedora_.style.display = '';
      break;
  }

  switch(this.value_.pattern) {
    case 'Dots':
      this.shellPattern_.setAttribute('fill', 'url(#polkadots)');
      break;
    case 'Stripes':
      this.shellPattern_.setAttribute('fill', 'url(#stripes)');
      break;
    case 'Hexagons':
      this.shellPattern_.setAttribute('fill', 'url(#hexagons)');
      break;
  }

  this.textContent_.nodeValue = this.value_.turtleName;

  this.updateSize_();
}

Atualizando tamanho

A atualização da propriedade size_ de um campo é muito importante, porque informa ao código de renderização de bloco como posicionar o campo. A melhor maneira de descobrir exatamente qual precisa ser a size_ é fazendo testes.

updateSize_() {
  const bbox = this.movableGroup_.getBBox();
  let width = bbox.width;
  let height = bbox.height;
  if (this.borderRect_) {
    width += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2;
    height += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2;
    this.borderRect_.setAttribute('width', width);
    this.borderRect_.setAttribute('height', height);
  }
  // Note how both the width and the height can be dynamic.
  this.size_.width = width;
  this.size_.height = height;
}

Cor de blocos correspondente

Se você quiser que os elementos do seu campo correspondam às cores do bloco a que estão anexados, substitua o método applyColour. Acesse a cor usando a propriedade de estilo do bloco.

applyColour() {
  const sourceBlock = this.sourceBlock_;
  if (sourceBlock.isShadow()) {
    this.arrow_.style.fill = sourceBlock.style.colourSecondary;
  } else {
    this.arrow_.style.fill = sourceBlock.style.colourPrimary;
  }
}

Como atualizar a possibilidade de editar

A função updateEditable pode ser usada para alterar a forma como o campo é exibido, dependendo se ele é editável ou não. A função padrão faz com que o segundo plano tenha ou não uma resposta de passar o cursor (borda) se for ou não for editável. A tela no bloco não muda de tamanho dependendo da capacidade de edição, mas todas as outras mudanças são permitidas.

updateEditable() {
  if (!this.fieldGroup_) {
    // Not initialized yet.
    return;
  }
  super.updateEditable();

  const group = this.getClickTarget_();
  if (!this.isCurrentlyEditable()) {
    group.style.cursor = 'not-allowed';
  } else {
    group.style.cursor = this.CURSOR;
  }
}

Serialização

A serialização envolve salvar o estado do campo para que ele possa ser recarregado no espaço de trabalho mais tarde.

O estado do espaço de trabalho sempre inclui o valor do campo, mas também pode incluir outro estado, como o estado da interface dele. Por exemplo, se o campo fosse um mapa com zoom que permitia ao usuário selecionar países, também seria possível serializar o nível de zoom.

Caso seu campo seja serializável, defina a propriedade SERIALIZABLE como true.

O Blockly fornece dois conjuntos de hooks de serialização para os campos. Um par de hooks funciona com o novo sistema de serialização JSON, e o outro funciona com o antigo sistema de serialização XML.

saveState e loadState

saveState e loadState são hooks de serialização que funcionam com o novo sistema de serialização JSON.

Em alguns casos, você não precisa fornecê-las, porque as implementações padrão funcionam. Se (1) o campo for uma subclasse direta da classe base Blockly.Field, (2) o valor for um tipo JSON serializável e (3) você só precisar serializar o valor, a implementação padrão funcionará bem.

Caso contrário, a função saveState retornará um objeto/valor serializável JSON que representa o estado do campo. A função loadState precisa aceitar o mesmo objeto/valor serializável JSON e aplicá-lo ao campo.

saveState() {
  return {
    'country': this.getValue(),  // Value state
    'zoom': this.getZoomLevel(), // UI state
  };
}

loadState(state) {
  this.setValue(state['country']);
  this.setZoomLevel(state['zoom']);
}

Serialização completa e dados de backup

saveState também recebe um parâmetro opcional doFullSerialization. Isso é usado por campos que normalmente fazem referência ao estado serializado por um serializador diferente (como modelos de dados de apoio). O parâmetro sinaliza que o estado referenciado não estará disponível quando o bloco for desserializado. Portanto, o campo precisa fazer toda a serialização. Por exemplo, isso é verdadeiro quando um bloco individual é serializado ou quando um bloco é copiado e colado.

Dois casos de uso comuns para isso são:

  • Quando um bloco individual é carregado em um espaço de trabalho em que o modelo de dados de apoio não existe, o campo tem informações suficientes no próprio estado para criar um novo modelo de dados.
  • Quando um bloco é copiado e colado, o campo sempre cria um novo modelo de dados de apoio em vez de referenciar um existente.

Um campo que usa esse parâmetro é o campo de variável incorporada. Normalmente, ele serializa o ID da variável que está referenciando, mas se doFullSerialization for verdadeiro, ele serializa todo o estado.

saveState(doFullSerialization) {
  const state = {'id': this.variable_.getId()};
  if (doFullSerialization) {
    state['name'] = this.variable_.name;
    state['type'] = this.variable_.type;
  }
  return state;
}

loadState(state) {
  const variable = Blockly.Variables.getOrCreateVariablePackage(
      this.getSourceBlock().workspace,
      state['id'],
      state['name'],   // May not exist.
      state['type']);  // May not exist.
  this.setValue(variable.getId());
}

O campo de variável faz isso para garantir que, se for carregado em um espaço de trabalho em que a variável não existe, ele possa criar uma nova variável para fazer referência.

toXml e fromXml

toXml e fromXml são hooks de serialização que funcionam com o antigo sistema de serialização XML. Use esses hooks apenas se necessário (por exemplo, se você estiver trabalhando em uma base de código antiga que ainda não foi migrada). Caso contrário, use saveState e loadState.

A função toXml precisa retornar um nó XML que representa o estado do campo. E a função fromXml precisa aceitar o mesmo nó XML e aplicá-lo ao campo.

toXml(fieldElement) {
  fieldElement.textContent = this.getValue();
  fieldElement.setAttribute('zoom', this.getZoomLevel());
  return fieldElement;
}

fromXml(fieldElement) {
  this.setValue(fieldElement.textContent);
  this.setZoomLevel(fieldElement.getAttribute('zoom'));
}

Propriedades editáveis e serializáveis

A propriedade EDITABLE determina se o campo precisa ter uma interface para indicar que é possível interagir. O padrão é true.

A propriedade SERIALIZABLE determina se o campo precisa ser serializado. O padrão é false. Se essa propriedade for true, talvez seja necessário fornecer funções de serialização e desserialização. Consulte Serialização.

Como personalizar o cursor

A propriedade CURSOR determina o cursor que os usuários veem quando passam o cursor sobre seu campo. Ele precisa ser uma string de cursor CSS válida. O padrão é o cursor definido por .blocklyDraggable, que é o cursor de orientação.