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

新しいフィールド タイプを作成する前に、フィールドをカスタマイズする他の方法のいずれかがニーズに合っているかどうかを検討してください。アプリで新しい値の型を保存する必要がある場合や、既存の値の型に対して新しい 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 です。デフォルト値が望ましくない場合は、適切な値を渡してください。validator パラメータは編集可能なフィールドにのみ存在し、通常は省略可能としてマークされます。バリデーターの詳細については、バリデーターのドキュメントをご覧ください。

構造

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

  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 関数を使用します。このイベント バインディング方法では、ドラッグ中にセカンダリ タッチが除外されます。進行中のドラッグの途中でもハンドラを実行する場合は、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_ をオーバーライドして、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 を表示するには、DropDownDiv と WidgetDiv という 2 つの特別な div のいずれかで HTML をラップします。これらの 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 メソッドをオーバーライドする必要があります。ブロックの 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 の場合は、シリアル化関数と逆シリアル化関数を指定する必要がある場合があります(シリアル化を参照)。

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 を使用して設定します。