在创建新的字段类型之前,请考虑其他自定义字段的方法是否能满足您的需求。如果您的应用需要存储新的值类型,或者您希望为现有值类型创建新的界面,则可能需要创建新的字段类型。
如需创建新字段,请执行以下操作:
- 实现构造函数。
- 注册 JSON 密钥并实现
fromJson
。 - 处理块内界面和事件监听器的初始化。
- 处理事件监听器的处置(系统会为您处理界面处置)。
- 实现价值处理。
- 添加字段值的文本表示形式,以提高无障碍功能。
- 添加其他功能,例如:
- 配置字段的其他方面,例如:
本部分假定您已阅读并熟悉字段剖析中的内容。
如需查看自定义字段的示例,请参阅自定义字段演示。
实现构造函数
字段的构造函数负责设置字段的初始值,并可选择设置本地验证器。无论源块是在 JSON 中还是在 JavaScript 中定义的,在源块初始化期间都会调用自定义字段的构造函数。因此,自定义字段在构造期间无法访问源块。
以下代码示例创建了一个名为 GenericField
的自定义字段:
class GenericField extends Blockly.Field {
constructor(value, validator) {
super(value, validator);
this.SERIALIZABLE = true;
}
}
方法签名
字段构造函数通常会接收一个值和一个本地验证器。该值是可选的,如果您未传递值(或传递的值未能通过类验证),则系统会使用超类的默认值。对于默认的 Field
类,该值为 null
。如果您不想要该默认值,请务必传递合适的值。验证器形参仅适用于可修改的字段,通常标记为可选。如需详细了解验证器,请参阅验证器文档。
结构
构造函数中的逻辑应遵循以下流程:
- 调用继承的超类构造函数(所有自定义字段都应从
Blockly.Field
或其某个子类继承),以正确初始化值并为您的字段设置本地验证器。 - 如果您的字段可序列化,请在构造函数中设置相应的属性。可编辑的字段必须可序列化,并且字段默认可编辑,因此您可能应将此属性设置为 true,除非您知道它不应可序列化。
- 可选:应用其他自定义设置(例如,标签字段允许传递 CSS 类,然后将其应用于文本)。
JSON 和注册
在 JSON 代码块定义中,字段由字符串(例如 field_number
、field_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_
期间,它会根据所选选项的类型显示有效元素并隐藏其他元素。
创建 DOM 元素可以使用 Blockly.utils.dom.createSvgElement
方法,也可以使用传统的 DOM 创建方法。
字段的块内显示要求如下:
- 所有 DOM 元素都必须是字段
fieldGroup_
的子元素。系统会自动创建字段组。 - 所有 DOM 元素都必须位于字段的报告维度内。
如需详细了解如何自定义和更新块内显示内容,请参阅呈现部分。
添加文字符号
如果您想向字段的文本添加符号(例如角度字段的度数符号),可以直接将符号元素(通常包含在 <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_
函数结束时,如果任何单个属性无效,则在返回 null
(无效)之前,该值会缓存到 cacheValidatedValue_
属性。缓存具有单独验证的属性的对象,可让 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 与 WidgetDiv
DropDownDiv
用于提供位于与字段连接的框内的编辑器。它会自动定位到靠近字段的位置,同时保持在可见范围内。角度选择器和颜色选择器是 DropDownDiv
的绝佳示例。
WidgetDiv
用于提供不在盒子内的编辑器。数字字段使用 WidgetDiv 通过 HTML 文本输入框覆盖该字段。虽然 DropDownDiv 会为您处理定位,但 WidgetDiv 不会。元素将需要手动定位。坐标系采用相对于窗口左上角的像素坐标。文本输入编辑器是 WidgetDiv
的一个很好的示例。
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));
}
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
函数在 DropDownDiv
上的 null
上下文中调用。在 WidgetDiv
上,它是在 WidgetDiv
的上下文中调用的。在任一情况下,传递处置函数时最好使用 bind 函数,如上述 DropDownDiv
和 WidgetDiv
示例所示。
→ 如需了解与处置编辑器无关的处置信息,请参阅处置。
更新了锁屏显示内容
render_
函数用于更新字段的块内显示,使其与内部值保持一致。
常见示例包括:
- 更改文本(下拉菜单)
- 更改颜色(颜色)
默认值
默认的 render_
函数会将显示文本设置为 getDisplayText_
函数的结果。getDisplayText_
函数会返回字段的 value_
属性(转换为字符串),前提是该属性已截断,以符合最大文本长度。
如果您使用的是默认的块内显示,并且默认的文本行为适用于您的字段,则无需替换 render_
。
如果默认文本行为适用于您的字段,但您的字段的块内显示具有其他静态元素,您可以调用默认的 render_
函数,但仍需要替换该函数以更新字段的大小。
如果默认文本行为不适用于您的字段,或者您的字段的块内显示具有其他动态元素,您将需要自定义 render_
函数。
自定义呈现
如果默认呈现行为不适用于您的字段,您将需要定义自定义呈现行为。这可能涉及各种操作,从设置自定义显示文本到更改图片元素,再到更新背景颜色。
所有 DOM 属性更改都是合法的,只需记住以下两点:
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
方法。您需要通过块的样式属性访问颜色。
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 序列化系统。
saveState
和loadState
saveState
和 loadState
是与新的 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
。此属性由通常引用由其他序列化程序(例如后备数据模型)序列化的状态的字段使用。该形参表示,在反序列化块时,所引用的状态将不可用,因此相应字段应自行完成所有序列化操作。例如,当序列化单个块或复制粘贴块时,此值为 true。
以下是两种常见的应用场景:
- 当某个块被加载到不存在支持数据模型的工作区时,该字段自身的状态包含足够的信息来创建新的数据模型。
- 复制粘贴块时,该字段始终会创建一个新的支持数据模型,而不是引用现有模型。
使用此字段的一个示例是内置变量字段。通常,它会序列化所引用变量的 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());
}
变量字段之所以这样做,是为了确保在加载到不存在相应变量的工作区时,它可以创建新的变量来引用。
toXml
和fromXml
toXml
和 fromXml
是与旧版 XML 序列化系统搭配使用的序列化钩子。仅在必须使用这些钩子时(例如,您正在处理尚未迁移的旧版代码库)才使用它们,否则请使用 saveState
和 loadState
。
您的 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
,您可能需要提供序列化和反序列化函数(请参阅序列化)。
使用 CSS 进行自定义
您可以使用 CSS 自定义字段。在 initView
方法中,向字段的 fieldGroup_
添加自定义类,然后在 CSS 中引用此类。
例如,如需使用其他光标,请执行以下操作:
initView() {
...
// Add a custom CSS class.
if (this.fieldGroup_) {
Blockly.utils.dom.addClass(this.fieldGroup_, 'myCustomField');
}
}
.myCustomField {
cursor: cell;
}
自定义光标
默认情况下,扩展 FieldInput
的类在用户将鼠标悬停在字段上时使用 text
光标,正在拖动的字段使用 grabbing
光标,所有其他字段使用 default
光标。如果您想使用其他光标,请使用 CSS 进行设置。