Criar um campo personalizado

Antes de criar um novo tipo de campo, considere se um dos outros métodos de personalização atende às suas necessidades. Se o aplicativo precisar armazenar um novo tipo de valor ou se você quiser criar uma nova interface para um tipo de valor atual, provavelmente será necessário criar um novo tipo de campo.

Para criar um campo, faça o seguinte:

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

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

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

Implementar um construtor

O construtor do campo é responsável por definir 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, independente de o bloco de origem ser definido em JSON ou JavaScript. Portanto, o campo personalizado não tem acesso ao bloco de origem durante a construção.

A amostra 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

Os construtores de campo geralmente recebem um valor e um validador local. O valor é opcional. Se você não transmitir um valor ou transmitir um valor que falhe na validação de 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 de validação só está presente em campos editáveis e geralmente é marcado como opcional. Saiba mais sobre validadores na documentação de validadores.

Estrutura

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

  1. Chame o superconstrutor herdado (todos os campos personalizados precisam herdar de Blockly.Field ou uma das subclasses dele) 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 eles são editáveis por padrão. Portanto, defina essa propriedade como "true" a menos que você saiba que ela não deve ser serializável.
  3. Opcional: aplique mais personalizações. Por exemplo, Campos de rótulo permitem que uma classe CSS seja transmitida e aplicada ao texto.

JSON e registro

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

Chame Blockly.fieldRegistry.register para adicionar o 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. Sua implementação precisa primeiro remover a referência de tokens de localização usando replaceMessageReferences e, em seguida, transmitir os valores ao construtor.

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

Inicializando

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

Display na tela de bloqueio

Durante a inicialização, você é responsável por criar tudo o que for necessário para a exibição do campo no bloco.

Padrões, segundo plano 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 os dois, além de alguns extras, chame a função initView da superclasse antes de adicionar o restante dos elementos DOM. Se você quiser que seu campo tenha um, mas não os dois, desses elementos, use as funções createBorderRect_ ou createTextElement_.

Como personalizar a construção do DOM

Se o campo for um campo de texto genérico (por exemplo, Entrada de texto), a construção do DOM será processada para você. Caso contrário, será necessário substituir a função initView para criar os elementos do 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. Depois, durante render_, ele mostra o elemento ativo e oculta o outro, com base no tipo da opção selecionada.

É possível criar elementos DOM usando o método Blockly.utils.dom.createSvgElement ou métodos tradicionais de criação de DOM.

Os requisitos para a exibição on-block 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 do DOM precisam permanecer dentro das dimensões informadas do campo.

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

Adicionar símbolos de texto

Se você quiser adicionar símbolos ao texto de um campo (como o símbolo de grau do campo Ângulo), anexe o elemento de símbolo (geralmente contido em um <tspan>) diretamente ao textElement_ do campo.

Eventos de entrada

Por padrão, os campos registram eventos de dica e mousedown (para mostrar editores). Se você quiser detectar outros tipos de eventos (por exemplo, se quiser processar arrastar em um campo), substitua 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 use a função Blockly.utils.browserEvents.conditionalBind. Esse método de vinculação de eventos filtra toques secundários durante arrastos. Se você quiser que o manipulador 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 na função bindEvents_ do campo, eles precisam ser cancelados na 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.

Tratamento de valores

→ Para informações sobre o valor de um campo x o texto dele, consulte Anatomia de um campo.

Ordem de validação

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

Implementar um validador de classe

Os campos só podem aceitar determinados valores. Por exemplo, campos numéricos só podem aceitar números, campos de cor só podem aceitar cores etc. Isso é garantido por validadores de classe e locais. O validador de classe segue as mesmas regras dos validadores locais, exceto que também é executado no construtor e, portanto, não deve fazer referência ao bloco de origem.

Para implementar o validador de classe do 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 a um campo com setValue for válido, você vai 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ó precisar armazenar o valor e não quiser fazer nenhum processamento personalizado, não é necessário substituir doValueUpdate_.

Caso contrário, se você quiser fazer coisas como:

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

Você precisará substituir doValueUpdate_:

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

Como processar 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 mostrar valores inválidos, substitua doValueInvalid_. Na maioria das circunstâncias, defina uma propriedade displayValue_ como o valor inválido, defina isDirty_ como true e substitua render_ para que a exibição no bloco seja atualizada com base em displayValue_ em vez de value_.

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

Valores de várias partes

Quando o campo contém um valor de várias partes (por exemplo, listas, vetores, objetos), talvez você queira que as partes sejam processadas 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). O armazenamento em cache do objeto com propriedades validadas individualmente permite que a função doValueInvalid_ as processe separadamente, basta fazer 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 aplicar esse padrão.

isDirty_

isDirty_ é uma flag usada na função setValue e em outras partes do campo para informar se ele precisa ser renderizado novamente. Se o valor de exibição do campo tiver mudado, isDirty_ geralmente será definido como true.

Texto

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

Se o texto do campo for diferente do valor dele, substitua 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 vai detectar automaticamente cliques e chamar showEditor_ no momento adequado. Você pode mostrar qualquer HTML no editor ao envolvê-lo em uma de duas divs especiais, chamadas DropDownDiv e WidgetDiv, que flutuam acima do restante da interface do Blockly.

O DropDownDiv é usado para fornecer editores que ficam dentro de uma caixa conectada a um campo. Ele se posiciona automaticamente perto do campo, mas dentro dos limites visíveis. O seletor de ângulo e o seletor de cores são bons exemplos da 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 o WidgetDiv para cobrir o campo com uma caixa de entrada de texto HTML. Embora a DropDownDiv faça o posicionamento para você, a WidgetDiv não faz. Os elementos precisam 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 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

As classes DropDownDiv e 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 no DropDownDiv. No WidgetDiv, ele é chamado no contexto do WidgetDiv. Em qualquer caso, é melhor usar a função bind ao transmitir uma função de descarte, conforme mostrado nos exemplos DropDownDiv e WidgetDiv acima.

→ Para informações sobre descarte que não sejam específicas para editores, consulte Descarte.

Atualizar a exibição no bloco

A função render_ é usada para atualizar a exibição em bloco do campo e corresponder ao valor interno dele.

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 de ser truncada para respeitar o comprimento máximo do texto.

Se você estiver usando a exibição 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 do campo tiver outros elementos estáticos, você poderá chamar a função padrão render_, mas ainda precisará substituir para atualizar o tamanho do campo.

Se o comportamento de texto padrão não funcionar para seu campo ou se a exibição on-block do campo tiver outros elementos dinâmicos, será necessário personalizar a função render_.

Fluxograma descrevendo como decidir se é necessário substituir render_

Personalizar a renderização

Se o comportamento de renderização padrão não funcionar para seu campo, será necessário definir um comportamento personalizado. Isso pode envolver qualquer coisa, desde definir um texto de exibição personalizado até mudar elementos de imagem e atualizar cores de plano de fundo.

Todas as mudanças de atributos do DOM são válidas. As únicas duas coisas a serem lembradas são:

  1. A criação do DOM precisa ser processada durante a inicialização, já que é mais eficiente.
  2. Sempre atualize a propriedade size_ para corresponder ao tamanho da exibição 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

Atualizar a propriedade size_ de um campo é muito importante, porque informa ao código de renderização do bloco como posicionar o campo. A melhor maneira de descobrir exatamente o que esse size_ deve ser é testando.

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

Combinar cores de blocos

Se você quiser que os elementos do 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;
  }
}

Atualizar a capacidade de edição

A função updateEditable pode ser usada para mudar a aparência do campo, 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 ele for ou não editável. A exibição no bloco não pode mudar 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 salva o estado do campo para que ele possa ser recarregado no espaço de trabalho mais tarde.

O estado do seu espaço de trabalho sempre inclui o valor do campo, mas também pode incluir outros estados, como o da interface do campo. Por exemplo, se o campo for um mapa com zoom que permite ao usuário selecionar países, você também poderá serializar o nível de zoom.

Se o campo for serializável, defina a propriedade SERIALIZABLE como true.

O Blockly oferece dois conjuntos de hooks de serialização para campos. Um par de hooks funciona com o novo sistema de serialização JSON, e o outro par 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, não é necessário fornecer esses dados, porque as implementações padrão funcionam. Se (1) seu campo for uma subclasse direta da classe base Blockly.Field, (2) seu valor for um tipo serializável em JSON e (3) você só precisar serializar o valor, a implementação padrão vai funcionar bem.

Caso contrário, a função saveState vai retornar um objeto/valor serializável em JSON que representa o estado do campo. A função loadState precisa aceitar o mesmo objeto/valor serializável em 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 apoio

saveState também recebe um parâmetro opcional doFullSerialization. Isso é usado por campos que normalmente referenciam o estado serializado por um serializador diferente (como modelos de dados de suporte). O parâmetro indica que o estado referenciado não estará disponível quando o bloco for desserializado. Portanto, o campo precisa fazer toda a serialização por conta própria. Por exemplo, isso acontece quando um bloco individual é serializado ou quando um bloco é copiado e colado.

Dois casos de uso comuns são:

  • Quando um bloco individual é carregado em um espaço de trabalho em que o modelo de dados de suporte 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 suporte em vez de referenciar um existente.

Um campo que usa isso é o de variável incorporada. Normalmente, ele serializa o ID da variável a que está fazendo referência, mas se doFullSerialization for verdadeiro, ele serializará 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 ele for carregado em um espaço de trabalho em que a variável não existe, uma nova variável possa ser criada para 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 for necessário (por exemplo, se você estiver trabalhando em um codebase antigo que ainda não foi migrado). Caso contrário, use saveState e loadState.

A função toXml precisa retornar um nó XML que represente o estado do campo. 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 com ele. 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).

Personalização com CSS

É possível personalizar o campo com CSS. No método initView, adicione uma classe personalizada ao fieldGroup_ do campo e faça referência a essa classe no CSS.

Por exemplo, para usar um cursor diferente:

initView() {
  ...

  // Add a custom CSS class.
  if (this.fieldGroup_) {
    Blockly.utils.dom.addClass(this.fieldGroup_, 'myCustomField');
  }
}
.myCustomField {
  cursor: cell;
}

Como personalizar o cursor

Por padrão, as classes que estendem FieldInput usam um cursor text quando um usuário passa o cursor sobre o campo. Os campos arrastados usam um cursor grabbing, e todos os outros campos usam um cursor default. Se quiser usar um cursor diferente, defina-o usando CSS.