建立自訂欄位

建立新欄位類型前,請先考量其他方法是否能滿足您自訂欄位的需求。如果應用程式需要儲存新的值類型,或是您想要為現有值類型建立新的 UI,可能就需要建立新的欄位類型。

如要建立新欄位,請按照下列步驟操作:

  1. 實作建構函式
  2. 註冊 JSON 鍵並實作 fromJson
  3. 處理區塊內 UI 和事件事件監聽器的初始化作業
  4. 處理事件監聽器的處置作業 (系統會為您處理 UI 處置作業)。
  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 元素。如果您希望欄位同時具備這兩項功能,以及其他額外功能,請先呼叫超類別 initView 函式,再新增其他 DOM 元素。如果您希望欄位包含其中一個元素 (但不包含兩個),可以使用 createBorderRect_createTextElement_ 函式。

自訂 DOM 建構

如果欄位是一般文字欄位 (例如 Text Input),系統會為您處理 DOM 建構作業。否則,您必須覆寫 initView 函式,以便在日後轉譯欄位時建立所需的 DOM 元素。

舉例來說,下拉式欄位可能會同時包含圖片和文字。在 initView 中,它會建立單一圖片元素和單一文字元素。接著,在 render_ 期間,系統會根據所選選項的類型,顯示有效元素並隱藏其他元素。

您可以使用 Blockly.utils.dom.createSvgElement 方法或傳統 DOM 建立方法建立 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_ 函式結束時,如果任何個別屬性無效,系統會將該值快取到 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,方法是將 HTML 包裝在兩個特殊 div 之一 (稱為 DropDownDiv 和 WidgetDiv),這些 div 會浮動在 Blockly 的其他 UI 上方。

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 都會處理毀損小工具 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 方法。您需要透過區塊的樣式屬性存取顏色。

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

序列化

序列化是指儲存欄位的狀態,以便日後重新載入至工作區。

工作區的狀態一律包含欄位的值,但也可能包含其他狀態,例如欄位 UI 的狀態。舉例來說,如果您的欄位是可縮放的地圖,可讓使用者選取國家/地區,您也可以序列化縮放等級。

如果欄位可序列化,您必須將 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。這項屬性會由通常參照由不同序列化器 (例如支援資料模型) 序列化的狀態的欄位使用。這個參數會指出在區塊經過反序列化時,參照的狀態將無法使用,因此欄位應自行執行所有序列化作業。舉例來說,當個別區塊序列化或區塊複製貼上時,就會是 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());
}

變數欄位會執行這項操作,確保在載入至變數不存在的工作區時,能夠建立新的變數以供參照。

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 屬性會決定欄位是否應提供 UI,指出可與其互動。預設為 true

SERIALIZABLE 屬性會決定是否要序列化欄位。預設值為 false。如果此屬性為 true,您可能需要提供序列化和反序列化函式 (請參閱「序列化」)。

自訂游標

CURSOR 屬性會決定使用者將滑鼠游標懸停在欄位上時,看到的滑鼠游標。應為有效的 CSS 游標字串。預設為 .blocklyDraggable 定義的游標,也就是抓取游標。