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