カスタム フィールドを作成する

新しいフィールドタイプを作成する前に、フィールドをカスタマイズするための他の方法のいずれかがニーズに合っているかどうかを検討してください。アプリケーションで新しい値の型を保存する必要がある場合や、既存の値の型に新しい 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 を呼び出して、フィールド クラスを 2 番目の引数として渡し、このマップにフィールド タイプを追加します。

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 の作成をカスタマイズする

フィールドが汎用テキスト フィールド(テキスト入力など)の場合、DOM の作成は自動的に処理されます。そうでない場合は、initView 関数をオーバーライドして、今後のフィールドのレンダリング時に必要な DOM 要素を作成する必要があります。

たとえば、プルダウン フィールドには画像とテキストの両方を含めることができます。initView では、1 つの画像要素と 1 つのテキスト要素が作成されます。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 関数を使用します。この方法でイベントをバインドすると、ドラッグ中の 2 番目のタップが除外されます。ドラッグの途中でもハンドラを実行する場合は、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_ をオーバーライドして、オンブロック ディスプレイが value_ ではなく displayValue_ に基づいて更新されるようにする必要があります。

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 をエディタに表示するには、Blockly の他の UI の上にフローティングする 2 つの特別な div(DropDownDiv と WidgetDiv)のいずれかにラップします。

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 のコンテキストで呼び出されます。どちらの場合も、上記の DropDownDivWidgetDiv の例に示すように、dispose 関数を渡すときに bind 関数を使用することをおすすめします。

→ エディタの破棄に固有ではない破棄については、破棄をご覧ください。

ブロック上のディスプレイの更新

render_ 関数は、フィールドのブロック内表示を内部値と一致するように更新するために使用されます。

一般的な例:

  • テキストを変更する(プルダウン)
  • 色を変更する(color)

デフォルト

デフォルトの render_ 関数は、表示テキストを getDisplayText_ 関数の結果に設定します。getDisplayText_ 関数は、テキストの最大長を考慮して切り捨てられた後、文字列にキャストされたフィールドの value_ プロパティを返します。

デフォルトのオンブロック表示を使用しており、デフォルトのテキスト動作がフィールドで機能する場合は、render_ をオーバーライドする必要はありません。

デフォルトのテキスト動作がフィールドで機能する場合でも、フィールドのブロック内表示に静的要素が追加されている場合は、デフォルトの render_ 関数を呼び出すことができますが、フィールドのサイズを更新するには、オーバーライドする必要があります。

フィールドでデフォルトのテキスト動作が機能しない場合や、フィールドのブロック内表示に追加の動的要素がある場合は、render_ 関数をカスタマイズする必要があります。

render_ をオーバーライドするかどうかを判断する方法を示したフローチャート

レンダリングのカスタマイズ

デフォルトのレンダリング動作がフィールドで機能しない場合は、カスタム レンダリング動作を定義する必要があります。カスタムの表示テキストの設定、画像要素の変更、背景色の更新など、さまざまなアクションが考えられます。

DOM 属性の変更はすべて有効です。ただし、次の 2 つの点に注意してください。

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

シリアル化

シリアル化とは、フィールドの状態を保存して、後でワークスペースに再読み込みできるようにすることです。

ワークスペースの状態には常にフィールドの値が含まれますが、フィールドの UI の状態など、他の状態も含まれる場合があります。たとえば、フィールドがズーム可能な地図で、ユーザーが国を選択できる場合は、ズームレベルをシリアル化することもできます。

フィールドがシリアル化可能である場合は、SERIALIZABLE プロパティを true に設定する必要があります。

Blockly には、フィールド用に 2 つのシリアル化フックが用意されています。1 組のフックは新しい JSON シリアル化システムで動作し、もう 1 組のフックは古い 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 も受け取ります。これは、通常は別のシリアライザ(バッキング データモデルなど)によってシリアル化された状態を参照するフィールドで使用されます。このパラメータは、ブロックが逆シリアル化されたときに参照される状態が使用できなくなることを示します。そのため、フィールドはすべてのシリアル化を自身で行う必要があります。たとえば、個々のブロックがシリアル化される場合や、ブロックがコピー ペーストされる場合がこれに該当します。

一般的なユースケースは次のとおりです。

  • 個々のブロックが、バッキング データモデルが存在しないワークスペースに読み込まれると、フィールドには新しいデータモデルを作成するために十分な情報が含まれます。
  • ブロックをコピーして貼り付けると、フィールドは既存のデータモデルを参照するのではなく、常に新しいバッキング データモデルを作成します。

組み込み変数フィールドは、このフィールドの 1 つです。通常は、参照している変数の 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 で定義されたカーソル(グラブ カーソル)になります。