Criar um campo personalizado

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

Para criar um campo, faça o seguinte:

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

Esta seção pressupõe que você leu e está familiarizado com o conteúdo em Anatomia de um campo.

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

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, independentemente de ele ser 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 de 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 falha 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 em campos editáveis e geralmente é marcado como opcional. Saiba mais sobre validadores nos documentos de validadores.

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 de uma das subclasses) para inicializar corretamente o valor e definir o validador local para o 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, é recomendável definir essa propriedade como verdadeira, a menos que você saiba que ela 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). 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);

Você também precisa definir a função fromJson. Sua implementação precisa primeiro remover as referências aos tokens de localização 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 o campo é criado, 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.

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

Como personalizar a construção de DOM

Se o campo for um campo de texto genérico (por exemplo, Text Input), a construção do DOM será processada para você. Caso contrário, será necessário 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.

A criação de elementos DOM pode ser feita usando o método Blockly.utils.dom.createSvgElement ou os métodos tradicionais de criação de DOM.

Os requisitos da exibição no 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 ficar dentro das dimensões informadas do campo.

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

Como 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 de mousedown (para serem usados na exibição de editores). Se você quiser detectar outros tipos de eventos (por exemplo, se quiser processar o arrastamento 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 é necessário usar a função Blockly.utils.browserEvents.conditionalBind. Esse método de vinculação de eventos filtra toques secundários durante arrastar. Se você quiser que o gerenciador seja executado mesmo durante um arrasto em andamento, use a função Blockly.browserEvents.bind.

Descarte

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

Se você inicializar 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 em comparação com o texto, consulte Anatomia de um campo.

Ordem de validação

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

Como implementar um validador de classe

Os campos só podem aceitar determinados valores. Por exemplo, campos numéricos devem aceitar apenas números, campos de cores devem aceitar apenas cores etc. Isso é garantido por validadores de classe e locais. O validador de classe segue as mesmas regras que os validadores locais, exceto que também é executado no construtor e, portanto, não pode 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 para 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ó precisa armazenar o valor e não quer 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.
  • Salvar se o valor atual é válido ou não.

É necessário substituir doValueUpdate_:

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

Como processar valores inválidos

Se o valor transmitido para o 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, os valores inválidos não sã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 override render_ para que a exibição no bloco seja atualizada com base no displayValue_ em vez do value_.

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

Valores com 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 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 qualquer propriedade individual for inválida, o valor será armazenado em cache na propriedade cacheValidatedValue_ antes de retornar null (inválido). Armazenar em cache o objeto com propriedades validadas individualmente permite que a função doValueInvalid_ as processe separadamente, simplesmente fazendo uma verificação !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 no momento 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, o 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 apropriado. É possível exibir qualquer HTML no editor envolvendo-o em um dos dois divs especiais, chamados 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 para ficar perto do campo, permanecendo dentro dos limites visíveis. O seletor de ângulo e o seletor de cores são bons exemplos de DropDownDiv.

Imagem do seletor de ângulo

O WidgetDiv é usado para fornecer editores que não estão dentro de uma caixa. Os campos numéricos usam o WidgetDiv para cobrir o campo com uma caixa de entrada de texto HTML. Enquanto o DropDownDiv processa o posicionamento para você, o WidgetDiv não. Os elementos precisam ser posicionados manualmente. O sistema de coordenadas está em coordenadas de pixel em relação ao canto superior esquerdo da janela. O editor de entrada de texto é um bom exemplo de 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));
}

Código de exemplo do 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

O DropDownDiv e o WidgetDiv lidam com a destruição dos elementos HTML do widget, mas você precisa descartar manualmente todos 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 ambos os casos, é 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 a eliminação não específica de editores, consulte Eliminação.

Como atualizar a tela no bloco

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

São exemplos comuns:

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

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 convertido em uma string, depois de ter sido truncada para respeitar o comprimento máximo de 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 padrão do texto funcionar para seu campo, mas a exibição no bloco do campo tiver outros elementos estáticos, chame a função render_ padrão, mas ainda será necessário substituí-la para atualizar o tamanho do campo.

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

Fluxograma que descreve como tomar a decisão de 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 de renderização personalizado. Isso pode envolver desde a configuração de texto de exibição personalizado até a alteração de elementos de imagem e a atualização de cores de plano de fundo.

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

  1. A criação do DOM precisa ser processada durante a inicialização, porque é mais eficiente.
  2. Sempre atualize a propriedade size_ para que corresponda 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_();
}

Como atualizar o 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 size_ precisa 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;
}

Cores de bloco correspondentes

Se você quiser que os elementos do campo correspondam às cores do bloco a que estão anexados, substitua o método applyColour. Você vai precisar acessar a cor pela 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 editabilidade

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 plano de fundo tenha/não tenha uma resposta de passar o cursor (borda) se ele for/não for editável. A exibição 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 serve para salvar 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 estado 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 ganchos 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 ganchos de serialização que funcionam com o novo sistema de serialização JSON.

Em alguns casos, não é necessário fornecer essas informações, porque as implementações padrão vão funcionar. Se (1) o campo for uma subclasse direta da classe Blockly.Field, (2) o valor for um tipo serializável em JSON e (3) você só precisar serializar o valor, a implementação padrão vai funcionar perfeitamente.

Caso contrário, a função saveState precisa retornar um objeto/valor serializável em JSON que represente o estado do campo. E 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. Ele é 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 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 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 campo de variável integrado. 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 for carregado em um espaço de trabalho em que a variável não existe, ele possa criar uma nova variável para referência.

toXml e fromXml

toXml e fromXml são ganchos de serialização que funcionam com o antigo sistema de serialização de XML. Use esses hooks apenas quando necessário (por exemplo, se você estiver trabalhando em um código 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 ele pode ser usado. 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 o campo. Ele precisa ser uma string de cursor CSS válida. O padrão é o cursor definido por .blocklyDraggable, que é o cursor de captura.