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:
- Implementar um construtor.
- Registre uma chave JSON e implemente o
fromJson
. - Processar a inicialização da IU do bloco e dos listeners de eventos.
- Processar o descarte de listeners de eventos (o descarte da interface é feito para você).
- Implemente o processamento de valores.
- Adicione uma representação em texto do valor do campo para fins de acessibilidade.
- Adicione outras funcionalidades, como:
- 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:
- 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. - 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.
- 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
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_
comonewValue
. - Define a propriedade
isDirty_
comotrue
.
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.
DropDownDiv x WidgetDiv
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
.
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
.
Exemplo de código de DropDownDiv
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_
.
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:
- A criação do DOM precisa ser feita durante a inicialização, porque é mais eficiente.
- 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.