Crea un campo personalizado

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:

  1. Implementa un constructor.
  2. Registra una clave JSON y, luego, implementa fromJson.
  3. Controla la inicialización de la IU en el bloque y los objetos de escucha de eventos.
  4. Controla la eliminación de los objetos de escucha de eventos (la eliminación de la IU se controla por ti).
  5. Implementa el control de valores.
  6. Agrega una representación de texto del valor de tu campo para mejorar la accesibilidad.
  7. Agrega funciones adicionales, como las siguientes:
  8. 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:

  1. 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.
  2. 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.
  3. 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

Diagrama de flujo que describe el orden en que se ejecutan los validadores

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_ en newValue.
  • Establece la propiedad isDirty_ en true.

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.

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.

Imagen del selector de ángulo

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.

Imagen del 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 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_.

Diagrama de flujo que describe cómo tomar la decisión de anular 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:

  1. La creación del DOM se debe controlar durante la inicialización, ya que es más eficiente.
  2. 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.