建立自訂欄位

建立新欄位類型前,請先考慮其他欄位自訂方法是否符合需求。如果應用程式需要儲存新的值類型,或是您想為現有的值類型建立新的 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、模型 (如果欄位有模型) 和繫結事件。

On-Block 顯示

在初始化期間,您有責任建立欄位在區塊顯示時所需的一切項目。

預設值、背景和文字

預設的 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,並覆寫 on-block 顯示的 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();
  }
}

系統會在 DropDownDivnull 環境中呼叫 dispose 函式。在 WidgetDiv 上,系統會在 WidgetDiv 的環境中呼叫該函式。無論是哪種情況,傳遞處置函式時最好使用 bind 函式,如上述 DropDownDivWidgetDiv 範例所示。

→ 如需處置編輯器以外的項目,請參閱「處置」。

更新區塊內顯示畫面

render_ 函式用於更新欄位的區塊內顯示內容,使其與內部值相符。

常見範例包括:

  • 變更文字 (下拉式選單)
  • 變更顏色 (顏色)

預設值

預設 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 提供兩組欄位序列化掛鉤。其中一組 Hook 適用於新的 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 序列化系統。請只在必要時使用這些 Hook (例如您正在處理尚未遷移的舊版程式碼集),否則請使用 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,您可能需要提供序列化和還原序列化函式 (請參閱「序列化」)。

使用 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 設定