Crea un nuevo tipo de campo

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 tipo de valor nuevo o si deseas crear una IU nueva para un tipo de valor existente, es probable que debas crear un tipo de campo nuevo.

Para crear un nuevo campo, 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 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 automáticamente).
  5. Implementa el manejo de valores.
  6. Agrega una representación de texto del valor de tu campo para fines de accesibilidad.
  7. Agrega funciones adicionales, como las siguientes:
  8. Configura aspectos adicionales de tu campo, como los siguientes:

En esta sección, se supone que leíste el contenido de Anatomía de un campo y que estás familiarizado con él.

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, opcionalmente, un validador local. Se llama al constructor del campo personalizado durante la inicialización del bloque de origen, independientemente de si el bloque de origen está definido en JSON o JavaScript. Por lo tanto, el campo personalizado no tiene acceso al bloque de origen durante la construcción.

En la siguiente muestra 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

Los constructores de campo generalmente aceptan un valor y un validador local. El valor es opcional y, si no pasas un valor (o pasas uno que no pasa la validación de clase), se usará el valor predeterminado de la superclase. Para la clase Field predeterminada, ese valor es null. Si no deseas ese valor predeterminado, asegúrate de pasar un valor adecuado. El parámetro del validador solo está presente para los campos editables y, por lo general, está marcado como opcional. Obtén más información sobre los validadores en la documentación sobre validadores.

Estructura

La lógica dentro de tu constructor debería seguir este flujo:

  1. Llama al superconstructor heredado (todos los campos personalizados deben heredarse de Blockly.Field o una de sus subclases) para inicializar correctamente el valor y configurar el validador local para tu campo.
  2. Si tu campo se puede serializar, configura la propiedad correspondiente en el constructor. Los campos editables se deben serializar, y estos se pueden editar de forma predeterminada, por lo que es probable que debas configurar esta propiedad como verdadera, a menos que sepas que no debería ser serializable.
  3. Opcional: Aplica personalizaciones adicionales (por ejemplo, los campos de etiqueta permiten que se pase una clase CSS, que luego se aplica al texto).

JSON y registro

En las definiciones de bloques JSON, los campos se describen con una string (p.ej., field_number, field_textinput). Blockly mantiene un mapa de estas strings a objetos de campo y llama a fromJson en el objeto adecuado durante la construcción.

Llama a Blockly.fieldRegistry.register para agregar tu tipo de campo a este mapa y pasa la clase del campo como el segundo argumento:

Blockly.fieldRegistry.register('field_generic', GenericField);

También debes definir tu función fromJson. Primero, la implementación debe anular la referencia de cualquier tabla de strings mediante 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);
};

Se está inicializando

Cuando se construye tu campo, básicamente solo contiene un valor. En la inicialización, se crea el DOM y el modelo (si el campo posee un modelo) y los eventos se vinculan.

Pantalla de bloqueo

Durante la inicialización, eres responsable de crear todo lo que necesites para la visualización en bloque del campo.

Valores predeterminados, fondo y texto

La función initView predeterminada crea un elemento rect de color claro y un elemento text. Si deseas que tu campo tenga ambos elementos, además de accesorios adicionales, llama a la función initView de la superclase antes de agregar el resto de los elementos del DOM. Si deseas que tu campo tenga solo uno de estos elementos, pero no ambos, puedes usar las funciones createBorderRect_ o createTextElement_.

Cómo personalizar 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 del DOM que necesitarás durante el procesamiento futuro 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 la opción seleccionada.

La creación de elementos del DOM se puede hacer con el método Blockly.utils.dom.createSvgElement o con los métodos tradicionales de creación de DOM.

Los requisitos de 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 a fin de obtener más detalles para personalizar y actualizar la pantalla bloqueada.

Cómo agregar símbolos de texto

Si quieres agregar símbolos al texto de un campo (como el símbolo de grados del campo Angle), puedes agregar el elemento de símbolo (por lo general, se encuentra en un <tspan>) directamente al textElement_ del campo.

Eventos de entrada

De forma predeterminada, los campos registran eventos de información sobre la herramienta y eventos de mousedown (se usan para mostrar editores). Si deseas escuchar otros tipos de eventos (p.ej., si deseas controlar los arrastres 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;
      }
  );
}

Por lo general, para vincular a un evento, 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 deseas que el controlador se ejecute incluso en medio de un arrastre en curso, puedes usar la función Blockly.browserEvents.bind.

Desechando

Si registraste objetos de escucha de eventos personalizados en la función bindEvents_ del campo, deberás anular su registro en la función dispose.

Si inicializaste la vista de tu campo correctamente (agregando todos los elementos del DOM a fieldGroup_), el DOM del campo se eliminará 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.

Pedido de validación

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

Cómo implementar un validador de clases

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. Esto se garantiza mediante validadores de clases y locales. El validador de clases sigue las mismas reglas que los validadores locales, excepto que también se ejecuta en el constructor y, por lo tanto, no debe hacer referencia al bloque de origen.

Para implementar el validador de clases de tu campo, anula la función doClassValidation_.

doClassValidation_(newValue) {
  if (typeof newValue != 'string') {
    return null;
  }
  return newValue;
};

Maneja 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.
  • Configura 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 quieres hacer lo siguiente:

  • Almacenamiento personalizado de newValue.
  • Cambia otras propiedades en función de 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;
}

Manejo de valores no válidos

Si el valor que se pasó 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 quieres 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 renderización_ para que la pantalla en bloque se actualice en función de displayValue_, en lugar de value_.

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

Valores de varias partes

Cuando tu campo contiene un valor multiparte (p.ej., listas, vectores y objetos), puedes desear que las partes se manejen 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 mostrar null (no válido). Almacenar en caché el objeto con propiedades validadas de forma individual permite que la función doValueInvalid_ las maneje por separado, simplemente mediante 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 por el momento no hay forma de aplicar este patrón.

isDirty_

isDirty_ es una marca que se usa en la función setValue y en otras partes del campo para determinar si se debe volver a renderizar. Si cambió el valor de visualización del campo, por lo general, isDirty_ se debe establecer como 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 al 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 oportuno. Puedes mostrar cualquier código HTML en tu editor uniendo 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 una caja conectada a un campo. Se posiciona automáticamente para estar cerca del campo mientras se mantiene 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 numéricos usan WidgetDiv para cubrir el campo con una casilla de entrada de texto HTML. Si bien DropDownDiv controla el posicionamiento por ti, no WidgetDiv. Los elementos deberán posicionarse manualmente. El sistema de coordenadas está en coordenadas de píxeles relativas a la parte 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);
}

Realice una limpieza

Tanto el DropDownDiv como el de WidgetDiv destruyen los elementos HTML del widget, pero debes descartar 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 DropDownDiv. En WidgetDiv, se llama en el contexto de WidgetDiv. En cualquier caso, es mejor usar la función bind cuando pasas una función de eliminación, como se muestra en los ejemplos DropDownDiv y WidgetDiv anteriores.

→ Si deseas obtener información sobre el desecho no específico de la eliminación de editores, consulta Cómo desechar.

Actualización de la pantalla bloqueada

La función render_ se usa para actualizar la visualización en bloque del campo a fin de que coincida con su valor interno.

A continuación, se muestran algunos ejemplos comunes:

  • Cambiar el texto (desplegable)
  • Cambiar el color (color)

Valores predeterminados

La función render_ predeterminada establece el texto de visualización en el resultado de la función getDisplayText_. La función getDisplayText_ muestra la propiedad value_ del campo transformada en una string, después de que se trunca para respetar la longitud máxima del texto.

Si usas la pantalla en bloque predeterminada 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 de tu campo tiene elementos estáticos adicionales, puedes llamar a la función render_ predeterminada, pero aún deberás anularla para actualizar el tamaño del campo.

Si el comportamiento de texto predeterminado no funciona para tu campo o la pantalla en bloque de tu campo tiene elementos dinámicos adicionales, deberás personalizar la función render_.

Diagrama de flujo en el que se describe cómo tomar la decisión de anular el procesamiento

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 la configuración de un texto de visualización personalizado y la modificación de los elementos de las imágenes hasta la actualización de los colores de fondo.

Todos los cambios en los atributos del DOM son legales. Lo único que debes recordar es el siguiente:

  1. La creación de DOM debe manejarse 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_();
}

Actualizando tamaño

Actualizar la propiedad size_ de un campo es muy importante, ya que le informa al código de renderización de bloques cómo posicionar el campo. La mejor manera de descubrir exactamente cuál debería ser ese size_ es experimentando.

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 adjuntos, debes anular el método applyColour. Deberá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;
  }
}

Actualizando la edición

La función updateEditable se puede usar para cambiar la forma en que aparece tu campo en función de si se puede editar o no. La función predeterminada hace que el fondo tenga o no una respuesta de desplazamiento (borde) si se puede editar o no. La pantalla bloqueada no debe cambiar de tamaño según su edición, pero están permitidos 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 pueda volver a cargarse en el lugar de trabajo más tarde.

El estado de tu lugar de trabajo siempre incluye el valor del campo, pero también podría incluir otro estado, como el estado de la IU de tu campo. Por ejemplo, si tu campo era un mapa con zoom que permitía al usuario seleccionar países, también podrías serializar el nivel de zoom.

Si tu campo se puede serializar, 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 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 JSON.

En algunos casos, no necesitas proporcionarlos, ya que las implementaciones predeterminadas funcionarán. Si (1) tu campo es una subclase directa de la clase Blockly.Field base, (2) el valor es un tipo serializable en JSON y (3) solo necesitas serializar el valor, la implementación predeterminada funcionará bien.

De lo contrario, la función saveState debería mostrar un objeto o valor serializable en JSON que represente el estado del campo. La 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']);
}

Serialización completa y datos de copia de seguridad

saveState también recibe un parámetro opcional doFullSerialization. Los campos que normalmente hacen referencia al estado serializado por un serializador diferente (como modelos de datos de copia de seguridad) lo usan. El parámetro indica que el estado al que se hace referencia no estará disponible cuando se deserializa el bloque, por lo que el campo debe realizar toda la serialización. Por ejemplo, esto es cierto cuando se serializa un bloque individual o cuando se copia y pega un bloque.

Dos casos de uso comunes son los siguientes:

  • Cuando se carga un bloque individual en un lugar de trabajo en el que no existe el modelo de datos de copia de seguridad, el campo tiene suficiente información en su propio estado para crear un modelo de datos nuevo.
  • Cuando se copia un bloque y se pega, el campo siempre crea un nuevo modelo de datos de copia de seguridad en lugar de hacer referencia a uno existente.

Uno de los campos que usa esto es el campo de variable integrada. Por lo general, 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 variable hace esto para garantizar que, si se carga en un lugar de trabajo en el que su variable no existe, pueda crear una variable nueva para hacer referencia.

toXml y fromXml

toXml y fromXml son hooks de serialización que funcionan con el sistema de serialización XML antiguo. Usa estos hooks solo si es necesario (p. ej., si trabajas en una base de código antigua que aún no se haya migrado). De lo contrario, usa saveState y loadState.

La función toXml debe mostrar un nodo XML que represente el estado del campo. Además, la 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. La configuración predeterminada es false. Si esta propiedad es true, es posible que debas proporcionar funciones de serialización y deserialización (consulta Serialización).

Personaliza el cursor

La propiedad CURSOR determina el cursor que verán los usuarios cuando se desplazan sobre tu campo. Debe ser una string de cursor CSS válida. El valor predeterminado es el cursor definido por .blocklyDraggable, que es el cursor de agarre.