创建自定义字段

在创建新字段类型之前,请考虑是否有其他自定义字段的方法可满足您的需求。如果您的应用需要存储新的值类型,或者您希望为现有值类型创建新的界面,则可能需要创建新的字段类型。

如需创建新字段,请执行以下操作:

  1. 实现构造函数
  2. 注册 JSON 密钥并实现 fromJson
  3. 处理区块级界面和事件监听器的初始化
  4. 处理事件监听器的处置(系统会为您处理界面处置)。
  5. 实现值处理
  6. 为提高可访问性,添加字段值的文本表示法
  7. 添加其他功能,例如:
  8. 配置字段的其他方面,例如:

本部分假定您已阅读并熟悉字段详解中的内容。

如需查看自定义字段示例,请参阅“自定义字段”演示

实现构造函数

字段的构造函数负责设置字段的初始值,并可选择设置本地验证器。无论源代码块是使用 JSON 还是 JavaScript 定义的,系统都会在源代码块初始化期间调用自定义字段的构造函数。因此,自定义字段在构建期间无法访问源代码块。

以下代码示例会创建一个名为 GenericField 的自定义字段:

class GenericField extends Blockly.Field {
  constructor(value, validator) {
    super(value, validator);

    this.SERIALIZABLE = true;
  }
}

方法签名

字段构造函数通常接受一个值和一个本地验证器。此值为可选值,如果您未传递值(或传递的值未通过类验证),则系统会使用父类的默认值。对于默认的 Field 类,该值为 null。如果您不希望使用该默认值,请务必传递合适的值。验证器参数仅适用于可修改的字段,通常标记为可选。如需详细了解验证器,请参阅“验证器”文档

结构

构造函数中的逻辑应遵循以下流程:

  1. 调用继承的超级构造函数(所有自定义字段都应从 Blockly.Field 或其某个子类继承),以正确初始化值并为字段设置本地验证器。
  2. 如果您的字段可序列化,请在构造函数中设置相应的属性。可修改的字段必须可序列化,并且字段默认可修改,因此除非您知道字段不应可序列化,否则应将此属性设置为 true。
  3. 可选:应用其他自定义(例如,标签字段允许传递 CSS 类,然后将其应用于文本)。

JSON 和注册

JSON 代码块定义中,字段由字符串(例如 field_numberfield_textinput)描述。Blockly 会维护一个将这些字符串映射到字段对象的映射,并在构建期间对相应对象调用 fromJson

调用 Blockly.fieldRegistry.register 将字段类型添加到此映射中,并将字段类作为第二个实参传入:

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

您还需要定义 fromJson 函数。您的实现应先使用 replaceMessageReferences 解引用对任何本地化令牌的引用,然后将值传递给构造函数。

GenericField.fromJson = function(options) {
  const value = Blockly.utils.parsing.replaceMessageReferences(
      options['value']);
  return new CustomFields.GenericField(value);
};

正在初始化

构建字段后,它基本上只包含一个值。在初始化过程中,系统会构建 DOM、构建模型(如果字段具有模型)并绑定事件。

屏幕上显示

在初始化期间,您负责创建字段在屏幕上显示所需的所有内容。

默认值、背景和文本

默认的 initView 函数会创建浅色 rect 元素和 text 元素。如果您希望字段同时具有这两种功能以及一些额外的功能,请在添加其余 DOM 元素之前调用父类 initView 函数。如果您希望字段具有其中一个元素(而不是同时具有这两个元素),可以使用 createBorderRect_createTextElement_ 函数。

自定义 DOM 构建

如果您的字段是通用文本字段(例如 Text Input),系统会为您处理 DOM 构建。否则,您需要替换 initView 函数,以创建您在日后呈现字段时需要的 DOM 元素。

例如,下拉菜单字段可能同时包含图片和文字。在 initView 中,它会创建一个图片元素和一个文本元素。然后,在 render_ 期间,它会根据所选选项的类型显示有效元素并隐藏其他元素。

您可以使用 Blockly.utils.dom.createSvgElement 方法或传统 DOM 创建方法创建 DOM 元素。

字段的广告展示位置要求如下:

  • 所有 DOM 元素都必须是字段的 fieldGroup_ 的子元素。系统会自动创建字段组。
  • 所有 DOM 元素都必须位于字段的报告尺寸内。

如需详细了解如何自定义和更新展示位置广告,请参阅呈现部分。

添加文本符号

如果您想向字段的文本添加符号(例如 Angle 字段的度符号),可以直接将符号元素(通常包含在 <tspan> 中)附加到字段的 textElement_

输入事件

默认情况下,字段会注册提示事件和 mousedown 事件(用于显示编辑器)。如果您想监听其他类型的事件(例如,处理对字段的拖动操作),则应替换字段的 bindEvents_ 函数。

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;
      }
  );
}

如需绑定到事件,您通常应使用 Blockly.utils.browserEvents.conditionalBind 函数。这种绑定事件的方法会滤除拖动期间的次要轻触。如果您希望处理脚本即使在拖动过程中也能运行,可以使用 Blockly.browserEvents.bind 函数。

处置

如果您在字段的 bindEvents_ 函数内注册了任何自定义事件监听器,则需要在 dispose 函数内取消注册这些监听器。

如果您正确初始化了字段的视图(通过将所有 DOM 元素附加到 fieldGroup_),则系统会自动处理字段的 DOM。

值处理

→ 如需了解字段的值与文本的区别,请参阅字段剖析

验证顺序

说明验证程序运行顺序的流程图

实现类验证器

字段应仅接受特定值。例如,数字字段应仅接受数字,颜色字段应仅接受颜色等。这可通过类和本地验证器来确保。类验证器遵循与本地验证器相同的规则,但它也会在构造函数中运行,因此不应引用源代码块。

如需实现字段的类验证器,请替换 doClassValidation_ 函数。

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

处理有效值

如果传递给使用 setValue 的字段的值有效,您将收到 doValueUpdate_ 回调。默认情况下,doValueUpdate_ 函数:

  • value_ 属性设置为 newValue
  • isDirty_ 属性设置为 true

如果您只需存储该值,而不想执行任何自定义处理,则无需替换 doValueUpdate_

否则,如果您想执行以下操作:

  • newValue 的自定义存储空间。
  • 根据 newValue 更改其他属性。
  • 保存当前值是否有效。

您需要替换 doValueUpdate_

doValueUpdate_(newValue) {
  super.doValueUpdate_(newValue);
  this.displayValue_ = newValue;
  this.isValueValid_ = true;
}

处理无效值

如果使用 setValue 向字段传递的值无效,您将收到 doValueInvalid_ 回调。默认情况下,doValueInvalid_ 函数不执行任何操作。这意味着,默认情况下,系统不会显示无效值。这也意味着,系统不会重新渲染该字段,因为不会设置 isDirty_ 属性。

如果您想显示无效值,则应替换 doValueInvalid_。在大多数情况下,您应将 displayValue_ 属性设置为无效值,将 isDirty_ 设置为 true,并替换 render_,以便基于 displayValue_(而非 value_)更新区块内显示内容。

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

多部分值

如果字段包含多部分值(例如列表、矢量、对象),您可能希望将各部分像单个值一样处理。

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;
}

在上面的示例中,newValue 的每个属性都会单独进行验证。然后,在 doClassValidation_ 函数结束时,如果任何单个属性无效,系统会先将值缓存到 cacheValidatedValue_ 属性,然后再返回 null(无效)。通过缓存包含单独验证的属性的对象,doValueInvalid_ 函数只需执行 !this.cacheValidatedValue_.property 检查,即可单独处理这些属性,而无需单独重新验证每个属性。

用于验证多部分值的此模式也可以在本地验证器中使用,但目前无法强制执行此模式。

isDirty_

isDirty_setValue 函数以及字段的其他部分中使用的标志,用于确定是否需要重新渲染字段。如果字段的显示值已更改,isDirty_ 通常应设置为 true

文本

→ 如需了解字段文本的使用位置以及它与字段值的区别,请参阅字段剖析

如果字段的文本与字段的值不同,您应替换 getText 函数以提供正确的文本。

getText() {
  let text = this.value_.turtleName + ' wearing a ' + this.value_.hat;
  if (this.value_.hat == 'Stovepipe' || this.value_.hat == 'Propeller') {
    text += ' hat';
  }
  return text;
}

创建编辑器

如果您定义了 showEditor_ 函数,Blockly 会自动监听点击,并在适当的时间调用 showEditor_。您可以通过将任何 HTML 封装在两个特殊的 div(称为 DropDownDiv 和 WidgetDiv)中,在编辑器中显示该 HTML。这两个 div 会浮动在 Blockly 界面的其余部分上方。

DropDownDiv 用于提供位于与字段关联的框内的编辑器。它会自动将自己定位在相应字段附近,同时保持在可见边界内。角度选择器和颜色选择器就是 DropDownDiv 的很好示例。

角度选择器的图片

WidgetDiv 用于提供不位于框架内的编辑器。数字字段使用 WidgetDiv 将 HTML 文本输入框覆盖在字段上。虽然 DropDownDiv 会为您处理定位,但 WidgetDiv 不会。需要手动放置元素。坐标系以相对于窗口左上角的像素坐标为单位。文本输入编辑器就是 WidgetDiv 的一个很好的示例。

文本输入编辑器的图片

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));
}

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);
}

清理

DropDownDiv 和 WidgetDiv 都会处理销毁 widget HTML 元素,但您需要手动处理已应用于这些元素的所有事件监听器。

widgetDispose_() {
  for (let i = this.editorListeners_.length, listener;
      listener = this.editorListeners_[i]; i--) {
    Blockly.browserEvents.unbind(listener);
    this.editorListeners_.pop();
  }
}

dispose 函数在 DropDownDivnull 上下文中调用。在 WidgetDiv 上,它会在 WidgetDiv 的上下文中调用。无论是哪种情况,在传递 Dispose 函数时,最好使用 bind 函数,如上面的 DropDownDivWidgetDiv 示例所示。

→ 如需了解与编辑器无关的处置操作,请参阅处置

更新区块内显示内容

render_ 函数用于更新字段在屏幕上的显示方式,使其与内部值相匹配。

常见示例包括:

  • 更改文本(下拉菜单)
  • 更改颜色 (color)

默认值

默认的 render_ 函数会将显示文本设置为 getDisplayText_ 函数的结果。getDisplayText_ 函数会返回字段的 value_ 属性(经过截断以遵守文本长度上限)转换为字符串。

如果您使用的是默认的“在屏幕上显示”方式,并且默认文本行为适用于您的字段,则无需替换 render_

如果默认文本行为适用于您的字段,但字段的块级显示包含其他静态元素,您可以调用默认的 render_ 函数,但仍需要替换它以更新字段的大小

如果默认文本行为不适用于您的字段,或者字段的屏幕内显示内容包含其他动态元素,您需要自定义 render_ 函数

描述如何确定是否替换 render_ 的流程图

自定义呈现

如果默认的呈现行为不适用于您的字段,您需要定义自定义呈现行为。这可以包括从设置自定义显示文本到更改图片元素再到更新背景颜色等任何操作。

所有 DOM 属性更改都是合法的,您只需记住以下两点:

  1. 应在初始化期间处理 DOM 创建,因为这样更高效。
  2. 您应始终更新 size_ 属性,使其与区块内显示屏的尺寸相匹配。
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_();
}

更新大小

更新字段的 size_ 属性非常重要,因为它会告知块呈现代码如何定位字段。若要确定 size_ 的确切值,最好的方法是进行实验。

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;
}

匹配的块颜色

如果您希望字段的元素与其所附加的块的颜色一致,则应替换 applyColour 方法。您需要通过块的 style 属性访问颜色。

applyColour() {
  const sourceBlock = this.sourceBlock_;
  if (sourceBlock.isShadow()) {
    this.arrow_.style.fill = sourceBlock.style.colourSecondary;
  } else {
    this.arrow_.style.fill = sourceBlock.style.colourPrimary;
  }
}

更新可编辑性

updateEditable 函数可用于更改字段的显示方式,具体取决于字段是否可修改。默认函数会使背景在可/不可修改时具有/不具有悬停响应(边框)。广告展示区域不应根据其可编辑性而更改大小,但允许进行所有其他更改。

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;
  }
}

序列化

序列化是指保存字段的状态,以便稍后将其重新加载到工作区中。

工作区的状态始终包含字段的值,但也可能包含其他状态,例如字段界面的状态。例如,如果您的字段是一个可缩放的地图,允许用户选择国家/地区,您还可以序列化缩放级别。

如果您的字段可序列化,您必须将 SERIALIZABLE 属性设置为 true

Blockly 为字段提供了两组序列化钩子。一对钩子适用于新的 JSON 序列化系统,另一对钩子适用于旧的 XML 序列化系统。

saveStateloadState

saveStateloadState 是可与新 JSON 序列化系统搭配使用的序列化钩子。

在某些情况下,您无需提供这些,因为默认实现即可正常运行。如果 (1) 您的字段是基准 Blockly.Field 类的直接子类,(2) 您的值是 JSON 可序列化类型,并且 (3) 您只需序列化该值,那么默认实现就非常适合!

否则,您的 saveState 函数应返回一个 JSON 可序列化对象/值,该对象/值表示字段的状态。您的 loadState 函数应接受相同的 JSON 可序列化对象/值,并将其应用于该字段。

saveState() {
  return {
    'country': this.getValue(),  // Value state
    'zoom': this.getZoomLevel(), // UI state
  };
}

loadState(state) {
  this.setValue(state['country']);
  this.setZoomLevel(state['zoom']);
}

完整序列化和后备数据

saveState 还会接收一个可选参数 doFullSerialization。通常引用由其他 serializer(例如后备数据模型)序列化的状态的字段会使用此属性。该参数表示在反序列化块时,引用的状态将不可用,因此该字段应自行执行所有序列化操作。例如,在序列化单个代码块或复制粘贴代码块时,就会出现这种情况。

这方面的两个常见用例如下:

  • 当单个分块被加载到不存在后备数据模型的工作区中时,该字段在其自身状态中拥有足够的信息来创建新的数据模型。
  • 复制粘贴某个代码块时,该字段始终会创建新的后备数据模型,而不是引用现有数据模型。

内置变量字段就是一个使用此属性的字段。通常,它会序列化其引用的变量的 ID,但如果 doFullSerialization 为 true,则会序列化其所有状态。

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());
}

变量字段执行此操作是为了确保,如果它被加载到其变量不存在的工作区中,则可以创建一个新变量以供引用。

toXmlfromXml

toXmlfromXml 是与旧版 XML 序列化系统配合使用的序列化钩子。仅在必要时(例如,您正在处理尚未迁移的旧代码库)使用这些钩子,否则请使用 saveStateloadState

您的 toXml 函数应返回一个表示字段状态的 XML 节点。您的 fromXml 函数应接受相同的 XML 节点并将其应用于字段。

toXml(fieldElement) {
  fieldElement.textContent = this.getValue();
  fieldElement.setAttribute('zoom', this.getZoomLevel());
  return fieldElement;
}

fromXml(fieldElement) {
  this.setValue(fieldElement.textContent);
  this.setZoomLevel(fieldElement.getAttribute('zoom'));
}

可修改和可序列化属性

EDITABLE 属性用于确定该字段是否应具有界面来指示用户可以与其互动。默认值为 true

SERIALIZABLE 属性用于确定是否应对字段进行序列化。默认值为 false。如果此属性为 true,您可能需要提供序列化和反序列化函数(请参阅序列化)。

自定义光标

CURSOR 属性决定了用户在将光标悬停在字段上时看到的光标。它应为有效的 CSS 光标字符串。默认情况下,此值为 .blocklyDraggable 定义的光标,即抓取光标。