建立新的欄位類型

建立新的欄位類型之前,請考慮用其他方法來自訂欄位類型是否符合需求。如果應用程式需要儲存新的值類型,或者您想為現有值類型建立新 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) 來描述。阻擋從這些字串到欄位物件的對應,並在建構期間針對適當的物件呼叫 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 建構

如果您的欄位是一般文字欄位 (例如「文字輸入」),系統會為您處理 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_ 函式結尾,如果有任何個別屬性無效,該值會在傳回 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),以便在 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 處理常式會刪除小工具 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_ 函式

說明如何決定是否要覆寫轉譯的流程圖_

自訂轉譯

如果預設轉譯行為不適用於您的欄位,您需要定義自訂顯示行為。這包括設定自訂顯示文字、變更圖片元素,或是更新背景顏色等。

所有 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

為欄位提供兩組序列化掛鉤。一組掛鉤可與新的 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。這適用於通常會參照不同序列化程式 (例如備份資料模型) 序列化狀態的欄位。參數表示在區塊反序列化時將無法使用參照的狀態,因此該欄位應執行所有序列化作業。例如,當個別區塊序列化或複製區塊時,情況都是如此。

有兩種常見用途:

  • 如果將個別區塊載入的工作區不存在備份資料模型,則該欄位本身狀態的資訊就足以建立新的資料模型。
  • 複製區塊時,欄位一律會建立新的備份資料模型,而非參照現有模型。

系統會使用一個內建變數欄位。通常,它會序列化其參照的變數 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 定義的遊標,也就是擷取遊標。