新しいフィールドタイプの作成

新しいフィールド タイプを作成する前に、フィールドをカスタマイズするための他の方法がニーズに適しているかどうかを検討してください。アプリで新しい値の型を保存する必要がある場合や、既存の値の型に新しい UI を作成する場合は、おそらく新しいフィールド型を作成する必要があります。

新しいフィールドを作成する手順は次のとおりです。

  1. コンストラクタを実装します
  2. JSON キーを登録して fromJson を実装します
  3. ブロック上の UI とイベント リスナーの初期化を処理する
  4. イベント リスナーの廃棄を処理する(UI の廃棄は自動的に処理されます)。
  5. 値の処理を実装します
  6. ユーザー補助機能のために、フィールドの値のテキスト表現を追加する
  7. 次のような機能を追加します。
  8. 次のようなフィールドの追加要素を構成します。

このセクションは、フィールドの構造の内容を読み、理解していることを前提としています。

カスタム フィールドの例については、カスタム フィールドのデモをご覧ください。

コンストラクタの実装

フィールドのコンストラクタは、フィールドの初期値を設定し、必要に応じてローカル バリデータをセットアップします。カスタム フィールドのコンストラクタは、ソースブロックが JSON と JavaScript のどちらで定義されているかにかかわらず、ソースブロックの初期化中に呼び出されます。そのため、カスタム フィールドは作成中の source ブロックにアクセスできません。

次のコードサンプルでは、GenericField という名前のカスタム フィールドを作成します。

class GenericField extends Blockly.Field {
  constructor(value, validator) {
    super(value, validator);

    this.SERIALIZABLE = true;
  }
}

メソッド シグネチャ

フィールド コンストラクタは通常、値とローカルバリデータを受け取ります。値は省略可能です。値を渡さない場合(またはクラス検証に失敗する値を渡す)場合は、スーパークラスのデフォルト値が使用されます。デフォルトの Field クラスの場合、この値は null です。このデフォルト値を使用しない場合は、適切な値を渡してください。Validator パラメータは編集可能なフィールドにのみ使用され、通常はオプションとマークされます。バリデータについて詳しくは、バリデータのドキュメントをご覧ください。

構造

コンストラクタ内のロジックは、次のフローに従う必要があります。

  1. 継承されたスーパー コンストラクタを呼び出して(すべてのカスタム フィールドは Blockly.Field またはそのサブクラスのいずれかから継承する必要があります)、値を適切に初期化してフィールドのローカル バリデータを設定します。
  2. フィールドがシリアル化可能な場合は、対応するプロパティをコンストラクタに設定します。編集可能なフィールドはシリアル化可能でなければならず、フィールドはデフォルトで編集可能です。そのため、シリアル化可能でないことがわかっている場合を除き、通常はこのプロパティを true に設定することをおすすめします。
  3. 省略可: 追加のカスタマイズを適用します(たとえば、ラベル フィールドを使用すると、CSS クラスを渡してテキストに適用できます)。

JSON と登録

JSON ブロック定義では、フィールドは文字列(例: field_numberfield_textinput)で記述します。これらの文字列からフィールド オブジェクトへのマップをブロック的に維持し、作成時に適切なオブジェクトの 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 が作成され、モデルが構築され(フィールドにモデルがある場合)、イベントがバインドされます。

On-Block ディスプレイ

初期化中は、フィールドのブロック上のディスプレイに必要なものをすべて作成する必要があります。

デフォルト、背景、テキスト

デフォルトの initView 関数は、ライトカラーの rect 要素と text 要素を作成します。フィールドに両方の要素とその他の要素を含める場合は、残りの DOM 要素を追加する前にスーパークラスの initView 関数を呼び出します。フィールドにこれらの要素の一方だけを設定したい場合は、createBorderRect_ 関数または createTextElement_ 関数を使用します。

DOM 構成のカスタマイズ

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

たとえば、プルダウン フィールドには画像とテキストの両方を含めることができます。initView で、1 つの画像要素と 1 つのテキスト要素が作成されます。次に、render_ の間に、選択したオプションのタイプに基づいて、アクティブな要素を表示し、他方の要素を非表示にします。

DOM 要素の作成には、Blockly.utils.dom.createSvgElement メソッドか従来の DOM 作成メソッドを使用できます。

フィールドのオンブロック表示の要件は次のとおりです。

  • すべての DOM 要素は、フィールドの fieldGroup_ の子である必要があります。フィールド グループは自動的に作成されます。
  • すべての DOM 要素は、フィールドのレポートのディメンション内に収まる必要があります。

オンブロック ディスプレイのカスタマイズと更新について詳しくは、レンダリング セクションをご覧ください。

テキスト記号を追加する

フィールドのテキストに記号(Angle フィールドの次数記号など)を追加する場合は、記号要素(通常は <tspan> に含まれています)をフィールドの textElement_ に直接追加できます。

入力イベント

デフォルトでは、ツールチップ イベントとマウスダウン イベント(エディタの表示に使用)が登録されます。他の種類のイベントをリッスンする場合(フィールドでのドラッグを処理する場合など)は、フィールドの 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 に設定し、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_ 関数の最後で、個々のプロパティが無効な場合、値は 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 を表示するには、DropDownDiv と WidgetDiv という 2 つの特別な div の 1 つをラップします。この 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 のコンテキストで呼び出されます。いずれの場合も、上記の DropDownDivWidgetDiv の例で示しているように、破棄関数を渡すときに bind 関数を使用することをおすすめします。

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

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

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

一般的な例:

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

デフォルト

デフォルトの 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 メソッドをオーバーライドする必要があります。色には、ブロックのスタイル プロパティからアクセスできます。

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 つのシリアル化フックが用意されています。フックの一方のペアは新しい 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 も受け取ります。これは、通常、別のシリアライザによってシリアル化された状態を参照するフィールド(バッキング データモデルなど)で使用されます。このパラメータは、ブロックのシリアル化解除時に参照された状態が利用できないことを通知するため、フィールド自体がシリアル化のすべてを行う必要があります。これは、たとえば個々のブロックがシリアル化される場合や、ブロックがコピー&ペーストされる場合に当てはまります。

これには、次の 2 つの一般的なユースケースがあります。

  • 個々のブロックが、バッキング データモデルが存在しないワークスペースに読み込まれると、フィールドには、新しいデータモデルを作成するのに十分な情報が独自の状態で存在します。
  • ブロックがコピー&ペーストされると、フィールドでは既存のデータモデルを参照するのではなく、常に新しいバッキング データモデルが作成されます。

このフィールドを使用するフィールドの 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 で定義されたカーソル(取得カーソル)が使用されます。