Antes de crear un nuevo tipo de campo, considera si uno de los otros métodos para personalizar campos se adapta a tus necesidades. Si tu aplicación necesita almacenar un nuevo tipo de valor o deseas crear una nueva IU para un tipo de valor existente, es probable que debas crear un nuevo tipo de campo.
Para crear un campo nuevo, haz lo siguiente:
- Implementa un constructor.
- Registra una clave JSON y, luego, implementa
fromJson
. - Controla la inicialización de la IU en el bloque y los objetos de escucha de eventos.
- Controla la eliminación de los objetos de escucha de eventos (la eliminación de la IU se controla por ti).
- Implementa el control de valores.
- Agrega una representación de texto del valor de tu campo para mejorar la accesibilidad.
- Agrega funciones adicionales, como las siguientes:
- Configura otros aspectos del campo, como los siguientes:
En esta sección, se supone que leíste y conoces el contenido de Anatomía de un campo.
Para ver un ejemplo de un campo personalizado, consulta la demostración de campos personalizados.
Cómo implementar un constructor
El constructor del campo es responsable de configurar el valor inicial del campo y, de manera opcional, configurar un validador local. Se llama al constructor del campo personalizado durante la inicialización del bloque fuente, independientemente de si el bloque fuente se define en JSON o JavaScript. Por lo tanto, el campo personalizado no tiene acceso al bloque fuente durante la construcción.
En el siguiente ejemplo de código, se crea un campo personalizado llamado GenericField
:
class GenericField extends Blockly.Field {
constructor(value, validator) {
super(value, validator);
this.SERIALIZABLE = true;
}
}
Firma del método
Por lo general, los constructores de campos toman un valor y un validador local. El valor es opcional y, si no pasas un valor (o pasas un valor que no supera la validación de clase), se usará el valor predeterminado de la superclase. Para la clase Field
predeterminada, ese valor es null
. Si no quieres ese valor predeterminado, asegúrate de pasar un valor adecuado. El parámetro de validador solo está presente para los campos editables y, por lo general, se marca como opcional. Obtén más información sobre los validadores en los documentos de validadores.
Estructura
La lógica dentro de tu constructor debe seguir este flujo:
- Llama al constructor superheredado (todos los campos personalizados deben heredarse de
Blockly.Field
o de una de sus subclases) para inicializar correctamente el valor y establecer el validador local para tu campo. - Si tu campo es serializable, establece la propiedad correspondiente en el constructor. Los campos editables deben ser serializables, y los campos son editables de forma predeterminada, por lo que probablemente deberías establecer esta propiedad como verdadera, a menos que sepas que no debería ser serializable.
- Opcional: Aplica personalización adicional (por ejemplo, los campos de etiqueta permiten pasar una clase CSS, que luego se aplica al texto).
JSON y registro
En las definiciones de bloques JSON, los campos se describen con una cadena (p.ej., field_number
, field_textinput
). Blockly mantiene un mapa de estas cadenas a los objetos de campo y llama a fromJson
en el objeto apropiado durante la construcción.
Llama a Blockly.fieldRegistry.register
para agregar tu tipo de campo a este mapa y pasar la clase de campo como segundo argumento:
Blockly.fieldRegistry.register('field_generic', GenericField);
También debes definir tu función fromJson
. Primero, tu implementación debe anular la referencia de cualquier referencia a tokens de localización con replaceMessageReferences
y, luego, pasar los valores al constructor.
GenericField.fromJson = function(options) {
const value = Blockly.utils.parsing.replaceMessageReferences(
options['value']);
return new CustomFields.GenericField(value);
};
Inicializando
Cuando se construye tu campo, básicamente solo contiene un valor. La inicialización es el punto en el que se compila el DOM, se compila el modelo (si el campo posee un modelo) y se vinculan los eventos.
Pantalla On-Block
Durante la inicialización, eres responsable de crear todo lo que necesites para la visualización del campo en el bloque.
Valores predeterminados, fondo y texto
La función initView
predeterminada crea un elemento rect
de color claro y un elemento text
. Si quieres que tu campo tenga ambos, además de algunos extras, llama a la función de superclase initView
antes de agregar el resto de los elementos DOM. Si quieres que tu campo tenga uno de estos elementos, pero no ambos, puedes usar las funciones createBorderRect_
o createTextElement_
.
Personalización de la construcción del DOM
Si tu campo es un campo de texto genérico (p.ej., Entrada de texto), se controlará la construcción del DOM por ti. De lo contrario, deberás anular la función initView
para crear los elementos DOM que necesitarás durante la renderización futura de tu campo.
Por ejemplo, un campo desplegable puede contener imágenes y texto. En initView
, crea un solo elemento de imagen y un solo elemento de texto. Luego, durante render_
, muestra el elemento activo y oculta el otro, según el tipo de opción seleccionada.
Los elementos del DOM se pueden crear con el método Blockly.utils.dom.createSvgElement
o con los métodos tradicionales de creación del DOM.
Los requisitos para la visualización en bloque de un campo son los siguientes:
- Todos los elementos del DOM deben ser secundarios del
fieldGroup_
del campo. El grupo de campos se crea automáticamente. - Todos los elementos del DOM deben permanecer dentro de las dimensiones informadas del campo.
Consulta la sección Renderización para obtener más detalles sobre cómo personalizar y actualizar la pantalla en el bloque.
Cómo agregar símbolos de texto
Si deseas agregar símbolos al texto de un campo (como el símbolo de grados del campo Ángulo), puedes agregar el elemento de símbolo (por lo general, contenido en un <tspan>
) directamente al textElement_
del campo.
Eventos de entrada
De forma predeterminada, los campos registran eventos de sugerencias y eventos de mousedown (para mostrar editores).
Si deseas escuchar otros tipos de eventos (p.ej., si deseas controlar el arrastre en un campo), debes anular la función bindEvents_
del 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 vincularte a un evento, por lo general, debes usar la función Blockly.utils.browserEvents.conditionalBind
. Este método de vinculación de eventos filtra los toques secundarios durante los arrastres. Si quieres que tu controlador se ejecute incluso en medio de un arrastre en curso, puedes usar la función Blockly.browserEvents.bind
.
Eliminación
Si registraste algún objeto de escucha de eventos personalizado dentro de la función bindEvents_
del campo, deberás anular su registro dentro de la función dispose
.
Si inicializaste correctamente la vista de tu campo (agregando todos los elementos DOM a fieldGroup_
), el DOM del campo se desechará automáticamente.
Manejo de valores
→ Para obtener información sobre el valor de un campo en comparación con su texto, consulta Anatomía de un campo.
Orden de validación
Cómo implementar un validador de clase
Los campos solo deben aceptar ciertos valores. Por ejemplo, los campos numéricos solo deben aceptar números, los campos de color solo deben aceptar colores, etcétera. Esto se garantiza a través de validadores locales y de clase. El validador de clase sigue las mismas reglas que los validadores locales, excepto que también se ejecuta en el constructor y, como tal, no debe hacer referencia al bloque de origen.
Para implementar el validador de clase de tu campo, anula la función doClassValidation_
.
doClassValidation_(newValue) {
if (typeof newValue != 'string') {
return null;
}
return newValue;
};
Controla valores válidos
Si el valor que se pasa a un campo con setValue
es válido, recibirás una devolución de llamada doValueUpdate_
. De forma predeterminada, la función doValueUpdate_
hace lo siguiente:
- Establece la propiedad
value_
ennewValue
. - Establece la propiedad
isDirty_
entrue
.
Si solo necesitas almacenar el valor y no quieres realizar ningún control personalizado, no es necesario que anules doValueUpdate_
.
De lo contrario, si deseas realizar acciones como las siguientes:
- Almacenamiento personalizado de
newValue
. - Cambia otras propiedades según
newValue
. - Guarda si el valor actual es válido o no.
Deberás anular doValueUpdate_
:
doValueUpdate_(newValue) {
super.doValueUpdate_(newValue);
this.displayValue_ = newValue;
this.isValueValid_ = true;
}
Cómo controlar valores no válidos
Si el valor que se pasa al campo con setValue
no es válido, recibirás una devolución de llamada doValueInvalid_
. De forma predeterminada, la función doValueInvalid_
no hace nada. Esto significa que, de forma predeterminada, no se mostrarán los valores no válidos. También significa que el campo no se volverá a renderizar, ya que no se establecerá la propiedad isDirty_
.
Si deseas mostrar valores no válidos, debes anular doValueInvalid_
.
En la mayoría de los casos, debes establecer una propiedad displayValue_
en el valor no válido, establecer isDirty_
en true
y anular render_ para que la pantalla en el bloque se actualice según el displayValue_
en lugar del value_
.
doValueInvalid_(newValue) {
this.displayValue_ = newValue;
this.isDirty_ = true;
this.isValueValid_ = false;
}
Valores de varias partes
Cuando tu campo contiene un valor de varias partes (p.ej., listas, vectores, objetos), es posible que desees que las partes se controlen como valores individuales.
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;
}
En el ejemplo anterior, cada propiedad de newValue
se valida de forma individual. Luego, al final de la función doClassValidation_
, si alguna propiedad individual no es válida, el valor se almacena en caché en la propiedad cacheValidatedValue_
antes de devolver null
(no válido). Al almacenar en caché el objeto con propiedades validadas de forma individual, la función doValueInvalid_
puede controlarlas por separado con solo realizar una verificación de !this.cacheValidatedValue_.property
, en lugar de volver a validar cada propiedad de forma individual.
Este patrón para validar valores de varias partes también se puede usar en validadores locales, pero, actualmente, no hay forma de aplicarlo.
isDirty_
isDirty_
es una marca que se usa en la función setValue
, así como en otras partes del campo, para indicar si el campo necesita volver a renderizarse. Si cambió el valor de visualización del campo, isDirty_
suele establecerse en true
.
Texto
→ Para obtener información sobre dónde se usa el texto de un campo y en qué se diferencia del valor del campo, consulta Anatomía de un campo.
Si el texto de tu campo es diferente del valor de tu campo, debes anular la función getText
para proporcionar el texto correcto.
getText() {
let text = this.value_.turtleName + ' wearing a ' + this.value_.hat;
if (this.value_.hat == 'Stovepipe' || this.value_.hat == 'Propeller') {
text += ' hat';
}
return text;
}
Cómo crear un editor
Si defines la función showEditor_
, Blockly escuchará automáticamente los clics y llamará a showEditor_
en el momento adecuado. Puedes mostrar cualquier código HTML en tu editor envolviéndolo en uno de los dos div
especiales, llamados DropDownDiv
y WidgetDiv
, que flotan sobre el resto de la IU de Blockly.
DropDownDiv vs. WidgetDiv
El DropDownDiv
se usa para proporcionar editores que se encuentran dentro de un cuadro conectado a un campo. Se posiciona automáticamente cerca del campo y permanece dentro de los límites visibles. El selector de ángulo y el selector de color son buenos ejemplos del DropDownDiv
.
El WidgetDiv
se usa para proporcionar editores que no se encuentran dentro de una caja. Los campos de número usan WidgetDiv
para cubrir el campo con un cuadro de entrada de texto HTML. Si bien el DropDownDiv
controla la posición por ti, el WidgetDiv
no lo hace. Los elementos deberán posicionarse de forma manual. El sistema de coordenadas se encuentra en coordenadas de píxeles relativas a la esquina superior izquierda de la ventana. El editor de entrada de texto es un buen ejemplo de WidgetDiv
.
Código de muestra 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));
}
Código de muestra 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);
}
Realiza una limpieza
Tanto DropDownDiv
como WidgetDiv
controlan la destrucción de los elementos HTML del widget, pero debes desechar manualmente los objetos de escucha de eventos que hayas aplicado a esos elementos.
widgetDispose_() {
for (let i = this.editorListeners_.length, listener;
listener = this.editorListeners_[i]; i--) {
Blockly.browserEvents.unbind(listener);
this.editorListeners_.pop();
}
}
Se llama a la función dispose
en un contexto null
en el DropDownDiv
. En WidgetDiv
, se llama en el contexto de WidgetDiv
. En cualquier caso, es mejor usar la función bind cuando se pasa una función de descarte, como se muestra en los ejemplos anteriores de DropDownDiv
y WidgetDiv
.
→ Para obtener información sobre la eliminación que no es específica para la eliminación de editores, consulta Eliminación.
Actualización de la pantalla en el bloque
La función render_
se usa para actualizar la visualización en bloque del campo y que coincida con su valor interno.
A continuación, se muestran algunos ejemplos comunes:
- Cambiar el texto (menú desplegable)
- Cambiar el color (color)
Valores predeterminados
La función render_
predeterminada establece el texto visible en el resultado de la función getDisplayText_
. La función getDisplayText_
devuelve la propiedad value_
del campo convertida en una cadena, después de que se haya truncado para respetar la longitud máxima del texto.
Si usas la pantalla predeterminada en bloque y el comportamiento de texto predeterminado funciona para tu campo, no es necesario que anules render_
.
Si el comportamiento de texto predeterminado funciona para tu campo, pero la visualización en bloque del campo tiene elementos estáticos adicionales, puedes llamar a la función render_
predeterminada, pero deberás anularla para actualizar el tamaño del campo.
Si el comportamiento de texto predeterminado no funciona para tu campo o la visualización en bloque de tu campo tiene elementos dinámicos adicionales, deberás personalizar la función render_
.
Cómo personalizar la renderización
Si el comportamiento de renderización predeterminado no funciona para tu campo, deberás definir un comportamiento de renderización personalizado. Esto puede implicar desde establecer texto de visualización personalizado hasta cambiar elementos de imagen y actualizar colores de fondo.
Todos los cambios en los atributos del DOM son válidos. Solo debes recordar dos cosas:
- La creación del DOM se debe controlar durante la inicialización, ya que es más eficiente.
- Siempre debes actualizar la propiedad
size_
para que coincida con el tamaño de la pantalla en el bloque.
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_();
}
Actualización del tamaño
Actualizar la propiedad size_
de un campo es muy importante, ya que le indica al código de renderización de bloques cómo posicionar el campo. La mejor manera de saber exactamente qué debe ser ese size_
es experimentar.
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;
}
Colores de bloques coincidentes
Si deseas que los elementos de tu campo coincidan con los colores del bloque al que están unidos, debes anular el método applyColour
. Querrás acceder al color a través de la propiedad de estilo del bloque.
applyColour() {
const sourceBlock = this.sourceBlock_;
if (sourceBlock.isShadow()) {
this.arrow_.style.fill = sourceBlock.style.colourSecondary;
} else {
this.arrow_.style.fill = sourceBlock.style.colourPrimary;
}
}
Cómo actualizar la capacidad de edición
La función updateEditable
se puede usar para cambiar la forma en que aparece tu campo según si es editable o no. La función predeterminada hace que el fondo tenga o no una respuesta de desplazamiento (borde) si es o no editable.
La pantalla en el bloque no debe cambiar de tamaño según su capacidad de edición, pero se permiten todos los demás cambios.
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;
}
}
Serialización
La serialización consiste en guardar el estado de tu campo para que se pueda volver a cargar en el espacio de trabajo más adelante.
El estado de tu espacio de trabajo siempre incluye el valor del campo, pero también podría incluir otro estado, como el de la IU del campo. Por ejemplo, si tu campo fuera un mapa con zoom que le permitiera al usuario seleccionar países, también podrías serializar el nivel de zoom.
Si tu campo es serializable, debes establecer la propiedad SERIALIZABLE
en true
.
Blockly proporciona dos conjuntos de hooks de serialización para los campos. Un par de hooks funciona con el nuevo sistema de serialización JSON, y el otro par funciona con el sistema de serialización XML anterior.
saveState
y loadState
saveState
y loadState
son hooks de serialización que funcionan con el nuevo sistema de serialización de JSON.
En algunos casos, no es necesario que proporciones estos datos, ya que las implementaciones predeterminadas funcionarán. Si (1) tu campo es una subclase directa de la clase base Blockly.Field
, (2) tu valor es un tipo serializable en JSON y (3) solo necesitas serializar el valor, la implementación predeterminada funcionará perfectamente.
De lo contrario, tu función saveState
debe devolver un objeto o valor serializable en JSON que represente el estado del campo. Además, tu función loadState
debe aceptar el mismo objeto o valor serializable en JSON y aplicarlo al campo.
saveState() {
return {
'country': this.getValue(), // Value state
'zoom': this.getZoomLevel(), // UI state
};
}
loadState(state) {
this.setValue(state['country']);
this.setZoomLevel(state['zoom']);
}
Datos de respaldo y serialización completos
saveState
también recibe un parámetro opcional doFullSerialization
. Los campos que normalmente hacen referencia a un estado serializado por un serializador diferente (como los modelos de datos de respaldo) usan este campo. El parámetro indica que el estado al que se hace referencia no estará disponible cuando se deserialice el bloque, por lo que el campo debe realizar toda la serialización por sí mismo. Por ejemplo, esto sucede cuando se serializa un bloque individual o cuando se copia y pega un bloque.
Estos son dos casos de uso comunes:
- Cuando se carga un bloque individual en un espacio de trabajo en el que no existe el modelo de datos de respaldo, el campo tiene suficiente información en su propio estado para crear un nuevo modelo de datos.
- Cuando se copia y pega un bloque, el campo siempre crea un nuevo modelo de datos de respaldo en lugar de hacer referencia a uno existente.
Un campo que usa esto es el campo de variables integrado. Normalmente, serializa el ID de la variable a la que hace referencia, pero, si doFullSerialization
es verdadero, serializa todo su 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());
}
El campo de variables hace esto para asegurarse de que, si se carga en un espacio de trabajo en el que no existe su variable, pueda crear una nueva variable a la que hacer referencia.
toXml
y fromXml
toXml
y fromXml
son hooks de serialización que funcionan con el sistema de serialización XML anterior. Solo usa estos hooks si es necesario (p. ej., si trabajas en una base de código antigua que aún no se migró). De lo contrario, usa saveState
y loadState
.
Tu función toXml
debe devolver un nodo XML que represente el estado del campo. Además, tu función fromXml
debe aceptar el mismo nodo XML y aplicarlo al 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'));
}
Propiedades editables y serializables
La propiedad EDITABLE
determina si el campo debe tener una IU para indicar que se puede interactuar con él. El valor predeterminado es true
.
La propiedad SERIALIZABLE
determina si el campo debe serializarse. El valor predeterminado es false
. Si esta propiedad es true
, es posible que debas proporcionar funciones de serialización y deserialización (consulta Serialización).
Personalización con CSS
Puedes personalizar tu campo con CSS. En el método initView
, agrega una clase personalizada al fieldGroup_
de tu campo y, luego, haz referencia a esta clase en tu CSS.
Por ejemplo, para usar un cursor diferente, haz lo siguiente:
initView() {
...
// Add a custom CSS class.
if (this.fieldGroup_) {
Blockly.utils.dom.addClass(this.fieldGroup_, 'myCustomField');
}
}
.myCustomField {
cursor: cell;
}
Cómo personalizar el cursor
De forma predeterminada, las clases que extienden FieldInput
usan un cursor text
cuando un usuario coloca el cursor sobre el campo, los campos que se arrastran usan un cursor grabbing
y todos los demás campos usan un cursor default
. Si quieres usar un cursor diferente, configúralo con CSS.